大家都知道Haskell是纯函数式语言,因为Haskell将side-effects
隔离出来了,但是“隔离”是怎么体现出来的,一直很难讲清楚,举个例子试着分析一下。
隔离副作用的表现就是,对于一个纯函数,永远无法引入副作用,换句话说,对于相同的输入,输出永远相同。在Haskell里,你永远无法定义出一个类型为String -> String
的函数,对于相同的输入,输出的String
值不同。
我们举一个John Hughes的Paper《Programming with Arrows》中的例子来说明。
假设我们想定义一个函数,计算某个字符串中某个单词的计数,最简洁的方式是使用Function Composition:
count w = length . filter (==w) . words
可以看出来,这个函数的类型是:String -> Int
。这是一个无副作用的函数,对于同样的字符串,输出的结果永远相同。
现在我们改变一下这个函数的功能,不从字符串里面统计,而是读一个文件,然后输出向终端输出最后的结果,类似unix
的wc
命令。我们可能很自然的会想将函数改造成这个样子:
count w = print . length . filter (==w) . words . readFile
代码虽然很直观,但是这个函数是编译不了的,因为类型不对,readFile
的类型是FilePath -> IO String
,返回值是IO String
,是一个IO Monad
,也就是说是有副作用的。
IO Monad
的内部状态是无法escape
的,所以你无法使用readFile
函数,在IO Monad
之外,写出一个类型为FilePath -> String
函数来读取文件内容并返回。通过这种类型上的约束,Haskell就可以避免副作用的引入。
如果想实现这个功能,函数类型就只能是FilePath -> IO ()
:
count w = (>>=print) . liftM (length . filter (==w) . words) . readFile
我们从后向前来解读一下。
readFile
的类型是FilePath -> IO String
,表示整个函数的参数类型是FilePath
,其实就是String
。
length . filter (==w) . words
的类型是String -> Int
,若想将这个函数应用到IO Monad
里,为了类型兼容,必须要先lift这个函数,即将a -> b
的函数lift成m a -> m b
。在我们的环境下,就是将 String -> Int
提升成IO String -> IO Int
。
这两部分组合后,即liftM (length . filter (==w) . words) . readFile
,大家应该可以看出来,返回值类型是IO Int
,但是print
的类型为a -> IO ()
,在这个上下文中,即为Int -> IO ()
,无法直接使用。如果想使用,就需要一个类型为IO Int -> (Int -> IO ()) -> IO ()
的函数,去掉具体类型的话,其实就是IO a -> (a -> IO b) -> IO b
,这不就是Monad的bind运算符(>>=)
吗?
最后理一下:
liftM (length . filter (==w) . words) . readFile
的类型是FilePath -> IO Int
>>=
的类型是IO Int -> (a -> IO ()) -> IO ()
print
的类型是Int -> IO ()
将这三个函数放到一起,就是:
count w filePath= (liftM (length . filter (==w) . words) . readFile) filePath >>= print
写成point-free的形式,就是:
count w = (>>=print) . liftM (length . filter (==w) . words) . readFile