F#到底有什么用?
奇妙游写到第五篇,前面的几篇都是开场白:
- 一个用F#编写WinForm的例子
- donet命令行工具,也就是F#的开发环境
- 关于函数和函数式编程的碎碎念
- 函数式编程的核心概念:值
下面,我们开始正式来搞点事情,看看F#能做些什么。在此之前,我们再复习F#的运行环境。
F# Interactive
那么F#到底有什么用呢?我们前面说了F#有一个命令行,可以用dotnet fsi
打开。这个叫做交互式开发环境,比较现代的语言,都会提供一个交互式的环境,比如Java、Python,都有。而函数式的语言,则更加注重这个环境,原因如下:
函数式的编程,注重由下向上来开发,为了实现一个系统的整体功能,先逐步实现更加底层的功能,慢慢把一个系统整合出来。相区别的是面向对象的程序开发,一开始会投入大量的精力来规划类层次结构、设计类的接口。函数虽然也是接口,但是函数的接口很轻。
每一个小函数的正确性比较容易证明,而正确的函数组合在一起,加上值的概念,整个软件系统的正确性也很容易保证;
在反复实验和测试函数的过程,就十分有必要有一个可以输个函数得到一个值的计算器。那些支持F#开发的环境,很容易就配置一个F# Interactive,比如Rider中Shift+Shift,Start F# Interactive就是这个样子:
在下面的提示符里面就能输入命令,帮助命令输入
#help;;
通过帮助可以看到,F# 交互窗口指令:
#r "file.dll";; // 引用(动态加载)给定的 DLL
#i "package source uri";; // 搜索包时包含包源 uri
#I "path";; // 为被引用的 DLL 添加给定搜索路径
#load "file.fs" ...;; // 像已编译和被引用的文件一样加载给定的文件
#time ["on"|"off"];; // 启用/停止计时
#help;; // 显示帮助
#r "nuget:FSharp.Data, 3.1.2";; // 加载 Nuget 包 'FSharp.Data' 版本 '3.1.2'
#r "nuget:FSharp.Data";; // 加载 Nuget 包 'FSharp.Data' 具有最高版本
#clear;; // 清除屏幕
#quit;; // 退出
帮助还会告诉你,F# 交互窗口命令行选项:
请参阅“dotnet fsi --help”以了解各个选项
总之,我们能够通过运行上面的命令运行一个开发环境,我们也能通过dotnet fsi filename.fsx
来执行一个脚本。
计算器
第一个作用:当然是高级计算器。
比如,小朋友问:1+2+3+…+100等于多少?
[1..100] |> List.sum;;
马上就有val it: int = 55
。这个问题可以口算,但是更大的数字怎么办?你说你还是能口算……那算我没说。
上面这里有个奇怪的东西|>
,这是一个运算符(术语:管道),其实很简单,就是
List.sum [1..100];;
这么写是因为可以连着写,用|>
,比如,100以内所有能被3整除的数和是多少?
[1..100]
|> List.filter (fun i -> i % 3 = 0)
|> List.sum;;
得到:
val it: int = 1683
还可以更加复杂:
1 − 1 2 + 1 3 − 1 4 + … 1 - \frac{1}{2} + \frac{1}{3} - \frac{1}{4} + \ldots 1−21+31−41+…
[1..1000000]
|> List.map (fun i-> (-1.0) ** (float i+1.0) / float i)
|> List.sum;;
计算 π \pi π
利用交互式计算器,可以解决所有小学生的奇怪计算。下面就来点严肃的,计算一下 π \pi π。
利用的加拿大滑铁卢大学的Bouweins提出的公式。
y 0 = 2 − 1 , α 0 = 6 − 4 2 y n = 1 − ( 1 − y n − 1 4 ) 1 / 4 1 + ( 1 − y n − 1 4 ) 1 / 4 α n = ( 1 + y n ) 4 α n − 1 − 2 2 n + 3 y n ( 1 + y n + y n 2 ) π = lim n → ∞ 1 α n \begin{split} &y_0 = \sqrt{2}-1, \alpha_0=6-4\sqrt{2}\\ &y_{n} = \frac{1-(1-y_{n-1}^4)^{1/4}}{1+(1-y_{n-1}^4)^{1/4}}\\ &\alpha_n = (1+y_n)^4\alpha_{n-1}-2^{2n+3}y_n(1+y_n+y_n^2)\\ &\pi = \lim_{n\to\infty}\frac{1}{\alpha_n} \end{split} y0=2−1,α0=6−42yn=1+(1−yn−14)1/41−(1−yn−14)1/4αn=(1+yn)4αn−1−22n+3yn(1+yn+yn2)π=n→∞limαn1
这个计算方法非常厉害,只需要迭代15次,精度就能达到20亿位。
那么我们编辑一个fsx文件:
let sqrt (x: decimal) n =
let rec _sqrt (x: decimal) (rn: decimal) n =
match n with
| 0 -> rn
| _ -> _sqrt x ((rn + x / rn) * 0.5m) (n - 1)
_sqrt x (x / 3m) n
let quad (x: decimal) = sqrt (sqrt x 32) 32
let sqrt2 = sqrt 2m 32
let yp (y: decimal) =
let y' = 1.0m - y * y * y * y
let y'' = quad y'
(1m - y'') / (1m + y'')
let ap (alpha: decimal) (y: decimal) (n: int) =
let term1 = (1m + y) * (1m + y) * (1m + y) * (1m + y) * alpha
let term2 = 2.0 ** (2.0 * float n + 1.0)
let term3 = y * (1m + y + y * y)
term1 - (decimal term2) * term3
let alpha n =
let rec y_a n =
match n with
| 0 -> sqrt2 - 1m, 6m - 4m * sqrt2
| _ ->
let y_1, alpha_1 = y_a (n - 1)
let y = yp y_1
let a = ap alpha_1 y n
y, a
let _, a = y_a n
1m / a
seq {0..10}
|> Seq.map (fun i -> i, alpha i)
|> Seq.iter (fun (i, pi) -> printfn $"%4i{i}\t%A{pi}")
为了得到更多的有效精度,我们采用了decimal
数据,这个数据类型最少可以确保28位的精确计算。
首先我们定义了一个参数是decimal的开方运算符,因为.NET针对这个数据类型只有加减乘除等运算,没有开方。
采用迭代法:
x 0 = a / 3 x n + 1 = 1 2 ( x n + a x n ) a = lim n → ∞ x n x_0 = a/3 \\ x_{n+1} = \frac{1}{2}(x_n + \frac{a}{x_n})\\ \sqrt{a} = \lim_{n\to\infty}x_n x0=a/3xn+1=21(xn+xna)a=n→∞limxn
这里使用了尾递归,而不采用循环。在F#中,尾递归会被编译器优化为循环,所以不用担心性能问题和栈溢出问题。
从下面的代码可以看到,F#在编制程序时,会把一小段一小段数学描述实现为一个个函数,这样的代码更加清晰,更加容易理解。
总结
- F#可以作为一个交互式计算器,可以解决所有小学生的奇怪计算。
- 利用F#很直观地实现数学表达式,所采用的代码不采用循环、不使用变量,而是采用递归,这是函数式编程的典型思考方式。