是类型类!还有函子,可应用函子和单子!

HoshinoTented

2019-10-29 22:55:41

Personal

类型类是 Haskell 中重要的组成部分,它类似于 Java 中的**接口**,接下来就让我们一起来看看奇妙的类型类吧。 ## 什么是类型类 Haskell 中可以把具有共同属性(或叫做特征)的类型归为一类,这一类就被称为类型类。 比如常见的 `Eq` 类型类,代表了一类可以进行 **比较相等和不相等** 的类型,或是 `Ord` 类型类,代表了一类可以进行 **比较大小** 的类型。 ## 函子(Functor) 在这之前先向一些新读者介绍一下 `map` 函数,`map` 函数是将一个列表的每一个值都应用上一个函数,然后将应用函数的结果再组成一个列表。很明显,`map` 函数只会改变列表内元素的类型,而不会改变列表的长度。 ```haskell map :: (a -> b) -> [a] -> [b] ``` 当然不止列表,树、集合,等等的许多类型都可以有自己的 map 函数,在没有类型类的情况下,就需要对每个类型都定义一个属于它们自己的 `map` 函数。 ```haskell mapTree :: (a -> b) -> Tree a -> Tree b mapTree = ... mapSet :: (a -> b) -> Set a -> Set b mapSet = ... ``` 而使用类型类,就是将这些散乱的 `map` 函数统一(抽象)起来,于是便有了 `Functor` 类型类。 `Functor` 类型类只定义了一个函数:`fmap` ```haskell fmap :: Functor f => (a -> b) -> f a -> f b ``` 抽象出来的 `fmap` 不仅能用于列表,还能用于几乎所有的容器类型,比如最简单的 `Identity` 类型。 ```haskell newtype Identity a = Identity { runIdentity :: a } ``` 不太简单的 `Maybe` 类型。 ```haskell data Maybe a = Nothing | Just a ``` 非常不简单的 `State` 类型(注意:mtl 库中并不是这么定义的,只是为了方便才这么写) ```haskell newtype State s a = State { runState :: s -> (a, s) } ``` Functor 可以看做是你有一个你无法打开的魔法盒子(f a),但是你想对盒子里的数值(a)应用一个函数(a -> b),这个时候就可以求助 `fmap`,让它帮你把函数应用到数值上,然后把结果再放进盒子里,还给你。 ```haskell > (+1) `fmap` (Just 1) Just 2 > (+1) `fmap` Nothing > Nothing ``` 为了方便,Haskell 标准库中定义了一个和 `fmap` 等价的 `(<$>)` 运算符。 ```haskell > (+1) <$> (Just 1) Just 2 ``` 既然能让一个 `Just 1` “加上” 1,那能不能让两个 Maybe Int 值相加呢? ```haskell > :t (+) <$> Just 1 (+) <$> Just 1 :: Num a => Maybe (a -> a) ``` 会发现函数跑到魔法盒子里面去了,但我们似乎无法通过目前的任何方法把它取出来,或者应用到其他的值上。解决这个问题的,就是接下来要介绍的 Applicative。 ## 可应用函子(Applicative) Applicative 类型类中主要有两个函数,用来把一个普通的值装进魔法盒子里的 `pure`,还有将装在魔法盒子里的函数应用到另一个装在魔法盒子里的值的 `(<*>)`。 ```haskell class Functor f => Applicative (f :: * -> *) where pure :: a -> f a (<*>) :: f (a -> b) -> f a -> f b -- ... ``` 首先是较为简单的 `pure`,可以把一个普通的值装进 Applicative 中: ```haskell > pure 1 :: Maybe Int Just 1 ``` 接着是和 `fmap` 类似的 `(<*>)`,可以把一个装在 Applicative 中的函数(f (a -> b))应用在另一个 装在 Applicative 中的值(f a)上。 比如可以通过这种方式实现两个 Maybe Int 的相加: ```haskell > (+) <$> Just 1 <*> Just 2 Just 3 > pure (+) <*> Just 1 <*> Just 2 Just 3 ``` ## 单子(Monad) 假设你打算用 Haskell 写一个抽象语法树: ```haskell data Expr = ILit Int | Add Expr Expr -- 省略无用的语法 | Div Expr Expr ``` 然后需要对它们进行求值: ```haskell eval :: Expr -> Int eval (ILit i) = i eval (Add a b) = eval a + eval b eval (Div a b) = eval a `div` eval b ``` 但这明显有个问题,一旦作为除数的表达式结果为 0,那么这个表达式就会报错。 能不能做到委婉地返回一个错误值,而不是强硬地报错呢? 我们可以使用用来处理错误的类型:Maybe ```haskell safeEval :: Expr -> Maybe Int safeEval (ILit i) = Just i safeEval (Add a b) = case safeEval a of Nothing -> Nothing Just a' -> case safeEval b of Nothing -> Nothing Just b' -> Just (a' + b') safeEval (Div a b) = case safeEval a of Nothing -> Nothing Just a' -> case safeEval b of Nothing -> Nothing Just b' -> if b' == 0 then Nothing else Just (a' `div` b') ``` 但会发现,有许多重复代码: ```haskell case something of Nothing -> Nothing Just m -> something' ``` 于是,可以把这些重复代码提取出来,做成一个新的函数: ```haskell ifJust :: Maybe a -> (a -> Maybe a) -> Maybe a ifJust Nothing _ = Nothing ifJust (Just a) f = f a ``` 然后,就可以重新编写我们的 safeEval 函数: ```haskell -- ... safeEval (Add a b) = ifJust (safeEval a) $ \a' -> ifJust (safeEval b) $ b' -> Just $ a' + b' safeEval (Div a b) = ifJust (safeEval a) $ \a' -> ifJust (safeEval b) $ b' -> if b' == 0 then Nothing else Just $ a' + b' ``` 而这个 `ifJust` 函数,就类似于单子的 `(>>=)`。 ```haskell class Applicative m => Monad (m :: * -> *) where return :: a -> m a (>>=) :: m a -> (a -> m b) -> m b ``` 单子和可应用函子有一些相同之处,比如都有相同功能的 `return` 和 `pure`。 而 `(>>=)` 就像上面的 `ifJust` 函数,可以进行连续的运算并自动处理一些重复的动作(比如重复处理错误的检查)。 ```haskell safeDiv (Div a b) = safeEval a >>= \a' -> safeEval b >>= \b' -> if b' == 0 then Nothing else Just $ a' `div` b' ``` 文章最开始介绍到的 `Identity` 和 `State` 也都是 Monad。 `Identity` 是最简单的 Monad,仅包含了一个值,而不做任何运算: ```haskell > Identity 1 >>= \i -> Identity (i + 2) Identity 3 > (+2) 1 3 ``` `State` 则较为复杂,以后的文章会做讲解。 Haskell 还针对 `(>>=)` 设计了一个语法糖,能够看出 `(>>=)` 对纯函数式编程的重要性。 ```haskell foo :: IO () foo = getLine >>= \s -> putStrLn s -- 等价于 foo' :: IO () foo' = do s <- getLine putStrLn s ``` Haskell 标准库还提供了两个函数:`liftM` 和 `ap` ```haskell fmap :: Functor f => (a -> b) -> f a -> f b liftM :: Monad m => (a -> b) -> m a -> m b liftM f a = do a' <- a return (f a') (<*>) :: Applicative f => f (a -> b) -> f a -> f b ap :: Monad m => m (a -> b) -> m a -> m b ap f a = do f' <- f a' <- a return (f' a') ``` 很明显,Functor 的 fmap 和 Applicative 的 (<*>) 都可以用这两个函数来实现。 ### Monad 的一些特点 Monad 的 `(>>=)` 是用于进行连续的运算,并自动处理一些重复的事情。 比如 Maybe Monad 就是自动处理错误,一旦一个环节发生错误,接下来的运算都会是错误的。 又或者 State Monad,进行连续运算的同时还维护了一个状态,十分适合在纯函数式编程中模拟变量。 不知道读者有没有发现一个现象,无论是 Functor,Applicative 还是 Monad,都没有能够把数值从这个魔法盒子里拿出来的操作,这就意味着一个值一旦进入了魔法盒子,就再也无法通过 Functor,Applicative 和 Monad 提供的操作拿出来了,这也是 IO Monad(用于处理副作用的 Monad)的一个重要性质。 ## 结尾 Monad 作为纯函数式编程中十分重要的一部分,学习 Monad 是必不可少的。 有趣的纯函数和类型世界里总是充满了困难和惊喜,希望这篇文章能成为推动你学习纯函数式编程的动力。