《函数式编程精粹》(1) 函数式思考
《Out of the tar pit》
这次课是最烧脑的一次课程
大型软件开发很困难。本质复杂性和偶然复杂性,我们认为的本质复杂性没那么复杂,而是我们采用的工具造成的。
复杂性影响可理解性。
我在问题领域没有这个控制,实现上加了很多控制。很多是我们思维方法的问题,。是我们的方法问题,是人造的问题,这个问题本来不存在。
函数式思考
内容概览
函数式基本概念。
Monad
抽象
简单
不好理解
一种非常强大的思考方式
分布式异步编程遇到的困难,erlang 通过 receive 的语义解决这个问题。Monad 就是一种高阶的方式解决。
Continuation
短短 200 行代码,包含控制流,并发语义等语义的编程语言。
乐趣和困难
乐趣所在:
函数式编程通常是把一个问题映射到一个数学问题去
困难:
体现在思维方式的变换
Introduction
Mathematical function
函数是数学意义上的函数:给一个输入永远返回同样的结果。非数学意义上的函数就不一定了。
好处:
非常好理解
Functions and values
函数其实也是一种值
Types
这个类型和以前的类型完全不一样的概念。C 语言的类型只是单纯为了编译器满意。
ADT
类型也是个代数系统 -- 类型系统
类型提供了一个很强大的思考框架,按照类型来思考,把一些错误变成一个编译错误。
scala
概念听起来很抽象,实际很好理解。
Function with multiple parameters
我们写一个多参数的函数到底是什么意思?
Defining functions
Function signatures
Mathematical function
函数是指的纯数学意义的函数
类型就是个集合
函数就是集合间的映射
从定义域映射到值域
其实学完高中之后理解是很自然的,但是学完了编程语言之后反而变得困难了。
meaning
函数的意义是由输入和输出决定的,如果函数没有参数没有返回值其实是没有意义的。
add1(X) -> X + 1.
-spec add1(integer()) -> integer().
符合这种类型的函数其实很多,这个类型其实是一个定理,程序是一个定理的证明。这也是目前的一个热点,类型系统和逻辑证明。
First Principle
just mapping
not "do" or "calculate"
denotational not operational (指示语义而不是操作语义)
immutablility
no side effecs 没有副作用
The power of pure function
Trivially parallelizable
非常容易并行
lazily evaluation
求值的时机可以是任意的。一种强大的解耦技术。
比如我可以定义“所有整数”
Partial Application, cache
原来在面向对象里用各种模式,函数就可以解决
Evaluation in any order
任意顺序结果都是一样的
Functions and values
No Variable, No Assignment!!!
和代数方程是一样的,可以等价替换
例子:代码重构就是代码替换,每一步替换都很小心。如果函数式编程就可以等同于代数方程化解。
在非函数式编程里就只能通过测试。
first class
函数跟值是没有区别的,函数本身也有类型。在处理上和变量没有区别。
high order calculation
通过函数捕获更高级的概念。计算就可以不再基于值来计算,可以根据某种关系来计算。
这是函数式编程里最强大的思维方式。如果只提一个点的话就是这个。
Types
erlang 里面有 any() 代表所有类型。
基本类型:pid(),reference(),atom(),integer()....
函数:fun( (T1) -> T2 )
复合类型: [Type()]
tuple() / {T1(),t2(),T3()}
Polymorphic Types
类型参数化
-type queue(Type) :: {fifo, list(Type), list(Type)}
product type ADT
ADT:代数化的数据类型。
类型是可以组合的,代数系统的本质上就是可以组合。
Type 本身是一个集合。Type1 * Type2 ==> 新的 Type 笛卡儿积
example
-type height()::integer()
-type name()::string()
-type person()::{height(),name() }
sum type ADT
类似于加法,和代数系统里面的加法是同构的。如何表达这个概念?
把两个集合加起来,类似于 Union
example
-type int_or_bool()::integer() | boolean().
-type result()::success | {failed,string}.
-type size()::small | big.
-type concact()::{email, string} | {phone, integer()}.
在 C 语言里可以用 Union 实现。看问题的角度不同,层次就是不一样,Union 本身很低层次,但是用这样的定义就是一个高层次的概念。
summary
用类型进行建模
用类型捕获领域里的概念,在此基础上对领域进行建模。如果定义的好的话,可以把 bug 变成编译错误。
What is DDD?
不考虑技术,只考虑领域和语义,关注概念和关系。--- 本质复杂性。
假设一开始就用面向对象的方式进行领域建模,很多问题就出来了,很多模式就出来了。
实际例子:如何通过类型表达领域约束
person_name
type first_name() :: string() //Required
type middle_name() :: string() //Option
type last_name() :: string() //Required
如何通过类型表达领域约束?
通常是定义一个 struct,三个属性都带,或者再加一个标志位,表示 middle name 是否有效。
本质区别:
一种是通过类型系统来解决
一种是通过代码实现来解决,is_valid 是一个值。
-type option_string()::{some_string, string()} | nothing.
这是一个 Sum Type.
进一步,把类型参数化:
-type option(A)::{some_A, A()} | nothing.
这样的话,想要处理 option 的话,必须处理两种类型。在编译期就可以检查是否处理了 nothing 了。
other
另一个问题:长度最多 50 的 string,这是一个领域概念
通过这样的概念去捕获领域的概念。
C 语言里怎么解决这个问题?以前都是返回个错误码。
Sum Type 可以用 Union 来实现,一个是结果,一个是 nothing。
返回值就是这个 Union,后续都来处理 Union。
这样做就可以组合了。
summary
程序大部分的 if else 都是在处理组合的问题,都是在处理如果有副作用,有出错如何处理的问题。
定义 Option 就可以解决。
表达:
领域概念
领域约束:在实现层面上把 BUG 变成编译错误
所以它也叫 TDD,T is Type。这个 TDD 倒是热点。
Functions with multiple parameters
Currying
把一个多参数的函数变成多个小的单参数的函数。
Haskell 语言层次就是 currying 的,就是多个参数的函数会编译成单个参数。
这两个函数在语义上是完全一样的,没有区别。
函数的真正类型:
fun( (integer()) -> fun((integer()) -> void) )
Haskell 表示法:
Int -> (Int -> Int)
Partial application
应用部分参数。
正因为可以 currying,才可以 partial application。是个因果关系。
流行的叫法叫依赖注入。其实在函数式编程里是个很自然的实现,参数传进一个执行函数。
假设一个函数参数很长,有些是容易变化的,有些是不变的。可以把不变的放在前面,变化的通过传入执行函数。这样还可以利用 partial application,配置一个一个的模板,把一个参数很长的函数返回若干个参数很少的函数。
currying2 / currying3
Function Definition & Signatrue
在 C 语言里更强调功能,在函数编程里更强调指示意义,更强调语义。
integer() -> ()
() -> string()
[A] -> A
(A->boolean()) -> (() -> string())
....
类型定义好了,基本上干什么事就清楚了。
评论