Programming Languages PartA Week4学习笔记——SML函数式编程
@
- Introduction to First-Class Functions 函数是一等公民
- Functions as Arguments
- Polymorphic Types and Functions as Arguments
- Anonymous Functions
- Unnecessary Function Wrapping
- Map and Filter
- Generalizing Prior Topics
- Lexical Scope
- Lexical Scope and Higher-Order Functions
- Why Lexical Scope
- Closures and Recomputation
- Fold and More Closures
- Closure Idiom: Combining Functions
- * Closure Idiom: Currying
- Partial Application
- Currying Wrapup
- Mutable References
- * Closure Idiom: Callbacks
- Standard-Library Documentation
- Optional: Abstract Data Types With Closures
- Optional: Closure Idioms Without Closures
- Optional: Java Without Closures
- 高等函数和闭包的重要用法总结
Introduction to First-Class Functions 函数是一等公民
前几周的课程虽然也在接触函数式编程的一些表达方式,但本周正式进入函数式编程的主题
函数作为一等公民(一等函数),本身就是一个值(fn类型),可以作为其他函数(高等函数)的参数、返回结果、tuple的元素、变量绑定、datatype的构造器或者异常等。
Functions as Arguments
函数作为参数传递
例子
上面的三个函数具有相似的模式,都是对自身的递归进行一个运算(+,* ,tl),可以用一等函数进行简化
然后可以对这些重载函数进行封装
Polymorphic Types and Functions as Arguments
使用函数作为高等函数的参数时要注意,一等函数的参数一般需要多态类型,适用性更广泛
Anonymous Functions
匿名函数在各种语言中都很常见,SML也有类似用法
对于上一节的n_times函数,其中一种调用方式是全部定义为函数
更好的一种方式是使用嵌套helper函数
或者将它直接写在调用的位置
但事实上我们只需要在这里调用一次这个函数,所以不需要function binding,因此只需要写成匿名形式。fn表示函数值,并且用=>代替函数的等号。
匿名函数最常用在高等函数中,但无法递归(因为没有名字)
同时,由于函数是个值,函数本身可以赋值给某个val,相当于给匿名函数命名
Unnecessary Function Wrapping
不要对函数重复包装,例如hd和tl已经是函数了,不需要再在匿名函数里包装一次。例如:
Map and Filter
重要的高等函数,
map,用来对列表的每个元素进行映射:
fun map(f,xs) =
case xs of
[] => []
|x::xs' => (f x) :: (map (f,xs'))
filter ,用来筛选符合条件的列表元素:
fun filter (f,xs) =
case xs of
[] => []
|x::xs' => if f x
then x::(filter (f,xs'))
else filter(f,xs')
Generalizing Prior Topics
一等函数有四大类应用场景,前面的课程着重介绍了作为函数参数和数据结构的元素。
这里介绍第三种用法,将函数作为函数返回值的用法:
这里值得注意的是最后一句话,在REPL的函数类型显示中,对于函数的->,默认括号从最后两项->开始,例如
t1->t2->t3->t4 相当于是 t1-> (t2->(t3->t4))
下面介绍第四种用法,使用更通用的函数类型,例如包含自定义的datatype
Lexical Scope
词法范围,词法作用域(也叫静态作用域),函数所处的作用域取决于函数定义位置而不是调用的位置(也就是说绑定的变量与调用位置附近代码块中定义的局部变量无关,而只与定义时的代码块附近的变量有关)
比方说下面这个C++的例子。
例子:
函数闭包,SML中的函数闭包指的是包含函数Code(代码定义)和定义时的Environment(变量环境(静态动态))两方面情况的值。 (所以从概念上来说,SML的函数闭包比JavaScript的闭包概念范围更大,但实质上类似)
也就是说函数作为一个值,本身包含Code和Environment两方面,传递时也同时传递。
函数调用时,在Environment的条件下,根据函数参数按照Code计算函数值。
接下来介绍的内容:
Lexical Scope and Higher-Order Functions
高等函数中词法作用域的使用
例子:
例二中 let部分从没有被函数中的in部分用到,所以完全可以不写(irrelevant)
Why Lexical Scope
比较函数的Lexical Scope 和Dynamic Scope(动态作用域)
原因一,函数意味着不依赖于所用的变量名(函数的作用是一定的,与内部的变量名无关):
原因二,函数可以被type-check 并且被推出定义的位置(不会因为变量shadowing而导致类型错误)
原因三,闭包能够容易地存储需要的数据
Dynamic scope存在于某些语言(例如Racket),exception可以看成是类似dynamic scope(在raise(调用)的附近找到handler来处理,而不是在定义的位置)
Closures and Recomputation
函数闭包可以避免对某些不依赖函数参数的值重复计算(将这类参数作为变量binding,则只需要计算一次)
重要的表达式:SML中,(e1; e2)表示执行计算e1的值,然后将其丢弃,再执行计算e2的值作为整个表达式的值(类似C语言中的逗号表达式)。可以用来执行print语句。
例如,此时allShorterThan1方法需要执行多次print,而allShorterThan2方法不需要重复计算,只需要执行一次binding语句,print也就只执行一次:
Fold and More Closures
Fold对列表的每个值嵌套计算f的值,下面的例子从列表左侧开始,称为folds left
Fold实现了类似迭代器的功能
fold的应用例子,以及一些复杂的例子
上面的例子想说明的是,闭包为传递的函数带来了更多的优势,使用时需要考虑lexical scope,因此也能够使用某些private变量
Closure Idiom: Combining Functions
(* 组合两个函数 *)
fun compose (f,g) = fn x => f(g x)
相当于复合函数,规则和数学中相同,调用顺序从右到左
SML标准库为该用法提供了中缀运算符(infix operater)字母 o
但如果想要让复合规则从左到右,需要使用infix关键词自己定义运算符 (例如从F#中学习得到的 |>管道运算符)及其用法(例如 fun x |> f = f x)。(这里的管道运算符用法和linux的管道运算符一样)
* Closure Idiom: Currying
柯里化(Currying),是函数式编程中的一个重要概念,根据百度百科的定义:
在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。所以对于有两个变量的函数yx,如果固定了 y = 2,则得到有一个变量的函数 2x。
在理论计算机科学中,柯里化提供了在简单的理论模型中比如只接受一个单一参数的lambda 演算中研究带有多个参数的函数的方式。
而ML语言同Haskell一样,正好属于只接受单一参数的语言,因此,想要研究多参数函数问题,除了之前使用的Tuple方法(利用一个tuple参数携带所有多参数),Currying是另一种可行的解决方案。
Currying就是将原有的多个参数的n-tuple拆分成n个真正单参数的function,然后依次返回functions,利用闭包特性,内层函数能够访问所有的n个参数
由于括号从左到右具有结合性,所以Currying的调用也可以不带括号
根据Currying的调用方式,采取了一种约定俗成的方法来简化其声明过程,即fun f p1 p2 p3 ... = e 也可表示Currying的声明,注意这里因为直接是fun符号所以使用赋值符号=
利用Currying实现Fold
Partial Application
偏函数应用(Partial Application),在调用时提供少于需要的变量,可以得到一个闭包(等待剩余参数)。类似于固定某几个变量,剩下一部分变量未定的函数。
partial application 可以避免一些无意义的函数封装
exists function判断列表中是否存在某种值
值得注意的是,partial application不允许创造一个多态函数,也就是说必须要定义或者让编译器能判断创造的函数的参数类型(不能是'a),否则会给出Warning
例如
val pairWithOne = List.map(fn x => (x,1))
(* 'a list -> ('a * int) list *)
(* Value Restriction !!! *)
但可以用一些方式来避免这样的情况发生,例如显式写出函数类型,或者让编译器能判断类型
Currying Wrapup
当我们想要在Tupled function 和 Currying function之间切换或者想要交换Currying function中某几个参数的顺序时,可以通过函数包装(wrapup),用一个辅助函数来实现。
(其实我觉得设计两种需要显式转换才能共通的函数调用方式会破坏语言的一致性,不知道ML的设计人员怎么考虑的)
效率问题,tupling和currying效率总体相差不大
Mutable References
虽然SML基本上不具备可修改的特性,但还是为特殊情况提供了可修改对的数据结构,即Reference
语法:
Reference的可修改指的是其中包含的数据可修改,因此获取这个数据需要使用特殊符号 ! e。
Reference 在这里类似于C语言的指针用法,相当于一个本身不可变的指针。ref本身绑定的变量是不可变的(例如想x,y,z不能改变他们的值,只能shadowing),但ref指向的其中包含的值是可变的(! e可以通过:=来赋值),相当于改变了ref指向位置的变量的值(或者直接改变了指向的变量地址,具体底层怎么做的不太清楚)
* Closure Idiom: Callbacks
闭包典型用法:回调。这也是非常重要的一节,回调在许多语言中都十分常用
我们在回调时需要可修改状态
Standard-Library Documentation
SML提供了标准库,其文档网址为https://www.standardml.org/Basis/manpages.html
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpSvZwSH-1663158632481)(http://47.108.198.122/images/blogimage-master/new_img/week4.assets/image-20220522091451783.png)]
Optional: Abstract Data Types With Closures
Optional: Closure Idioms Without Closures
Optional: Java Without Closures
本课程的一大重点就是将某种语言的语法semantics(特性)在其他语言中通过习惯用法idioms的方式来实现,包括之后的课程中也有很多这样的例子。
这一节就是在Java中实现闭包用法的方式
高等函数和闭包的重要用法总结
高等函数中传递一等函数作为参数,函数都具有闭包特性。
map
filter
fold