F#:管道与组合还是...组合?

时间:2018-08-01 18:56:37

标签: f#

我对所有事物都是陌生的-F#,一般编程和这个社区。我是一名数学专业的学生,​​在本科期间曾短暂接触过计算机科学。我试图完成F#中的某些任务,而"F# Cheat Sheet"展示了似乎三种不同的方法来构成函数,而没有说明重复。这是链接中的相关信息,了解我的意思。

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. squareNegateThenPrint'
  

这似乎是完成完全相同的3种方法的列表   事情,   这里有什么细微差别吗?我坚信给定相同的int   它们都将返回相同的int,但是除此之外呢?什么啊   我没看到吗?每种产品的优缺点是什么   三种方法?

2和3都使用“运算符”,而1似乎是通常的“数学”方式,即通过旧函数将函数组合成新函数。我怀疑选项3确实等于1(在某种意义上定义了>>运算符,以便实际上将square >> negate >> print计算为print (negate (square x)),但是由于您看到了函数名称按其发生的顺序而不是通常的数学符号的相反顺序,并且定义这种方式可以节省一键或两次按键,因为您不必在函数名称的末尾包括x,因为>>运算符可能会使左侧函数自动继承对右侧函数变量的依赖,而无需明确引用该变量。

  

但是,管道方法如何发挥作用呢?是管道   一个更一般的运算符,恰好适合   功能组成?

此外,我在Google上做了很多工作,并尝试在发布之前阅读文档,但是我什么都没得到。我敢肯定,如果我继续前进并继续学习该语言,那么明年的某个时候,我会理解它们之间的区别。但是我也相信这里的人可以加快这一过程,并解释或提供一些很好的例子。最后,我不精通C#或其他任何语言(数学除外),因此对完全菜鸟(而不只是f#菜鸟)的解释表示赞赏。谢谢!

2 个答案:

答案 0 :(得分:14)

首先-是的,所有这些方式在逻辑上和编译为硬件时都是等效的。这是因为|>>>运算符被定义为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是{{ 1}},因此它可以正确解析成员查找。在第二个示例中,编译器不知道string是什么,因为它尚未遇到参数x


另一个陷阱与恐惧Value Restriction有关。简单来说,它说(而不是逻辑!) value (与函数相反)的定义不能通用。这具有与可变性有关的模糊原因-请参阅链接的文章以获取解释。

将其应用于函数组成时,请考虑以下代码(请注意"abcd"f都是通用函数)

g

在这里,let f x = [x] let g y = [y] let h1 = f >> g let h2 x = x |> f |> g 可以很好地编译,但是h2不会,因为抱怨价值限制。


在实践中,这三种方式之间的选择通常取决于可读性和便利性。这些都不是天生的。在编写代码时,我通常只是根据自己的喜好进行选择。

答案 1 :(得分:4)

这些都是在不同情况下使用的基本上等同的概念。没有普遍适用的正确方法,但有时您可以利用通过实践和更多地接触F#编程模式而变得明显的流水线和合成运算符。

举几个例子,在处理序列时经常使用流水线,因为它允许很长的操作链以一种可读的方式组合在一起,看起来像流利的查询语法。

[0..100]
|> List.filter (fun x -> x % 2 = 0)
|> List.map (fun x -> x / 2)
|> List.sum

对于许多一直使用F#的人来说,这比List.sum (List.map (fun x -> x / 2) (List.filter (fun x -> x % 2 = 0) [0..100]))之类的东西可读性强。

使用高阶函数(例如bind)时经常使用合成。例如:

[0..5] 
|> List.tryFind (fun x -> x = 3) 
|> Option.bind ((*) 3 >> Some)

在这里,我们使用流水线在列表上执行tryFind,并通过管道将其返回到Option的{​​{1}}类型,该函数采用签名为{{1}的函数},但是如果我们已经有一个函数Option.bind(例如乘法),则可以使用int -> Option 'bint -> int组成该函数,并将组成的函数传递给>>。 (Some只是将bind部分应用于乘法函数,返回将任何整数乘以3的函数的F#语法。

我们可以通过编写(*) 3来实现相同的目的,但是3运算符和部分应用程序语法为我们节省了一些击键。