今年寒假看了一下Clean语言,给我留下的印象:难用,不过它也有一些有趣的特性,例如uniqueness typing和对泛型编程的支持(当然还有其他我不太感兴趣的),这篇主要记录一下uniqueness typing。
纯函数式编程语言里的IO
这里,“纯”当然是指Haskell推崇的纯,怎样在一个纯函数式语言里表达IO操作呢?Haskell的答案是IO Monad,而Clean的答案是uniqueness typing。
假设我们的语言里有一个函数getLine
从stdin获取输入,getLine
的每次调用会有副作用,可以返回不同的值,要怎样用纯函数来表达getLine
呢?一个想法就是让getLine
接受外部状态作为参数,返回一个新的状态和输入的字符串,getLine
的类型就可以表示成World -> (String, World)
,我们可以这样读入两行:
getTwoLine world0 =
let (line1, world1) = getLine world0
(line2, world2) = getLine world1
in ((line1, line2), world2)
可以发现,其实我们是通过不断传递新的World
参数来能表达语句间的数据依赖,从而让运行时按顺序执行语句。但是这里有一个问题,我们可能传递错了或重复传递:
badTwoLine world0 =
let (line1, world1) = getLine world0
(line2, world2) = getLine world0 -- use world0 the second time
in ((line1, line2), world2)
为了解决这个问题,IO Monad会隐式传递World
,而uniqueness typing会禁止一个World
被重复使用(准确说法是不会被共享)。
Uniqueness typing
在Clean的类型系统里,一个值除了有类型外,还会有一个唯一性属性(uniqueness attribute)。如果一个值是唯一的(用*
表示),它就不能被多次使用,一个函数使用完一个唯一值之后,返回值中也必须有一个新的唯一值,然后继续被传递。一些函数既能作用在唯一值,又能作用在非唯一值上,这时就能用匿名属性(.
)或者属性变量(就像类型变量一样)来表示。
例如上面的badTwoLine
函数,world0
被使用了两次,Clean编译器能发现这个错误,说world0
需要的唯一性不能被满足。
除此之外,还有不少细节问题,例如数据结构里元素的唯一性会向外传播,属性间有子类型关系,闭包的唯一性得等,整个属性系统变得很复杂,还好编译器能推导大部分的属性,化简了不少工作。
惰性求值
Clean程序会从Start :: *World -> *World
函数开始执行,运行时系统会向Start
传进一个唯一的World
(实际上只是个数字),从这个唯一的World
我们可以生成新的World
或其他唯一值,通过这些唯一值就能执行IO和控制程序执行的顺序,甚至可以原地修改数据,对性能优化也有帮助。
Clean的执行是基于图规约的惰性求值,整个程序就是一个图,这个图就是维一值传递的网络,开始于*World
结束于*World
,只有对最后的*World
的求值有意义的路径才会被执行,其他都被忽略。例如下面的程序
Start world0 = world1
where
(_, file0, world1) = fopen "file" FReadText world0
(_, char, file1) = freadc file0
(_, world2) = fclose file1 world1
// fopen :: !{#Char} !Int !*f -> (!Bool,!*File,!*f)
// freadc :: !*File -> (!Bool,!Char,!*File)
// fclose :: !*File !*f -> (!Bool,!*f)
where部分按顺序读下来,
fopen
用world0
打开文件,返回world1
和file0 :: *File
freadc
从file0
里读一个字符,返回file1
fclose
用world1
关闭file1
,返回world2
但是,实际是2和3是不被执行的,因为Start
返回的是world1
,而world1
的求值在第1步就完成了。为了让freadc
和fclose
被执行,Start
必须返回一个依赖于freadc
和fclose
的*World
,在这里就是world2
。
这也是Clean程序的模式,从一个*World
生成更多唯一值,最后回到*World
。