haskell context

1,为啥需要Functor, Applicative, 和 Monad这三个号称非常高级的家伙???

One way to understand functions is as a means of transforming one type into another. Let’s visualize two types as two shapes, a circle and a square, as shown in figure 1.

figure-1.png

These shapes can represent any two types, Int and Double, String and Text, Name and FirstName, and so forth. When you want to transform a circle into a square, you use a function. You can visualize a function as a connector between two shapes, as shown in figure 2.

figure-2.png

This connector can represent any function from one type to another. This shape could represent (Int -> Double), (String -> Text), (Name -> FirstName), and so forth. When you want to apply a transformation, you can visualize placing your connector between the initial shape (in this case, a circle) and the desired shape (a square); see figure 3.

figure-3.png

As long as each shape matches correctly, you can achieve your desired transformation.

The two best examples of types in context that you’ve seen are Maybe types and IO types. Maybe types represent a context in which a value might be missing, and IO types represent a context in which the value has interacted with I/O. Keeping with our visual language, you can imagine types in a con- text as shown in figure 4.

figure-4.png

These shapes can represent types such as IO Int and IO Double, Maybe String and Maybe Text, or Maybe Name and Maybe FirstName. Because these types are in a context, you can’t simply use your old connector to make the transformation. To perform the transformation of your types in a context, you need a connector that looks like figure 5.

figure-5.png

This connector represents functions with type signatures such as (Maybe Int -> Maybe Double), (IO String -> IO Text), and (IO Name -> IO FirstName). With this connector, you can easily transform types in a context, as shown in figure 6.

figure-6.png

The wide range of existing functions from a -> b can not use with context types, this is where Functor, Applicative, and Monad come in. You can think of these type classes as adapters that allow you to work with different connectors so long as the underlying types (circle and square) are the same.

figure-8.png
figure-8.png

The other problem occurs when an entire function is in a context. For example, a function of the type Maybe (Int -> Double) means you have a function that might itself be missing. This may sound strange, but it can easily happen when using partial application with Maybe or IO types. Figure 9 illustrates this interesting case.

figure-9.png
figure-10.png

When you combine all three of these type classes, there’s no function that you can’t use in a context such as Maybe or IO, so long as the underlying types match. This is a big deal because it means that you can perform any computation you’d like in a context and have the tools to reuse large amounts of existing code between different contexts.

2,Functor:适配参数和返回值都不在context中的函数

-- f表示接受一个泛形参数的类型(kind f = * -> *)
-- 例如 Maybe,List,以及 Map Int (kind Map Int = * -> *)
class Functor (f :: * -> *) where
    fmap :: (a -> b) -> f a -> f b

-- <$> 是 fmap 的别名
(<$>) :: Functor f => (a -> b) -> f a -> f b

-- Maybe实现了Functor
instance Functor Maybe where
    fmap func (Just n) = Just (func n)
    fmap func Nothing = Nothing
fmap.png
fmap.png
successfulRequest :: Maybe Int
successfulRequest = Just 6
failedRequest :: Maybe Int
failedRequest = Nothing

fmap (+ 1) successfulRequest = Just 7
fmap (+ 1) failedRequest = Nothing
(+ 1) <$> successfulRequest = Just 7
(+ 1) <$> failedRequest = Nothing

3, Applicative

-- Functor的fmap只能接受一个context参数
-- 无法处理以下类型
addMaybe :: Maybe Int -> Maybe Int -> Maybe Int

Functor’s fmap only works on single-argument functions. The problem you need to solve now is generalizing Functor’s fmap to work with multiple arguments. Multi-argument functions are just a chain of single-argument functions. The key to solving your problem lies in being able to perform partial application in a context such as Maybe or IO.

-- type + = Int -> Int -> Int = Int -> (Int -> Int)
maybeAdd = (+) <$> Just 1
type maybeAdd = Maybe (Int -> Int)

The (+) operator is a function that takes two values; by using <$> on a Maybe value, you created a function waiting for a missing value, but it’s inside a Maybe. You now have a Maybe function, but there’s no way to apply this function!!!

Functor的核心问题是无法利用context中的函数,这正是Applicative要解决的问题之一

-- 继承Functor
class Functor f => Applicative (f :: * -> *) where
    pure :: a -> f a
    (<*>) :: f (a -> b) -> f a -> f b
    GHC.Base.liftA2 :: (a -> b -> c) -> f a -> f b -> f c
    (*>) :: f a -> f b -> f b
    (<*) :: f a -> f b -> f a
    -- 至少实现pure,(<*>)或liftA2之一
    {-# MINIMAL pure, ((<*>) | liftA2) #-}

    -- 从(<*>)推导liftA2
    (a -> b -> c) -> f a -> f b = (a -> (b -> c)) -> f a -> f b
                                          = f (b -> c) -> fb
                                          = f b -> f c -> fb
                                          = (f b -> f c) -> fb
                                          = f c

    --从 liftA2推导(<*>)
    (a -> b -> c) -> f a -> f b -> f c = (a -> (b -> c)) -> f a -> f b -> f c
                                 = f (b -> c) -> f b -> f c
                                 = (<*>)

    -- Applicative用于适配参数在context返回值不在context的函数
    (f a -> b) -> f a = b = f b
<*>.png
-- 有了Applicative就可以处理两个context参数了
maybeAdd <*> Just 5 = Just 6

-- 3个context参数也是可以的
-- (?)表示任意的跟addMaybe对应的非context函数,利用(?)实现addMaybe
(?) :: Int -> Int -> Int -> Int
addMaybe :: Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int

addMaybe1 = (?) <$> Just 1
type addMaybe1 = Maybe (Int -> Int -> Int)
                           = Maybe (Int -> (Int -> Int))

addMaybe2 = addMaybe1 <*> Just 2 
type addMaybe2 = Maybe (Int -> Int)

addMaybe3 = addMaybe2 <*> Just 3
type addMaybe3 = Maybe Int

-- 同理可以证明n个context参数也是可以的
-- 突然发现越来越有意思了哦 哈哈

-- Applicative用于创建数据类型
data User = User{ name :: String, score :: Int} 
    deriving Show

readInt :: IO Int
readInt = read <$> getLine

main :: IO ()
main = do
    putStrLn "Enter a username and score"
    -- 数据构造User相当于 String -> Int -> User(此处表示类型构造)
    user <- User <$> getLine <*> readInt
    print user

4,Monad

import qualified Data.Map as Map

type UserName = String
type GamerId = Int
type PlayerCredits = Int
userNameDB :: Map.Map GamerId UserName
userNameDB = Map.fromList [(1,"nYarlathoTep"),
                           (2,"KINGinYELLOW"),
                           (3,"dagon1997"),
                           (4,"rcarter1919"),
                           (5,"xCTHULHUx"),
                           (6,"yogSOThoth")]
creditsDB :: Map.Map UserName PlayerCredits
creditsDB = Map.fromList [("nYarlathoTep",2000),
                          ("KINGinYELLOW",15000),
                          ("dagon1997",300),
                          ("rcarter1919",12),
                          ("xCTHULHUx",50000),
                          ("yogSOThoth",150000)]

lookupUserName :: GamerId -> Maybe UserName
lookupUserName id = Map.lookup id userNameDB

lookupCredits :: UserName -> Maybe PlayerCredits
lookupCredits username = Map.lookup username creditsDB

-- Applicative无法实现使用上面两个函数实现下面的转换
creditsFromId :: GamerId -> Maybe PlayerCredits

-- 只能加一层包装,IO actions无法模式匹配,Monad正是用于解决此问题
altLookupCredits :: Maybe UserName -> Maybe PlayerCredits
altLookupCredits Nothing = Nothing
altLookupCredits (Just username) = lookupCredits username

Monad继承Applicative,添加了可以利用a -> m b实现context类型转换的能力

class Applicative m => Monad (m :: * -> *) where
    (>>=) :: m a -> (a -> m b) -> m b
    -- 忽略第一个参数,常用于链接没有返回值的IO actions
    (>>) :: m a -> m b -> m b
    -- 跟pure完全一样,Monad比Applicative出现的更早
    return :: a -> m a
    -- 用于出错时返回结果
    fail :: String -> m a
    {-# MINIMAL (>>=) #-}

-- 使用Monad实现
creditsFromId :: GamerId -> Maybe PlayerCredits
creditsFromId id = lookupUserName id >>= lookupCredits

-- >>用于忽略putStrLn的结果
echoVerbose :: IO ()
echoVerbose = putStrLn "Enter a String an we'll echo it!" >>
    getLine >>= putStrLn

-- hello name
askForName :: IO ()
askForName = putStrLn "What is your name?"

nameStatement :: String -> String
nameStatement name = "Hello, " ++ name ++ "!"

-- type (\name -> return "hello") = Monad m => p -> m [Char]
-- type (\name -> "hello") = p -> [Char]
-- 瞬间懵逼了,最后发现此处的return正是Monad中的return函数
helloName :: IO ()
helloName = askForName >>
             getLine >>=
             (\name ->
                 return (nameStatement name)) >>=
             putStrLn
Monad转do.png

Monad转成do:
1,>>连接的actions转成单行语句
2,>>=后面是lambda时,用<-连接lambda的参数和>>=前的context value构成赋值语句,lambda的body成为整个>>=的结果。

do转Monad.png

do转Monad:
1,没有返回值的语句用 >> 跟后面的语句连接
2,<- 对应的语句,右边用 >>= 跟以左边为参数名的最终lambda链接。如果<-下的第一条语句非let,那此语句就是最终lambda的body,否则最终lambda的body通过如下方式构造:<-下面的每一个let语句构造一层立即调用的lambda,=左边是参数名,=右边是调用lambda时的参数值,下一个let构造的lambda成为上一个let构造的lambda的body,第一条非let语句成为最后一个let构造的lambda的body。最终<-下的所有let以及第一个非let构成的立即调用lambda成为最终lambda的body。

相同的代码在不同的context下重用:

-- 问题设置:判断是否通过学位考核
data Grade = F | D | C | B | A deriving (Eq, Ord, Enum, Show, Read)
data Degree = HS | BA | MS | PhD deriving (Eq, Ord, Enum, Show, Read)
data Candidate = Candidate
    { candidateId :: Int,
      codeReview :: Grad,
      cultureFit :: Grade,
      education :: Degree } deriving Show

viable :: Candidate -> Bool
viable candidate = all (== True) tests
    where passedCoding = codeReview candidate > B
          passedCultureFit = cultureFit candidate > C
          educationMin = education candidate >= MS
          tests = [passedCoding
                  ,passedCultureFit
                  ,educationMin]

-- IO context
readInt :: IO Int
readInt = getLine >>= (return . read)
readGrade :: IO Grade
readGrade = getLine >>= (return . read)
readDegree :: IO Degree
readDegree = getLine >>= (return . read)

readCandidate :: IO Candidate
readCandidate = do
    putStrLn "enter id:"
    cId <- readInt
    putStrLn "enter code grade:"
    codeGrade <- readGrade
    putStrLn "enter culture fit grade:"
    cultureGrade <- readGrade
    putStrLn "enter education:"
    degree <- readDegree
    return (Candidate { candidateId = cId,
                        codeReview = codeGrade,
                        cultureFit = cultureGrade,
                        education = degree })

assessCandidateIO :: IO String
assessCandidateIO = do
    candidate <- readCandidate
    let passed = viable candidate
    let statement = if passed
                    then "passed"
                    else "failed"
    return statement

-- Maybe context
assessCandidateMaybe :: Int -> Maybe String
assessCandidateMaybe cId = do
    candidate <- Map.lookup cId candidateDB
    let passed = viable candidate
    let statement = if passed
                    then "passed"
                    else "failed"
    return statement

Notice that assessCandidateIO and assessCandidateMaybe is essentially identical. This is because after you assign a variable with <- in do-notation, you get to pretend it’s an ordinary type that’s not in a particular context. The Monad type class and do-notation have abstracted away the context you’re working in. The immediate benefit in this case is you get to solve your problem without having to think about missing values at all. The larger benefit in terms of abstraction is that you can start thinking about all problems in a context in the same way. Not only is it easier to reason about potentially missing values, but along the way you can start designing programs that work in any context.

Monad 和 do 抽象隐藏了不同的context,使得do下面的代码可以在不同的context下重用。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容