Programming Languages PartA Week4学习笔记——SML函数式编程

@

Introduction to First-Class Functions 函数是一等公民

前几周的课程虽然也在接触函数式编程的一些表达方式,但本周正式进入函数式编程的主题

image-20220518103624143

函数作为一等公民(一等函数),本身就是一个值(fn类型),可以作为其他函数(高等函数)的参数、返回结果、tuple的元素、变量绑定、datatype的构造器或者异常等。

image-20220519111130305
image-20220519161404367

Functions as Arguments

函数作为参数传递

image-20220519161549229

例子

image-20220519161821562

上面的三个函数具有相似的模式,都是对自身的递归进行一个运算(+,* ,tl),可以用一等函数进行简化

image-20220519162239841

然后可以对这些重载函数进行封装

image-20220519162502442

Polymorphic Types and Functions as Arguments

使用函数作为高等函数的参数时要注意,一等函数的参数一般需要多态类型,适用性更广泛

image-20220519214838438

image-20220519214931698

image-20220519220209237

Anonymous Functions

匿名函数在各种语言中都很常见,SML也有类似用法

对于上一节的n_times函数,其中一种调用方式是全部定义为函数

image-20220519222548256

更好的一种方式是使用嵌套helper函数

image-20220519222646744

或者将它直接写在调用的位置

image-20220519223414459

image-20220519223622584

但事实上我们只需要在这里调用一次这个函数,所以不需要function binding,因此只需要写成匿名形式。fn表示函数值,并且用=>代替函数的等号。

image-20220519225209157

image-20220519231931924

image-20220519231944049

匿名函数最常用在高等函数中,但无法递归(因为没有名字)

同时,由于函数是个值,函数本身可以赋值给某个val,相当于给匿名函数命名

image-20220519233522743

image-20220519233742805

Unnecessary Function Wrapping

不要对函数重复包装,例如hd和tl已经是函数了,不需要再在匿名函数里包装一次。例如:

image-20220520092257720

image-20220520092406194

Map and Filter

重要的高等函数,

map,用来对列表的每个元素进行映射:

fun map(f,xs) = 
	case xs of
		[] => []
	   |x::xs' => (f x) :: (map (f,xs'))

image-20220520093015422

image-20220520093033571

filter ,用来筛选符合条件的列表元素:

fun filter (f,xs) = 
	case xs of
		[] => []
	   |x::xs' => if f x 
	   			  then x::(filter (f,xs'))
	   			  else filter(f,xs')

image-20220520094550623

image-20220520094619109

Generalizing Prior Topics

一等函数有四大类应用场景,前面的课程着重介绍了作为函数参数和数据结构的元素。

image-20220520095558153

这里介绍第三种用法,将函数作为函数返回值的用法:

image-20220520100021939

这里值得注意的是最后一句话,在REPL的函数类型显示中,对于函数的->,默认括号从最后两项->开始,例如

t1->t2->t3->t4 相当于是 t1-> (t2->(t3->t4))

image-20220520100031752

下面介绍第四种用法,使用更通用的函数类型,例如包含自定义的datatype

image-20220520100345303

image-20220520101158870

image-20220520101208042

image-20220520101521294

Lexical Scope

词法范围,词法作用域(也叫静态作用域),函数所处的作用域取决于函数定义位置而不是调用的位置(也就是说绑定的变量与调用位置附近代码块中定义的局部变量无关,而只与定义时的代码块附近的变量有关)

比方说下面这个C++的例子。

image-20220527215307841

image-20220527215558018

image-20220520102711309

例子:

image-20220520102926272

image-20220520102939159

函数闭包,SML中的函数闭包指的是包含函数Code(代码定义)和定义时的Environment(变量环境(静态动态))两方面情况的值。 (所以从概念上来说,SML的函数闭包比JavaScript的闭包概念范围更大,但实质上类似)

也就是说函数作为一个值,本身包含Code和Environment两方面,传递时也同时传递。

函数调用时,在Environment的条件下,根据函数参数按照Code计算函数值。

image-20220520103004498

接下来介绍的内容:

image-20220520104539878

Lexical Scope and Higher-Order Functions

高等函数中词法作用域的使用

image-20220520110010893

例子:

image-20220520113501806

image-20220520113948350

例二中 let部分从没有被函数中的in部分用到,所以完全可以不写(irrelevant)

image-20220520113917876

image-20220520113958177

Why Lexical Scope

比较函数的Lexical Scope 和Dynamic Scope(动态作用域)

image-20220520114140920

原因一,函数意味着不依赖于所用的变量名(函数的作用是一定的,与内部的变量名无关):

image-20220520114423643

原因二,函数可以被type-check 并且被推出定义的位置(不会因为变量shadowing而导致类型错误)

image-20220520115018018

原因三,闭包能够容易地存储需要的数据

image-20220520124036044

Dynamic scope存在于某些语言(例如Racket),exception可以看成是类似dynamic scope(在raise(调用)的附近找到handler来处理,而不是在定义的位置)

image-20220520124856888

Closures and Recomputation

image-20220520153320877

函数闭包可以避免对某些不依赖函数参数的值重复计算(将这类参数作为变量binding,则只需要计算一次)

image-20220520153855775

重要的表达式:SML中,(e1; e2)表示执行计算e1的值,然后将其丢弃,再执行计算e2的值作为整个表达式的值(类似C语言中的逗号表达式)。可以用来执行print语句。

image-20220520154129470

例如,此时allShorterThan1方法需要执行多次print,而allShorterThan2方法不需要重复计算,只需要执行一次binding语句,print也就只执行一次:

image-20220520154416512

Fold and More Closures

Fold对列表的每个值嵌套计算f的值,下面的例子从列表左侧开始,称为folds left
image-20220520161112192

Fold实现了类似迭代器的功能

image-20220520161319330

fold的应用例子,以及一些复杂的例子

image-20220520162940967

image-20220520163400296

image-20220520163741787

上面的例子想说明的是,闭包为传递的函数带来了更多的优势,使用时需要考虑lexical scope,因此也能够使用某些private变量

image-20220520164415788

Closure Idiom: Combining Functions

image-20220520230045662

(* 组合两个函数 *)
fun compose (f,g) = fn x => f(g x)

image-20220520230248209

相当于复合函数,规则和数学中相同,调用顺序从右到左

SML标准库为该用法提供了中缀运算符(infix operater)字母 o

image-20220520230545206

但如果想要让复合规则从左到右,需要使用infix关键词自己定义运算符 (例如从F#中学习得到的 |>管道运算符)及其用法(例如 fun x |> f = f x)。(这里的管道运算符用法和linux的管道运算符一样)

image-20220520230711696

image-20220520234003240

* Closure Idiom: Currying

柯里化(Currying),是函数式编程中的一个重要概念,根据百度百科的定义:

在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。

在直觉上,柯里化声称“如果你固定某些参数,你将得到接受余下参数的一个函数”。所以对于有两个变量的函数yx,如果固定了 y = 2,则得到有一个变量的函数 2x。

在理论计算机科学中,柯里化提供了在简单的理论模型中比如只接受一个单一参数的lambda 演算中研究带有多个参数的函数的方式。

而ML语言同Haskell一样,正好属于只接受单一参数的语言,因此,想要研究多参数函数问题,除了之前使用的Tuple方法(利用一个tuple参数携带所有多参数),Currying是另一种可行的解决方案。

image-20220521101437152

Currying就是将原有的多个参数的n-tuple拆分成n个真正单参数的function,然后依次返回functions,利用闭包特性,内层函数能够访问所有的n个参数

image-20220521102532694

image-20220521102552347

由于括号从左到右具有结合性,所以Currying的调用也可以不带括号

image-20220521102744572

根据Currying的调用方式,采取了一种约定俗成的方法来简化其声明过程,即fun f p1 p2 p3 ... = e 也可表示Currying的声明,注意这里因为直接是fun符号所以使用赋值符号=

image-20220521102919339

image-20220521104529917

利用Currying实现Fold

image-20220521104510806

Partial Application

偏函数应用(Partial Application),在调用时提供少于需要的变量,可以得到一个闭包(等待剩余参数)。类似于固定某几个变量,剩下一部分变量未定的函数。

image-20220521110120646

partial application 可以避免一些无意义的函数封装

image-20220521154131598

image-20220521154302902

image-20220521154726440

exists function判断列表中是否存在某种值

image-20220521155018920

值得注意的是,partial application不允许创造一个多态函数,也就是说必须要定义或者让编译器能判断创造的函数的参数类型(不能是'a),否则会给出Warning

image-20220521155851230

例如

val pairWithOne = List.map(fn x => (x,1)) 
(* 'a list -> ('a * int) list *)
(* Value Restriction !!! *)

但可以用一些方式来避免这样的情况发生,例如显式写出函数类型,或者让编译器能判断类型

image-20220521160311392

Currying Wrapup

当我们想要在Tupled function 和 Currying function之间切换或者想要交换Currying function中某几个参数的顺序时,可以通过函数包装(wrapup),用一个辅助函数来实现。

(其实我觉得设计两种需要显式转换才能共通的函数调用方式会破坏语言的一致性,不知道ML的设计人员怎么考虑的)

image-20220521161939015

效率问题,tupling和currying效率总体相差不大

image-20220521162743685

Mutable References

虽然SML基本上不具备可修改的特性,但还是为特殊情况提供了可修改对的数据结构,即Reference

image-20220521163625412

语法:

image-20220521163733187

Reference的可修改指的是其中包含的数据可修改,因此获取这个数据需要使用特殊符号 ! e。

Reference 在这里类似于C语言的指针用法,相当于一个本身不可变的指针。ref本身绑定的变量是不可变的(例如想x,y,z不能改变他们的值,只能shadowing),但ref指向的其中包含的值是可变的(! e可以通过:=来赋值),相当于改变了ref指向位置的变量的值(或者直接改变了指向的变量地址,具体底层怎么做的不太清楚)

image-20220521164031002

* Closure Idiom: Callbacks

闭包典型用法:回调。这也是非常重要的一节,回调在许多语言中都十分常用

image-20220521165040016

我们在回调时需要可修改状态

image-20220521165736003

image-20220521165821742

image-20220521170309979

image-20220522085806977

Standard-Library Documentation

SML提供了标准库,其文档网址为https://www.standardml.org/Basis/manpages.html

image-20220522091254457

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZpSvZwSH-1663158632481)(http://47.108.198.122/images/blogimage-master/new_img/week4.assets/image-20220522091451783.png)]

image-20220522091921627

Optional: Abstract Data Types With Closures

Optional: Closure Idioms Without Closures

Optional: Java Without Closures

本课程的一大重点就是将某种语言的语法semantics(特性)在其他语言中通过习惯用法idioms的方式来实现,包括之后的课程中也有很多这样的例子。

这一节就是在Java中实现闭包用法的方式

高等函数和闭包的重要用法总结

高等函数中传递一等函数作为参数,函数都具有闭包特性。

map

filter

fold

posted @ 2022-09-14 20:32  自闭火柴的玩具熊  阅读(118)  评论(0编辑  收藏  举报