表达式是在环境中求值的。
如果允许表达式改变环境,
那么,其他表达式的求值结果,就是不可预料的。
环境,是专业术语,也可称为执行环境,
里面保存的是,变量和值的绑定关系。
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数量少的真正原因吧。