美文网首页
haskell context

haskell context

作者: 码农崛起 | 来源:发表于2018-05-27 03:11 被阅读0次

    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下重用。

    相关文章

      网友评论

          本文标题:haskell context

          本文链接:https://www.haomeiwen.com/subject/ajxmjftx.html