首页 > 解决方案 > F#:配管 vs. 作曲 vs. ......作曲?

问题描述

我对一切都很陌生——F#、一般的编程和这个社区。我是一名数学家,在本科期间短暂接触过计算机科学。我正在尝试在 F# 中完成一些任务,而“F# 备忘单”展示了似乎是三种不同的方法来组合函数而没有解释重复。这是链接中的相关信息,以了解我的意思。

let 关键字还定义了命名函数。

let negate x = x * -1 
let square x = x * x 
let print x = printfn "The number is: %d" x

let squareNegateThenPrint x = 
print (negate (square x)) 

管道运算符 |> 用于将函数和参数链接在一起。双反引号标识符可以方便地提高可读性,尤其是在单元测试中:

let ``square, negate, then print`` x = 
    x |> square |> negate |> print

组合运算符 >> 用于组合函数:

let squareNegateThenPrint' = 
    square >> negate >> print

通过检查并在 VS F# 中与功能交互:

  1. squareNegateThenPrint x
  2. ``平方,取反,然后打印'' x
  3. 平方否定然后打印'

看起来这是完成完全相同的事情的 3 种方法的列表,这里有任何细微差别吗?我相信给定相同的 int 他们都会返回相同的 int,但除此之外呢?我没看到什么?这三种方法各自的优点和缺点是什么?

2 和 3 都使用“运算符”,而 1 似乎是组合函数以从旧函数创建新函数的常用“数学”方式。我怀疑选项 3 真正等同于 1(从某种意义上说,>>运算符被定义square >> negate >> print为实际计算为,print (negate (square x))但代码在可读性方面有优势,因为您可以按照它们发生的顺序看到函数名称,而不是使用通常的数学相反的顺序符号,并且以这种方式定义可以节省一两次击键,因为您不必x在函数名称的末尾包含,因为>>运算符可能使左侧函数自动继承对右侧函数变量的依赖,而无需显式引用变量。

但是,管道方法是如何发挥作用的呢?管道算子是不是一个更通用的算子,恰好适用于函数组合?

另外,我在谷歌上做了很多,并尝试在发布之前阅读文档,但我没有得到任何结果。我敢肯定,如果我继续学习这门语言,明年的某个时候我会理解其中的差异。但我也相信这里有人可以加快这个过程并解释或提供一些很好的例子。最后,我不精通 C#,或者实际上任何其他语言(数学除外),所以对一个完全的菜鸟而不是一个 f# 菜鸟的解释表示赞赏。谢谢!

标签: f#

解决方案


首先- 是的,所有这些方式在“逻辑上”和编译到硬件时都是等效的。这是因为|>and>>运算符被定义为inline。定义大致如下:

let inline (|>) x f = f x
let inline (>>) f g = fun x -> g (f x)

关键字的意思inline是编译器会把对函数的调用替换为函数体,然后编译结果。因此,以下两种情况:

x |> f |> g
(f >> g) x

将按照与以下内容完全相同的方式进行编译:

g (f x)

然而,在实践中,存在一些陷阱。


一个问题是类型推断及其与类/接口的相互作用。考虑以下:

let b = "abcd" |> (fun x -> x.Length)
let a = (fun x -> x.Length) "abcd"

尽管这些定义在逻辑上和编译形式上都是等价的,但是第一个定义将编译,而第二个则不会。发生这种情况是因为 F# 中的类型推断是从左到右进行的,没有双反,因此,在第一个定义中,当编译器到达 时x.Length,它已经知道这x是 a string,因此它可以正确解析成员查找。在第二个示例中,编译器不知道是什么x,因为它还没有遇到参数"abcd"


另一个问题与可怕的价值限制有关。简单来说,它说在语法上(不是逻辑上!)一个(而不是函数)的定义不能是通用的。这与可变性有关的原因不明 - 请参阅链接文章以获得解释。

将此应用于函数组合,请考虑以下代码(请注意,两者f都是g通用函数):

let f x = [x]
let g y = [y]

let h1 = f >> g
let h2 x = x |> f |> g

在这里,h2会编译h1得很好,但不会抱怨值限制。


在实践中,这三种方式之间的选择通常归结为可读性和便利性。这些都不是天生就比其他更好。在编写代码时,我通常会根据自己的喜好进行选择。


推荐阅读