F#探险之旅(三):命令式编程(下)

简介:

控制流程(Control Flow) 

函数式编程(中)一文中,我们初步了解了F#在函数式编程范式下的控制流程,即if, elif, then, else等组成的结构。在命令式编程范式下,F#提供了更多的控制流程支持,包括if,while和for。

在命令式编程范式下的if结构与函数式编程下对应结构的主要差别在于,对于if分支,调用的函数为unit类型(即无返回值),而且并不要求必须使用else分支:

F# Code
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Thursday then
print_endline
"Thursday play list: lazy afternoon"


这里print_endline函数的类型为string -> int。尽管else分支不是必须的,但如果需要,你也可以加上,不过else分支也必须为unit类型。

F# Code
if System.DateTime.Now.DayOfWeek = System.DayOfWeek.Thursday then
print_endline
"Thursday play list: lazy afternoon"
else
print_endline
"Alt play list: pop music"


至此,不管if结构的分支是否返回值,我们都有办法表示,这样就跟C#的if结构一致了。

在C#中,如果一个分支的语句多于一条,需要使用花括号,而在F#中,分支所包含的语句要通过缩进来表示。

for循环是命令式编程中一种常见的结构。如果你有过C#或VB.NET的经验,那么很容易理解:

F# Code
let sentence = [| "To "; "live "; "is "; "to "; "function." |]

for index = 0 to Array.length sentence - 1 do
System.Console.Write sentence.[index]


在C#中,for循环是否执行需要看中间的bool表达式的结果,而这里则是看局部值index是否在指定的范围内,而且初始值要小于终止值。F#还提供了另一种for循环结构:

复制代码
F# Code
let whitePollution = [| "This term refers to "; "pollution caused by ";
"litter of used plastic bags, "; "polystyrene cups, ";
"food containers and paper." |]

for index = Array.length whitePollution - 1 downto 0 do
System.Console.Write whitePollution.[index]
复制代码


此时index的值将以递减顺序变化。

while循环也较为简单,与C#很相似,直接看个例子吧:

复制代码
F# Code
// 压洲
let pressureContinent = ref [ "This phrase's pronunciation is ";
"similar to \"Asia\" in Chinese, "; "but it means ";
"a continent of pressure." ]

while (List.nonempty !pressureContinent) do
System.Console.Write(List.hd !pressureContinent);
pressureContinent := List.tl !pressureContinent
复制代码


循环推导(Loops over Comprehensions) 

可以使用for循环来枚举一个集合,这种方式与C#中的foreach结构类似。下面的例子对一个字符串数组进行枚举。

F# Code
let words = [| "Red"; "Lorry"; "Yellow"; "Lorry" |]

for word in words do
print_endline word


调用.NET类库中的静态方法和属性 

F#中的命令式编程有一个极为有用的特性,它能够调用由任意.NET语言编写的类库,这包括BCL本身。不过在调用由F#编写的类库和由其它语言编写的类库时有所不同,因为F#类库拥有额外的元数据,比如一个方法是否接受一个元组或者其参数是否可被柯里化,这些元数据专用于F#。Microsoft.FSharp.Reflection API的产生很大程度上是由于这些元数据,这些API用于在F#和.NET的元数据间进行交互。

调用类的静态或实例方法和属性的基本语法是相同的,而在调用由非F#类库中的方法时必须使用括号(在F#中通常可用空格)。非F#类库中的方法不能被柯里化,方法本身也不是值,因此不能作为参数传递。遵循了这些规则,调用非F#类库的方法就变得直白、简单了。先来看看如何使用静态属性和方法:

复制代码
F# Code
#light
open System.IO

if File.Exists("test.txt") then
print_endline
"test.txt is present"
else
print_endline
"test.txt does not exist"
复制代码


Exists方法是File类的静态方法,这里的用法跟C#或VB.NET中很像。但这里的代码风格不太像函数式编程的风格,我们可以将.NET类库的方法做个简单的包装

复制代码
F# Code
let exists filePath = File.Exists(filePath)
let files = ["test1.txt"; "test2.txt"; "test3.txt"]

let results = List.map exists files
print_any results
复制代码


上面的代码功能是对列表中的文件逐一进行检测,看它是否存在。exists函数对Exist方法做了包装,这样就可以以函数式编程的风格调用它了。

如果调用的.NET方法有很多参数,可能会忘掉某个参数的用途,在VS中可以查看参数信息(快捷键Ctrl+K, P)。而在F#中我们还可以使用具名参数(named argumengs)

复制代码
F# Code
open System.IO

let file = File.Open(path = "test.txt",
mode = FileMode.Append,
access = FileAccess.Write,
share = FileShare.None)

print_any file.Length
file.Close()
复制代码


使用.NET类库中的对象和实例成员 

除了类的静态成员,我们也可以创建类的实例并使用它的成员(字段、属性、事件、方法):

复制代码
F# Code
#light
open System.IO

let file = new FileInfo("notExisting.txt")
if not file.Exists then
using(file.CreateText()) (
fun stream ->
stream.WriteLine(
"hello, f#"))
file.Attributes
<- FileAttributes.ReadOnly

print_endline file.FullName
复制代码


这段代码引用命名空间,创建FileInfo类的实例,检查文件是否存在,(如果不存在的话)创建文件,写入文本,设置属性值,熟悉C#或VB.NET的你是否感觉很眼熟?这里创建的实例很像记录类型,它引用的对象本身(file)是不能修改的,但它包含的内容则是可以修改的(Attributes属性)。设置属性值时要用“<-”操作符。using其实是一个操作符,用于清理资源(对比下C#中的using语句)。

再考虑一下上面的例子,中间的两步是创建实例,设置属性,这是我们经常做的事情:

C# Code
Person person = new Person(1);
person.Name
= "Steve";
person.BirthOn
= DateTime.Now;


F#中还可以把上述过程简化:

复制代码
F# Code
open System.IO
let fileName = "test.txt"
let file =
if File.Exists(fileName) then
Some(
new FileInfo(fileName, Attributes = FileAttributes.ReadOnly))
else
None
复制代码


使用.NET类库中的索引器(Indexer) 

索引器是.NET中的一个重要的概念,它使得一个集合类看起来像是一个数组。它本质上是名为Item的特殊属性。基于上述两点,F#提供了两种方式来访问索引器。

复制代码
F# Code
#light
open System.Collections.Generic

let stringList =
let temp = new ResizeArray<string>() in
temp.AddRange([
|"one"; "two"; "three"|]);
temp

let itemOne = stringList.Item(0)
let itemTwo = stringList.[1]

printfn
"%s %s" itemOne itemTwo
复制代码


第一种以属性的方式访问,第二种以数组的方式访问。

注意:上面例子中的代码很简单,却展示了F#中的一种常用模式。在创建标识符stringList时,首先将其实例化,然后调用它的实例成员(AddRange)设置状态,最后返回。

使用.NET类库中的事件(Event) 

对于Windows Forms和Web Forms的开发人员,恐怕没有不知道事件的含义吧?我们可以将函数附加到事件上,比如Button的Click事件,这些附加的函数有时称为事件处理器(Event Handler)。

向事件添加一个处理器函数也很简单。每个事件都暴露了Add方法,由于事件在非F#类库中定义,因此Add方法需要带有括号。在F#中,通常可使用匿名函数作为处理器函数

下面的例子使用了Timer类及其Elapsed事件:

F# Code-Timer的Elapsed事件


能附加事件处理器,当然也能移除事件处理器,这需要使用RemoveHandler方法。RemoveHandler方法接受一个委托值,这个委托值封装了.NET中的方法,使得它可在方法间像值一样传递,不过在用RemoveHandler前要用AddHandler添加事件处理器,而不是Add方法

对.NET类型应用模式匹配 

模式匹配使得我们可以针对不同的值进行不同的运算。此外,F#还允许对.NET类型进行匹配,这要用到:?操作符。

复制代码
F# Code
#light
let simpleList = [box 1; box 2.0; box "three"]

let recognizeType (item : obj) =
match item with
| :? System.Int32 -> print_endline "An integer"
| :? System.Double -> print_endline "A double"
| :? System.String -> print_endline "A string"
| _ -> print_endline "Unkown type"
复制代码


我们不可能罗列所有的.NET类型,最后一行的作用在于匹配所有的其它类型。很自然的,在对类型进行匹配时,我们不仅想知道类型,还想了解当前的值,可以这么做:

复制代码
F# Code
#light
let simpleList = [box 1; box 2.0; box "three"]

let recognizeType (item : obj) =
match item with
| :? System.Int32 as x -> printfn "An integer: %i" x
| :? System.Double as x -> printfn "A double: %f" x
| :? System.String as x -> printfn "A string: %s" x
| x -> printfn "An object: %A" x
复制代码


前面的文章中我们了解了异常处理的基本用法,这里的技术也可用在异常处理中,因为我们往往会根据类型捕获异常。

复制代码
F# Code
let now = System.DateTime.Now
System.Console.WriteLine(now)

try
if now.Second % 3 = 0 then
raise (
new System.Exception())
else
raise (
new System.ApplicationException())
with
| :? System.ApplicationException ->
print_endline
"A second that was not a multiple of 3"
| _ ->
print_endline
"A second that was a multiple 3"
复制代码


|> 操作符(Pipe-Forward Operator) 

在应用.NET类库时,“|>”操作符很有用,因为它可以帮助编译器正确地推导出函数参数的类型。它的定义很简单:

F# Code
let (|>) x f = f x


类型信息为:

Tips
'a -> ('a -> 'b) -> 'b


可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。除了这样将参数“转交”外,“|>”更重要的作用在于帮助编译器进行类型推导:

复制代码
F# Code
open System
let dateList = [ new DateTime(1999, 9, 18);
new DateTime(2000, 9, 19);
new DateTime(2001, 9, 20) ]

List.iter (
fun d -> print_int d.Year) dateList
复制代码


此时编译器会报告错误,因为它不能推导出d的类型(这个让我感到有点奇怪,iter函数的类型为(a’ -> unit) -> a’ list -> unit,它能推导出dateList的类型,却不能得出d的类型)。此时使用“|>”就没问题了,因为我们显式地告诉编译器d的类型:

F# Code
dateList |> List.iter (fun d -> print_int d.Year)


要了解事情的端倪,我们最好再来看第二个例子:

复制代码
F# Code
type fsDate = { year : int; month : int; day : int }
let fsDateList =
[ { year =
1999; month = 12; day = 31 }
{ year =
2000; month = 12; day = 31 }
{ year =
2001; month = 12; day = 31 } ]

List.iter (
fun d -> print_int d.year) fsDateList
fsDateList
|> List.iter (fun d -> print_int d.year)
复制代码


这段代码不会有编译错误,虽然看起来跟前一个例子很像,其主要区别在于fsDate是F#中的自定义类型,DateTime则是非F#类库中的类型。我们可以得出结论,不管是外部的.NET类型还是F#类型,“|>”都可使用,而F#的自动类型推导最好用在F#类型上

复制代码
F# Code
let methods = System.AppDomain.CurrentDomain.GetAssemblies()
|> List.of_array
|> List.map (fun assm -> assm.GetTypes())
|> Array.concat
|> List.of_array
|> List.map (fun t -> t.GetMethods())
|> Array.concat

print_any methods
复制代码


“|>”操作符还可用于串联多个函数调用,每次函数调用都将返回值传给下一个函数。

小结

走马观花,这一站的风景看得差不多了,命令式编程的核心部分也介绍完毕。有了函数式编程和命令式编程的知识,我们应该有信心解决大部分问题了。使用F#,我们可以选择合适的编程范式,而不是囿于特定的一种范式。下一站,我们将看到第三种主要的编程范式——面向对象编程。

注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。

参考:
《Foundations of F#》 by Robert Pickering
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
F# Specs


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/09/28/fs-adventure-ip-part-two.html,如需转载请自行联系原作者。

目录
相关文章
|
4月前
|
机器学习/深度学习 测试技术
ChronosX: 可使用外生变量的时间序列预测基础模型
时间序列预测中,基础模型虽在单变量任务中表现出色,但引入协变量支持仍面临挑战。Chronos研究团队提出ChronosX架构,通过适配器层有效整合历史与未来协变量信息,适用于任何单变量模型。实验表明,ChronosX显著提升预测性能,尤其在复杂数据集上优势明显。消融研究进一步验证了协变量模块的重要性。尽管需要轻量训练,但其灵活性和通用性为时间序列建模提供了新思路,未来或可通过类似LLM提示机制实现更高效的协变量处理。
222 16
ChronosX: 可使用外生变量的时间序列预测基础模型
|
3月前
|
消息中间件 监控 Docker
Docker环境下快速部署RabbitMQ教程。
就这样,你成功地用魔法召唤出了RabbitMQ,还把它和你的应用程序连接了起来。现在,消息会像小溪流水一样,在你的系统中自由流淌。别忘了,兔子们不喜欢孤独,他们需要你细心的关怀,不时地监控它们,确保他们的世界运转得井井有条。
236 18
|
6月前
|
Unix Linux 编译器
windows下和linux下cmake的规则有区别吗
通过合理使用CMake的条件逻辑和平台特定的配置选项,开发者可以编写更加灵活和健壮的CMake脚本,确保项目在Windows和Linux上的一致性和可移植性。
327 76
|
8月前
|
算法 数据可视化 安全
基于DWA优化算法的机器人路径规划matlab仿真
本项目基于DWA优化算法实现机器人路径规划的MATLAB仿真,适用于动态环境下的自主导航。使用MATLAB2022A版本运行,展示路径规划和预测结果。核心代码通过散点图和轨迹图可视化路径点及预测路径。DWA算法通过定义速度空间、采样候选动作并评估其优劣(目标方向性、障碍物距离、速度一致性),实时调整机器人运动参数,确保安全避障并接近目标。
308 68
|
6月前
|
存储 NoSQL Java
【数据结构进阶】哈希表
哈希表是一种高效的数据结构,通过哈希函数实现数据映射,支持平均O(1)时间复杂度的查找、插入和删除操作。本文详细介绍了哈希表的基本概念、哈希函数的设计(如直接定址法和除留余数法)以及哈希冲突的解决方法(如开放定址法和链地址法)。同时,文章通过代码实例展示了线性探测和链地址法两种哈希表的实现过程,并分析了各自的优缺点。最后总结指出,合理选择哈希函数和冲突解决策略是优化哈希表性能的关键。
431 2
|
7月前
|
存储 人工智能 安全
实时拦截攻击并响应威胁,聊聊服务器DDoS防御软件
实时拦截攻击并响应威胁,聊聊服务器DDoS防御软件
235 16
|
8月前
|
算法 决策智能
基于SA模拟退火优化算法的TSP问题求解matlab仿真,并对比ACO蚁群优化算法
本项目基于MATLAB2022A,使用模拟退火(SA)和蚁群优化(ACO)算法求解旅行商问题(TSP),对比两者的仿真时间、收敛曲线及最短路径长度。SA源于金属退火过程,允许暂时接受较差解以跳出局部最优;ACO模仿蚂蚁信息素机制,通过正反馈发现最优路径。结果显示SA全局探索能力强,ACO在路径优化类问题中表现优异。
|
Linux Shell UED
探索 Linux 命令 `dircolors`:自定义 `ls` 命令的颜色输出
`dircolors` 是 Linux 中用于自定义 `ls` 命令颜色输出的工具,它读取配置文件(默认 `/etc/DIR_COLORS` 或通过 `LS_COLORS` 环境变量)并生成 shell 变量。
|
消息中间件 存储 监控
图文并茂!深入了解RocketMQ的过期删除机制
大家好,我是Leo。 今天聊一下RocketMQ的文件过期删除机制
图文并茂!深入了解RocketMQ的过期删除机制
|
机器学习/深度学习 JavaScript 前端开发
好用不卡,这些插件和配置让你的 Webstorm 更牛逼!(上)
作为前端开发者,最趁手的搬砖工具无外乎 Webstorm 和 VSCode,Webstorm 像苹果系统,闭源、收费、官方有良好而强大的开发能力、智能索引和补全功能无出其右者,VSCode 就像安卓,开源、持续迭代更新、社区充满活力。 Webstorm 的 2021.3 版更新后,以往卡顿的情况缓解了很多,就算重新安装 node_modules 也不会像以前一样卡死半天,因为卡顿退坑 Webstorm 的小伙伴可以回来看看 😂 在下使用 Webstorm 较多,总结了一些不错的插件和实用 Tips,希望能帮到你~
好用不卡,这些插件和配置让你的 Webstorm 更牛逼!(上)