目录

IO 和 Monad

学习函数式编程总是给人一种学习哲学的错觉,Haskell 不仅定义了程序运算需要的类型,还给输入输出(IO)这样动作也定义了类型,并且把运行程序时的外部状态(RealWorld)当成函数的参数。

IO

haskell 中 IO 的类型定义:

1
2
3
newtype IO a
  = GHC.Types.IO (GHC.Prim.State# GHC.Prim.RealWorld
                  -> (# GHC.Prim.State# GHC.Prim.RealWorld, a #))

简化表示如下,

1
type IO a = GHC.Types.IO (World -> (# World, a #))

程序运行时,输入值需要参与到程序运算中。用函数表示输入动作:

1
World -> (# World, a #)

输出值不能再参与程序运算。用函数表示输出动作:

1
World -> (# World, () #)

GHCi 中用 :l 加载下面的程序文件:

1
2
3
4
5
6
7
8
9
{-# LANGUAGE UnboxedTuples         #-}
import GHC.Types

noExec (GHC.Types.IO a) = a

inStr = noExec (getLine)
outStr = noExec (putStrLn "hello world!")

exec = GHC.Types.IO

然后用 :t inStr:t outStr 查看类型,确实是我们说的那样。

Monad

考虑函数与单个数值或字符之间的运算:

  • (i -> o) -> i -> o
1
2
3
4
ghci> ((+4) 2) + ((*2) 3)
12
ghci> (\x ->(\y -> if x > y then x else y)) 8 6
8

调用函数就能计算结果,不过这样做计算对人来说太麻烦了。

简化计算的办法是将值放在上下文环境(e)中,例如 [1,2,3](1,'a',3.14) 等。怎么在不破坏值的上下文前提下,应用函数完成计算呢?

  • f -> e(i) -> e(o)

换句话说,考虑 f 可能的情况,定义通用计算模型?

1
2
3
4
f = (i -> o)
f = e(i -> o)
f = e(i -> e(o))
f = (e(i) -> o)

Maybe

后面的讨论会用到 Maybe 类型:

1
data Maybe a = Nothing | Just a
1
2
3
4
ghci> :t Nothing
Nothing :: Maybe a
ghci> :t Just 2
Just 2 :: Num a => Maybe a

Functor

处理函数暴露,值处于上下文的情况:

  • (i -> o) -> e(i) -> e(o)
1
2
3
4
ghci> (+3) (Just 2)
error
ghci> fmap (+3) (Just 2)
Just 5

fmap 怎么实现的?模式匹配:

1
2
fmap f Nothing  = Nothing
fmap f (Just x) = Just (f x)

fmap 的动作是将上下文中的值取出,用函数处理后,重新放回上下文:

1
2
3
4
5
6
class Functor f where
    fmap :: (a -> b) -> f a -> f b

instance Functor Maybe where
    fmap f Nothing  = Nothing
    fmap f (Just x) = Just (f x)

只要能被 fmap 处理,这种上下文就是 Functor,Maybe 是 Functor

Applicative

处理函数与值处于同一种上下文的情况:

  • e(i -> o) -> e(i) -> e(o)
1
2
3
4
ghci> (Just (+3)) (Just 2)
error
ghci> (Just (+3)) <*> (Just 2)
Just 5

实现 <*> 同样用模式匹配:

1
2
3
4
5
fmap f Nothing  = Nothing
fmap f (Just x) = Just (f x)

Nothing <*> _ = Nothing
(Just f) <*> x = fmap f x

<*> 的动作是直接从上下文中取出函数,之后就是 Functor 的情况了,用 fmap 处理

1
2
3
4
5
6
7
8
class Functor f => Applicative f where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b

instance Applicative Maybe where
    pure = Just
    Nothing <*> _ = Nothing
    (Just f) <*> x = fmap f x

只要能被 <*> 处理,这种上下文就是 Applicative,Maybe 是 Applicative

Monad

处理值与函数的返回值处于同一种上下文的情况:

  • e(i) -> (i -> e(o)) -> e(o)

这里交换了函数和值的顺序,数据流向是从左往右的。

1
2
3
4
5
6
ghci> (Just (+3)) <*> (Just 2)
Just 5
ghci> (\x -> Just (x + 3)) (Just 2)
error
ghci> (Just 2) >>= (\x -> Just (x + 3))
Just 5

实现 >>= (发音为 bind) 同样用模式匹配:

1
2
Nothing >>= f = Nothing
Just x  >>= f = f x

>>= 的动作是直接从上下文中取出值,应用到函数上

1
2
3
4
5
6
7
8
9
class Applicative m => Monad m where
  (>>=) :: m a -> (a -> m b) -> m b
  (>>) :: m a -> m b -> m b
  return :: a -> m a

instance Monad Maybe where
    return x = Just x
    Nothing >>= _ = Nothing
    Just x >>= f = f x

能被 >>= 处理,并且实现 Applicative 的行为就是 Monad。每种 Monad 至少能处理三种函数:

1
2
3
f = (i -> o)
f = e(i -> o)
f = e(i -> e(o))

我们可以利用 >>= 的性质实现链式操作:

1
2
3
ghci> let add3 = (\x -> Just (x + 3))
ghci> (Just 2) >>= add3 >>= add3 >>= add3 >>= add3
Just 14

IO 类型也被 Haskell 定义为一种 Monad:

1
ghci> getLine >>= readFile >>= putStrLn

Haskell 为我们提供了一些用于 Monad 的语法糖,称为 do 语法:

1
2
3
4
foo = do
    filename <- getLine
    contents <- readFile filename
    putStrLn contents

这篇 文章中,我们用函数模拟了一阶逻辑, 而 Monad 是函数对结构化程序的模拟。

补充

处理值与函数的参数处于同一种上下文的情况:

  • (e(i) -> o) -> e(i) -> e(o)
1
2
3
4
5
6
class Func f where
    comp :: (f a -> b) -> f a -> f b

instance Func Maybe where
    comp f Nothing  = Nothing
    comp f x = Just (f x)
1
2
ghci> comp (\x -> if x == (Just "pwd") then "true" else "false") (Just "pwd")
Just "true"

这里 查阅 Maybe 的相关实现, 有时间再补充。

推荐阅读