F#探险之旅(五):透过F#理解函数式编程(中)

简介:

F#系列随笔索引

列表(List)是函数式编程(FP)的基础。事实上,FP的重要代表Lisp的名字即源自“List Processing”,它的发明者John McCarthy于1960年发表的论文向我们展示了,在只给定几个简单的操作符和一个表示函数的记号的基础上,如何构造出一个完整的编程语言,他的主要思想之一是用一种简单的数据结构列表来表示代码和数据。

链表(Linked list)是Lisp的主要数据结构之一,并且Lisp的源代码本身也由列表构成。F#中的列表类型表示为链表,它与C#中的数组、泛型List<T>类型有着明显的不同。链表可以用下面的图表示:

首先我们来看一下FP中列表的基本操作(其中的代码都由F#实现)。

列表的基本操作

cons:它是“construct”的缩写,用于构造列表,意即将一个元素添加到列表的开头。我们先约定空表表示为[],在此基础上再约定操作符“::”表示cons操作,这样我们就可以构造任意的列表了。如:

F# Code - 列表的cons操作
let emptyList = [] // []
let oneItem = 3 :: [] // [3]
let twoItems = 2 :: oneItem // [2; 3]
let threeItems = 1 :: twoItems // [1; 2; 3]


可以看到这里是如何通过“cons”操作来一步一步构造列表的。

car:它表示“Contents of the Address part of the Register”,意即列表的第一个元素。F#中使用List模块的hd(Head)函数来执行car操作:

F# Code - 列表的car操作
let stringList = ["No "; "one "; "really "; "listens to "; "anyone else."]
List.hd stringList
// "No "


cdr:它表示“Contents of the Decrement part of the Register”,意即列表中第一个元素之外的元素。F#中使用List模块的tl(Tail)函数来执行cdr操作:

F# Code - 列表的cdr操作
let stringList = ["No "; "one "; "really "; "listens to "; "anyone else."]
List.tl stringList
// ["one "; "really "; "listens to "; "anyone else."]


有了这三种基本操作,其它的操作都可以推导出来了。比如:

concat:该操作用于连接两个列表。在F#用“@”操作符执行该操作。

F# Code
let list1 = [2; 3; 4]
let list2 = [5; 6; 7]
let largeList = list1 @ list2
print_any largeList
// [2; 3; 4; 5; 6; 7]


length:该检查列表的元素数量,在F#中使用List模块的length函数:

F# Code
let list1 = [2; 3; 4]
List.length list1
// 3


nth:该操作返回列表的第n个元素,在F#中使用List模块的nth函数:

F# Code
let list1 = [2; 3; 4]
List.nth list1
2 // 4


这里代码用来获取list1中的索引(基于0)为2的元素,返回4。

现在再来看看List模块还有哪些重要的函数:

List模块(Microsoft.FSharp.Collections.List)的函数 

List.rev:很明显,它可以翻转一个列表。要注意的是该函数会创建整个列表的一个副本,所以要注意性能问题。

List.zip:该函数的签名为a’ list -> b’ list -> (a’ * b’) list,将两个列表打包为一个元组的列表:

F# Code
print_any(List.zip [1; 2] ["one"; "two"]) // [(1, "one"); (2, "two")]


List.exists:该函数的签名类型为(a’ -> bool) -> a’ list -> ‘a,顾名思义,它用于检查列表是否包含了满足指定谓词函数的元素。

List.find:该函数的签名类型为(a’ -> bool) -> a’ list -> ‘a,可以看到它接受两个参数,第一个参数是谓词函数,第二个参数及传入的列表。可以这么理解,find函数对列表的元素逐一检查,看是否满足上面所说的谓词函数,如果找到了,返回该元素的值,否则抛出异常。

F# Code
let result = List.find (fun i -> i * i = 64) [1..10]
print_int result
// 8


这里检查[1..10]中的每个数字,返回8。但如果找不到任何元素满足的话,会抛出KeyNotFoundException,这时可以使用tryfind,这个类似于C#中TryParse方法。

List.filter:该函数接受的参数与find函数类似,不过它的功能是对列表的元素进行过滤,将所有满足谓词函数的元素构造为一个列表返回:

F# Code
let list3 = List.filter (fun i -> i % 2 = 0) [1..20]
print_any list3
// [2; 4; 6; 8; 10; 12; 14; 16; 18; 20]


另外,还有功能强大的聚合函数(Aggregate Operators),即iter、map和fold。(事实上,F#中的Set、Seq、Option和Array模块都支持这三种操作)

List.iter:该函数将枚举列表中的每个元素,并将每个元素应用于指定的函数,如:

F# Code
List.iter (fun i -> printfn "List contains %d" i) [1..5]


输出结果为:

复制代码
F# Code
List contains 1
List contains
2
List contains
3
List contains
4
List contains
5
复制代码


List.map:map函数用将列表转换为另一个列表。它的签名类型为:

Type Infomation
(‘a –> ‘b) –> ‘a list –> ‘b list


看看这个效果图就容易理解了,对第一个列表的元素逐一应用函数,从而得到一个新的列表:

F# Code
let x = List.map (fun i -> i * (-1)) [1..5]
printfn
"%A" x // [-1; -2; -3; -4; -5]


List.fold:在这三个函数中,fold最为强大,不过也最为复杂。它的功能可以理解为:假定我们有三个值,初始值baseValue,函数fun,列表list,逐一访问list中的每个元素,对其应用函数fun,将fun的执行结果累加到baseValue,fold将baseValue的最终值返回。在逐一访问列表时,可以采用从左到右或从右向左的方式,所以fold函数有两个实现:fold_left和fold_right。

F# Code
let accumulate acc x = acc + x
let totalList = List.fold_left accumulate 0 [1..100]
printfn
"1+2+..+100 = %d" totalList // 5050


这里baseValue是0,函数是accumulate,列表是[1..100],最终结果为5050。

列表与模式匹配和递归的结合 

初学列表时,容易像C#中的集合类型那样去看待它。最近学习了一下Haskell,为它的纯粹和优雅所折服,其中的列表部分大量使用了模式匹配和递归,这个过程也让我重新理解了列表。相比于F#的List模块,Haskell提供了额外的列表操作函数,这里我想通过在F#中实现这些函数来看看如何结合使用列表与模式匹配和递归。

take:接受两个参数,一个数字,一个列表,用于从列表开头获取指定个数的元素组成的新列表:

复制代码
F# Code
let rec take (count: int) (l: 'a list) =
match l with
| _ when count <= 0 -> []
| [] -> []
| x :: xs -> x :: take (count - 1) xs

let list1 = [1; 2; 3; 4; 5]
print_any(take
0 list1) // []
print_any(take 1 list1) // [1]
print_any(take 3 list1) // [1; 2; 3]
复制代码


这里同时使用了递归和模式匹配,如果count小于等于0,返回空列表;否则返回从开头计数的指定个数的元素。

drop:该函数也接受两个参数,从列表开头移除指定个数的元素,将剩下的元素组成的列表返回:

复制代码
F# Code
let rec drop (count: int) (l: 'a list) =
match l with
| _ when count <= 0 -> l
| [] -> []
| head :: tail -> drop (count - 1) tail

let list1 = [1; 2; 3; 4; 5]
print_any(drop
0 list1) // [1; 2; 3; 4; 5]
print_any(drop 1 list1) // [2; 3; 4; 5]
print_any(drop 5 list1) // []
复制代码


如果count小于等于0,返回原列表;否则移除指定个数的元素。这里使用了headtail,这样代码的可读性会更好。

通过take和drop函数,我们可以看到,首先得把列表理解为链表,然后在此基础上应用递归和模式匹配,就可以完成很多复杂的操作

小结 

本文介绍了函数式编程(FP)中的列表操作。首先是函数式编程中列表的三种基本操作,在此基础上我们可以推导出其它的各种操作;随后介绍了F#中List模块中的重要函数;最后通过两个自定义函数来展示如何结合使用列表、递归和模式匹配。顺便提一句,强烈建议你学习一下Haskell来了解FP的基本思想,在F#中很容易就能使用命令式编程的方式编写程序,这种灵活性往往使人偏离FP,尤其是在初学FP时。这就像我们学习英语的过程,想象一下,如果把你空投到美国(或其它英语国家),你的英语的进步是不是会快得多?

F#系列随笔索引

参考
Emacs Lisp基础函数
Lisp简介
Mastering F# Lists
Lisp Programming Language
Learn You a Haskell


本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/11/03/fsharp-adventure-fp-list-processing.html,如需转载请自行联系原作者。

目录
相关文章
|
21天前
|
Rust 安全 Go
揭秘Rust语言:为何它能让你在编程江湖中,既安全驰骋又高效超车,颠覆你的编程世界观!
【8月更文挑战第31天】Rust 是一门新兴的系统级编程语言,以其卓越的安全性、高性能和强大的并发能力著称。它通过独特的所有权和借用检查机制解决了内存安全问题,使开发者既能享受 C/C++ 的性能,又能避免常见的内存错误。Rust 支持零成本抽象,确保高级抽象不牺牲性能,同时提供模块化和并发编程支持,适用于系统应用、嵌入式设备及网络服务等多种场景。从简单的 “Hello World” 程序到复杂的系统开发,Rust 正逐渐成为现代软件开发的热门选择。
37 1
|
6天前
|
前端开发 算法 JavaScript
探索编程之海:我的技术感悟之旅
【9月更文挑战第14天】在编程的浩瀚海洋中,我是一位勇敢的探险者。每一次代码的编写,都是对未知领域的挑战。本文将分享我在技术探索中的心得体会,从初识编程的迷茫到逐渐找到自己的航线,再到不断精进技艺的过程。通过这段旅程,我深刻理解了“你必须成为你希望在世界上看到的改变”这句话的内涵,并将它融入到我的学习和实践中。让我们一起跟随这篇文章,揭开编程世界的神秘面纱,找到属于自己的航道。
27 9
|
3天前
|
算法 搜索推荐 程序员
编程之舞:从代码到艺术的蜕变
【9月更文挑战第16天】在数字世界的画布上,每一行代码都是笔触,每一个算法都是构图。本文将带你领略编程的艺术性,探讨如何通过技术感悟提升代码的审美和实用性。我们将一起探索代码示例,它们如同艺术品一般,既展现逻辑之美,又富有创造力。
|
2月前
|
JavaScript 前端开发 Java
代码之舞:从编程新手到资深开发者的旅程
【7月更文挑战第19天】编程,一种现代魔法,让无数人为之着迷。本文将通过个人的技术感悟,探讨如何从一个对代码一无所知的新手成长为一名能够自如驾驭复杂项目的资深开发者。我们将穿越编程语言的选择、学习资源的利用、项目实践的重要性以及持续学习的必要,最终达到技术与创造力的和谐共舞。
47 10
探索编程世界的奇幻之旅
【8月更文挑战第8天】在编程的世界中,我们既是创造者,又是探险者。从最初对代码的好奇到逐渐掌握各种编程语言和工具,每一步都充满了挑战与惊喜。本文将分享我的编程之路,以及在这个过程中获得的感悟与经验。
|
3月前
|
机器学习/深度学习 人工智能 自然语言处理
探索编程世界的奇幻旅程
【6月更文挑战第18天】在数字时代的浪潮中,编程不仅是技术操作的体现,更是一种思维的展现。本文将通过一系列生动的故事和实例,揭示编程背后的逻辑美学与创造力的火花,带领读者走进编程世界的奇幻之旅。
|
4月前
|
程序员
代码与禅意:编程中的悟性之旅
【5月更文挑战第31天】在数字世界的繁花似锦中,我们常常忽略了编码背后蕴含的哲学。本文将探讨编程不仅仅是一门技术,更是一种艺术和内省的过程。从禅宗的角度出发,我们将一窥那些静谧的代码行间所折射出的深邃智慧,以及它如何影响程序员的思考方式和解决问题的策略。
|
4月前
|
算法 程序员
代码与禅意:编程中的心流体验
【4月更文挑战第10天】在编码的世界中,技术感悟往往与禅宗哲学不谋而合。本文探索了编程时的心流状态——一种既集中又放松的创造性境界,它如何与禅宗中追求的“在动中寻静”相呼应。通过深入分析编程过程中的心流体验,我们揭示了如何在逻辑严谨与创造力之间找到平衡点,进而提升编程效率和内在满足感。
34 0
|
JavaScript 前端开发 机器人
梦回战国,领略两千多年前公孙龙如何将面向对象运用得炉火纯青
2200 年前的战国时期,赵国平原君的食客公孙龙有一天骑着白马进城时,被守城的官兵以马不能进城而将其拦下.公孙龙当众即兴演讲,口述&quot;白马非马&quot;一论.守城的官兵被说的一愣一愣的,无法反驳。于是公孙龙就骑着他&#39;不是马的白马&#39;大摇大摆进城去了,这其实就是历史上最为经典的一次面向对象思维的阐述
28619 11