是时候介绍如何在F#中定义函数了,在你没有接触过函数式编程语言之前,你也许会觉得C#/Java的语法已经够丰富了,有什么任务做不了呢?当你读过函数式编程之Currying和函数式编程之Partial application,你就会发现C#在函数式编程方面已经略显无力了,虽然我用C#模拟了这两种函数式特性,但是实现价值已经不大了,也许你从来没见到过有人这样用C#,因为对于C#而言就是OO范式,只是借鉴了少数函数式特性而已,比如LINQ。
当然写这篇博客的目的除了让西安的.NET社区发展壮大,让更多的人加入我们,一起参加线下分享,对我自己而言我是有私心的,那就是把整个函数式编程思想捋一遍,如果我能讲明白说明我是理解的,对不明白的地方还会继续查阅书籍。
另外写博客的好处还在于我可以畅所欲言,其实我作为过来人很想给年轻人一些建议和指引。当大家都在热衷于学习各类关于分布式架构
,微服务
的时候我其实是想告诉他们静下心来,特别当你还是一个经验不算丰富的开发者。作为一个还算对微服务/DDD/CQRS
略有经验的人,我是不会轻易提出这些方案的。除非我的客户和交付团队具备了这样的能力,我才会安心的增加这种复杂度。我也会在未来写出诸如软件架构的十种风格
之类的连载,但是当大家没搞明白这些架构风格的来龙去脉之前把时间花在上面会走很多弯路,有时候你觉得高不可攀的东西早在几十年前就有了,只是现在换个名字,换个框架而已。
反之我们在看了很多三五年,甚至是十年以上的来自候选人的作业,能够达到标准的寥寥无几。我们在考察候选人的代码风格,对事物的抽象能力。而你的代码随意命名,没有使用语言的任何新特性,没有关键的业务抽象,更不知道什么是SOLID 原则, 你跟我谈DDD?怎么可能。
定义函数
言归正传,看看用C#如何实现求平方和
的功能:
public int SumOfSquare(int n)
{
var sum = Enumerable.Range(1, n)
.Select(x => x * x)
.Sum();
return sum;
}
// 调用
var result = SumOfSquare(100)
这是一段用LINQ编写的声明式风格的代码,相比for loop而言,这段代码已经非常优雅了。
在F#是如何实现的呢?
// 定义一个求平方的函数square
let square x = x * x
// 定义sumOfSquares函数
let sumOfSquares n =
[1..n]
|> List.map square
|> List.sum
// 调用
sumOfSquares 100
符号|>
被称作是管道符,它的作用是把第一个表达式的输出放入第二个表达式的输入,以此类推将一段函数连接起来。
上面的代码可以描述为:
- 先定义一个求平方的函数
sqaure
- 把[1..n]生成的list放入List.map函数中,List.map的定义为:
('a -> 'b) -> 'a list -> 'b list
即接受一个'a -> 'b类型的任意函数,同时接受'a类型的list数据,最终返回`b类型的list数据,跟C#中的Select函数差不多。
- 把List.map函数返回的数据放入 List.sum函数中
由于type inference的原因,F#几乎不用声明类型,另外也没有大括号和分号用来表示语句终止。F#采用了和Python类似的思路,用缩进来表示不同的作用域。
理解管道符 |>
上面的例子使用到了管道符|>
,其实它并不是什么神秘的东西,他也是一个函数,只不过函数名就叫|>
而已。
F#通过下面的代码来定义|>
:
let (|>) x f = f x
用()括起来的函数名可以理解为运算符,例如+
和-
,运算符是支持中缀表达式的,例如你可以这样使用:
2 |> string
其中2就是管道符定义中的x,string函数就是管道符定义中的f,最终等价于:
string 2
再来一个例子:
let add x y = x + y
2 |> add 3
其实就是:
add 3 2
定义能够做Partial application的函数
Partial application是指在调用函数式只提供部分参数,从而生成一个嵌入已知参数的新函数,这里面的关键在于函数的参数顺序。
let addWithPluggableLogger logger x y =
let result = x + y
logger "x+y" result
result
例如我们定义了一个具有logging能力的add函数,参数logger作为一个相对稳定的参数被设计到了第一个位置,比如你可以用consoleLogger做Partial application:
let addWithConsoleLogger = addWithPluggableLogger consoleLogger
addWithConsoleLogger 1 2
试想如果参数logger被设计到了其他的位置,可能就无法定义出addWithConsoleLogger这样的函数了。
那么到底如何设计函数的参数顺序呢?下面的指导原则可以供参考:
- 对于稳定的参数要设计在前面,因为你最有可能对相对稳定/固定的参数做Partial application
- 对数据类的参数放在后面,例如List库的大多函数
List.map
和List.minBy
都是这样设计的:
List.map (fun i -> i + 1) [0;1;2;3]
List.minBy (fun i -> i + 1) [0;1;2;3]
基于这样的函数定义,你才可以通过管道符把对一个数据集合的操作连接起来:
let result =
[1..10]
|> List.map (fun i -> i+1)
|> List.filter (fun i -> i>10)
另外这样定义的函数还特别容易粘接和组合,下一篇将介绍如何组合函数。