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.
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.
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.
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.
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.
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.
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.
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.
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
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
-- 有了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:
1,>>连接的actions转成单行语句
2,>>=后面是lambda时,用<-连接lambda的参数和>>=前的context value构成赋值语句,lambda的body成为整个>>=的结果。
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下重用。