Haskell的纯函数性

表达式是在环境中求值的。

如果允许表达式改变环境,

那么,其他表达式的求值结果,就是不可预料的。


环境,是专业术语,也可称为执行环境,

里面保存的是,变量和值的绑定关系。


Haskell是一门纯函数式语言,

并不是说,函数的每次求值结果都相同。

而是说,任何函数都不会修改它的执行环境


例如,对于从屏幕上读取字符的函数,

每次调用,并不一定会返回相同的值。

但是,执行后,它确实不会影响环境中的绑定关系。


因此,输入输出操作,也是纯函数,

理解执行环境和现实世界的区别,是关键。


IO

Haskell是一门静态强类型的语言。

任何表达式在编译前类型都是确定的。


输入输出与IO a类型有关。

其中,IO是类型构造器,:k IO = * -> *

因此,IO a才是一个具体类型。


IO是如何定义的呢?

type IO a = World -> (a, World)

World类型的值表示现实世界的状态。


IO a类型的值是一个函数,它接受现实世界的当前状态作为参数,

返回一个元组,这个元组由类型a的值和对现实世界改变后的状态构成。


由于,我们并不需要手动传递World类型的值作为参数来调用,

所以,应该把IO a类型的值理解为一个惰性求值的表达式

对该表达式按需求值,得到一个a类型的值。


IO a类型的表达式,称为action

IO a类型的action中,惰性包含了一个a类型的值。


例如:

getChar :: IO Char

求值getChar表达式,就会得到一个Char类型的值。


例如:

IO ( )

其中,( )表示空元组类型,该类型只有一个值,空元组( )。

“( )”既可以表示类型,又可以表示值,取决于它出现的上下文。

另外,Haskell中习惯将空元组看做无用数据。


在IO ( )中,( )是作为IO类型构造器的参数出现的,表示空元组类型,

所以说,IO ( )类型的action中,惰性包含了一个空元组值。


例如:

putStrLn :: String -> IO ( )

putStrLn是一个函数,

它接受String类型的值作为参数,

返回一个IO ( )类型的action。


do

main :: IO ( )

main = do

    putStrLn "Please input: "

    inpStr <- getLine

    putStrLn $ "Hello " ++ inpStr


以上程序定义了main函数,是一个IO ( )类型的action。


do关键字,是Haskell中的一种写法,

用来表示顺序执行。


其中,

putStrLn :: String -> IO ( )

getLine :: IO String


putStrLn $ "Hello " ++ inpStr

= putStrLn ( "Hello " ++ inpStr )


因此,

putStrLn "Please input: " :: IO ( ),在屏幕上输出字符串“Please input: ”,

inpStr <- getLine,求值惰性表达式getLine,把action里面包含的值拿出来,绑定到inpStr上。

putStrLn $ "Hello " ++ inpStr,在屏幕上输出字符串"Hello "与变量inpStr连接后的结果。


IO操作,每次执行的效果并不相同,

仍然都是纯函数

在于它们不会修改执行环境中的变量绑定关系。


结语

很多人指出,Haskell使用了Monad将纯函数与不纯函数分离开,

这种说法,其实是为了解释给不熟悉的人看的。

事实上,Haskell中的任何函数都是纯函数


类似的,

Haskell语言中Bug的数量可以控制到非常少,

并不是因为通过Monad将IO和其他函数分离开,

而是因为任何函数都不能改变环境中变量的绑定关系,

即,函数的纯粹性。


此外,Monad,Applicative和Functor这些类型类,

提高了语言的抽象程度,让复杂度更可控,

我想,这才是Bug数量少的真正原因吧。