"dummies"、IO+Maybe 的最简单的非平凡 monad 转换器示例
Simplest non-trivial monad transformer example for "dummies", IO+Maybe
有人可以给出一个非常简单(几行)的 monad 转换器示例,这是非常重要的(即不使用 Identity monad - 我理解)。
例如,某人将如何创建一个执行 IO 并且可以处理故障(可能)的 monad?
可以证明这一点的最简单示例是什么?
我浏览了一些 monad 转换器教程,它们似乎都使用了 State Monad 或 Parsers 或一些复杂的东西(对于新手)。我希望看到比这更简单的东西。我认为 IO+Maybe 会很简单,但我自己真的不知道该怎么做。
如何使用 IO+Maybe monad 堆栈?
什么会在上面?底部会是什么?为什么?
在什么样的用例中会想要使用 IO+Maybe monad 或 Maybe+IO monad?创建这样一个复合 monad 是否有意义?如果是,什么时候,为什么?
当然,MaybeT
monad 转换器是:
newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}
我们可以这样实现它的 monad 实例:
instance (Monad m) => Monad (MaybeT m) where
return a = MaybeT (return (Just a))
(MaybeT mmv) >>= f = MaybeT $ do
mv <- mmv
case mv of
Nothing -> return Nothing
Just a -> unMaybeT (f a)
这将允许我们执行 IO,并在某些情况下选择优雅地失败。
例如,假设我们有这样一个函数:
getDatabaseResult :: String -> IO (Maybe String)
我们可以用那个函数的结果独立地操作 monads,但是如果我们这样组合它:
MaybeT . getDatabaseResult :: String -> MaybeT IO String
我们可以忘记那个额外的 monadic 层,把它当作一个普通的 monad。
假设您必须使用在某种意义上 "may fail" 的 IO
值,例如 foo :: IO (Maybe a)
、func1 :: a -> IO (Maybe b)
和 func2 :: b -> IO (Maybe c)
。
手动检查绑定链中是否存在错误会迅速产生可怕的 "staircase of doom":
do
ma <- foo
case ma of
Nothing -> return Nothing
Just a -> do
mb <- func1 a
case mb of
Nothing -> return Nothing
Just b -> func2 b
如何以某种方式 "automate" 这个?或许我们可以围绕 IO (Maybe a)
设计一个带有绑定函数的新类型,该函数会自动检查第一个参数是否是 IO
中的 Nothing
,从而省去我们自己检查它的麻烦。像
newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }
绑定函数:
betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
ma <- runMaybeOverIO mia
case ma of
Nothing -> return Nothing
Just a -> runMaybeOverIO (mf a)
这有效!而且,更仔细地观察它,我们意识到我们没有使用 IO
monad 独有的任何特定函数。稍微概括一下新类型,我们可以使它适用于任何底层 monad!
newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }
本质上,这就是 MaybeT
转换器 works 的方式。我遗漏了一些细节,比如如何为转换器实现 return
,以及如何将 "lift" IO
值转换为 MaybeOverM IO
值。
注意 MaybeOverIO
有种类 * -> *
而 MaybeOverM
有种类 (* -> *) -> * -> *
(因为它的第一个 "type argument" 是一个 monad 类型构造函数,它本身需要"type argument").
这是可用的 here 作为 .lhs 文件。
MaybeT
转换器将允许我们像抛出异常一样打破 monad 计算。
我先快速回顾一下一些预备知识。跳到 为 IO 添加 Maybe 的权力 以获得一个可行的例子。
首先导入一些:
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
经验法则:
In a monad stack IO is always on the bottom.
其他类似 IO 的 monad 通常也会出现在底部,例如状态转换器 monad ST
.
MaybeT m
is a new monad type which adds the power of the Maybe monad to the monad m
- e.g. MaybeT IO
.
我们稍后会讨论这种力量是什么。现在,习惯于将 MaybeT IO
视为 maybe+IO monad 堆栈。
Just like IO Int
is a monad expression returning an Int
, MaybeT IO Int
is a MaybeT IO
expression returning an Int
.
习惯阅读复合类型签名是理解 monad 转换器的一半。
Every expression in a do
block must be from the same monad.
即这是有效的,因为每个语句都在 IO-monad:
greet :: IO () -- type:
greet = do putStr "What is your name? " -- IO ()
n <- getLine -- IO String
putStrLn $ "Hello, " ++ n -- IO ()
这将不起作用,因为 putStr
不在 MaybeT IO
monad 中:
mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? " -- IO monad - need MaybeT IO here
...
幸好有办法解决这个问题。
To transform an IO
expression into a MaybeT IO
expression use liftIO
.
liftIO
是多态的,但在我们的例子中它的类型是:
liftIO :: IO a -> MaybeT IO a
mgreet :: MaybeT IO () -- types:
mgreet = do liftIO $ putStr "What is your name? " -- MaybeT IO ()
n <- liftIO getLine -- MaybeT IO String
liftIO $ putStrLn $ "Hello, " ++ n -- MaybeT IO ()
现在 mgreet
中的所有语句都来自 MaybeT IO
monad。
Every monad transformer has a "run" function.
运行 函数 "runs" monad 堆栈的最顶层 returning
来自内层的值。
对于MaybeT IO
,运行函数是:
runMaybeT :: MaybeT IO a -> IO (Maybe a)
示例:
ghci> :t runMaybeT mgreet
mgreet :: IO (Maybe ())
ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()
也试试 运行ning:
runMaybeT (forever mgreet)
您需要使用 Ctrl-C 来跳出循环。
到目前为止 mgreet
没有做任何我们在 IO 中可以做的事情。
现在我们将通过一个例子来展示混合的力量
带有 IO 的 Maybe monad。
为 IO 添加 Maybe 的权力
我们将从提出一些问题的程序开始:
askfor :: String -> IO String
askfor prompt = do
putStr $ "What is your " ++ prompt ++ "? "
getLine
survey :: IO (String,String)
survey = do n <- askfor "name"
c <- askfor "favorite color"
return (n,c)
现在假设我们想让用户能够结束调查
尽早输入 END 来回答问题。我们可能会处理它
这样:
askfor1 :: String -> IO (Maybe String)
askfor1 prompt = do
putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- getLine
if r == "END"
then return Nothing
else return (Just r)
survey1 :: IO (Maybe (String, String))
survey1 = do
ma <- askfor1 "name"
case ma of
Nothing -> return Nothing
Just n -> do mc <- askfor1 "favorite color"
case mc of
Nothing -> return Nothing
Just c -> return (Just (n,c))
问题是 survey1
有熟悉的楼梯问题
如果我们添加更多问题,则不会扩展。
我们可以使用 MaybeT monad 转换器来帮助我们。
askfor2 :: String -> MaybeT IO String
askfor2 prompt = do
liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- liftIO getLine
if r == "END"
then MaybeT (return Nothing) -- has type: MaybeT IO String
else MaybeT (return (Just r)) -- has type: MaybeT IO String
请注意 askfor2
中的所有 statemens 如何具有相同的 monad 类型。
我们使用了一个新功能:
MaybeT :: IO (Maybe a) -> MaybeT IO a
以下是类型的计算方式:
Nothing :: Maybe String
return Nothing :: IO (Maybe String)
MaybeT (return Nothing) :: MaybeT IO String
Just "foo" :: Maybe String
return (Just "foo") :: IO (Maybe String)
MaybeT (return (Just "foo")) :: MaybeT IO String
这里return
来自IO-monad。
现在我们可以这样写我们的调查函数了:
survey2 :: IO (Maybe (String,String))
survey2 =
runMaybeT $ do a <- askfor2 "name"
b <- askfor2 "favorite color"
return (a,b)
尝试 运行ning survey2
并通过键入 END 作为对任一问题的答复来提前结束问题。
捷径
我知道如果我不提及以下快捷方式,我会收到人们的评论。
表达式:
MaybeT (return (Just r)) -- return is from the IO monad
也可以简单写成:
return r -- return is from the MaybeT IO monad
另外,MaybeT (return Nothing)
的另一种写法是:
mzero
此外,两个连续的 liftIO
语句总是可以组合成一个 liftIO
,例如:
do liftIO $ statement1
liftIO $ statement2
等同于:
liftIO $ do statement1
statement2
通过这些更改,我们的 askfor2
函数可以写成:
askfor2 prompt = do
r <- liftIO $ do
putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
getLine
if r == "END"
then mzero -- break out of the monad
else return r -- continue, returning r
从某种意义上说,mzero
成为一种突破 monad 的方式——就像抛出异常一样。
另一个例子
考虑这个简单的密码询问循环:
loop1 = do putStr "Password:"
p <- getLine
if p == "SECRET"
then return ()
else loop1
这是一个(尾部)递归函数,工作正常。
在传统语言中,我们可以将其写成带有 break 语句的无限 while 循环:
def loop():
while True:
p = raw_prompt("Password: ")
if p == "SECRET":
break
使用 MaybeT 我们可以用与 Python 代码相同的方式编写循环:
loop2 :: IO (Maybe ())
loop2 = runMaybeT $
forever $
do liftIO $ putStr "Password: "
p <- liftIO $ getLine
if p == "SECRET"
then mzero -- break out of the loop
else return ()
最后的 return ()
继续执行,并且由于我们处于 forever
循环中,控制权返回到 do 块的顶部。请注意,loop2
可以 return 的唯一值是 Nothing
,它对应于跳出循环。
根据具体情况,您可能会发现编写 loop2
比递归 loop1
.
更容易
有人可以给出一个非常简单(几行)的 monad 转换器示例,这是非常重要的(即不使用 Identity monad - 我理解)。
例如,某人将如何创建一个执行 IO 并且可以处理故障(可能)的 monad?
可以证明这一点的最简单示例是什么?
我浏览了一些 monad 转换器教程,它们似乎都使用了 State Monad 或 Parsers 或一些复杂的东西(对于新手)。我希望看到比这更简单的东西。我认为 IO+Maybe 会很简单,但我自己真的不知道该怎么做。
如何使用 IO+Maybe monad 堆栈? 什么会在上面?底部会是什么?为什么?
在什么样的用例中会想要使用 IO+Maybe monad 或 Maybe+IO monad?创建这样一个复合 monad 是否有意义?如果是,什么时候,为什么?
当然,MaybeT
monad 转换器是:
newtype MaybeT m a = MaybeT {unMaybeT :: m (Maybe a)}
我们可以这样实现它的 monad 实例:
instance (Monad m) => Monad (MaybeT m) where
return a = MaybeT (return (Just a))
(MaybeT mmv) >>= f = MaybeT $ do
mv <- mmv
case mv of
Nothing -> return Nothing
Just a -> unMaybeT (f a)
这将允许我们执行 IO,并在某些情况下选择优雅地失败。
例如,假设我们有这样一个函数:
getDatabaseResult :: String -> IO (Maybe String)
我们可以用那个函数的结果独立地操作 monads,但是如果我们这样组合它:
MaybeT . getDatabaseResult :: String -> MaybeT IO String
我们可以忘记那个额外的 monadic 层,把它当作一个普通的 monad。
假设您必须使用在某种意义上 "may fail" 的 IO
值,例如 foo :: IO (Maybe a)
、func1 :: a -> IO (Maybe b)
和 func2 :: b -> IO (Maybe c)
。
手动检查绑定链中是否存在错误会迅速产生可怕的 "staircase of doom":
do
ma <- foo
case ma of
Nothing -> return Nothing
Just a -> do
mb <- func1 a
case mb of
Nothing -> return Nothing
Just b -> func2 b
如何以某种方式 "automate" 这个?或许我们可以围绕 IO (Maybe a)
设计一个带有绑定函数的新类型,该函数会自动检查第一个参数是否是 IO
中的 Nothing
,从而省去我们自己检查它的麻烦。像
newtype MaybeOverIO a = MaybeOverIO { runMaybeOverIO :: IO (Maybe a) }
绑定函数:
betterBind :: MaybeOverIO a -> (a -> MaybeOverIO b) -> MaybeOverIO b
betterBind mia mf = MaybeOverIO $ do
ma <- runMaybeOverIO mia
case ma of
Nothing -> return Nothing
Just a -> runMaybeOverIO (mf a)
这有效!而且,更仔细地观察它,我们意识到我们没有使用 IO
monad 独有的任何特定函数。稍微概括一下新类型,我们可以使它适用于任何底层 monad!
newtype MaybeOverM m a = MaybeOverM { runMaybeOverM :: m (Maybe a) }
本质上,这就是 MaybeT
转换器 works 的方式。我遗漏了一些细节,比如如何为转换器实现 return
,以及如何将 "lift" IO
值转换为 MaybeOverM IO
值。
注意 MaybeOverIO
有种类 * -> *
而 MaybeOverM
有种类 (* -> *) -> * -> *
(因为它的第一个 "type argument" 是一个 monad 类型构造函数,它本身需要"type argument").
这是可用的 here 作为 .lhs 文件。
MaybeT
转换器将允许我们像抛出异常一样打破 monad 计算。
我先快速回顾一下一些预备知识。跳到 为 IO 添加 Maybe 的权力 以获得一个可行的例子。
首先导入一些:
import Control.Monad
import Control.Monad.Trans
import Control.Monad.Trans.Maybe
经验法则:
In a monad stack IO is always on the bottom.
其他类似 IO 的 monad 通常也会出现在底部,例如状态转换器 monad ST
.
MaybeT m
is a new monad type which adds the power of the Maybe monad to the monadm
- e.g.MaybeT IO
.
我们稍后会讨论这种力量是什么。现在,习惯于将 MaybeT IO
视为 maybe+IO monad 堆栈。
Just like
IO Int
is a monad expression returning anInt
,MaybeT IO Int
is aMaybeT IO
expression returning anInt
.
习惯阅读复合类型签名是理解 monad 转换器的一半。
Every expression in a
do
block must be from the same monad.
即这是有效的,因为每个语句都在 IO-monad:
greet :: IO () -- type:
greet = do putStr "What is your name? " -- IO ()
n <- getLine -- IO String
putStrLn $ "Hello, " ++ n -- IO ()
这将不起作用,因为 putStr
不在 MaybeT IO
monad 中:
mgreet :: MaybeT IO ()
mgreet = do putStr "What is your name? " -- IO monad - need MaybeT IO here
...
幸好有办法解决这个问题。
To transform an
IO
expression into aMaybeT IO
expression useliftIO
.
liftIO
是多态的,但在我们的例子中它的类型是:
liftIO :: IO a -> MaybeT IO a
mgreet :: MaybeT IO () -- types:
mgreet = do liftIO $ putStr "What is your name? " -- MaybeT IO ()
n <- liftIO getLine -- MaybeT IO String
liftIO $ putStrLn $ "Hello, " ++ n -- MaybeT IO ()
现在 mgreet
中的所有语句都来自 MaybeT IO
monad。
Every monad transformer has a "run" function.
运行 函数 "runs" monad 堆栈的最顶层 returning 来自内层的值。
对于MaybeT IO
,运行函数是:
runMaybeT :: MaybeT IO a -> IO (Maybe a)
示例:
ghci> :t runMaybeT mgreet
mgreet :: IO (Maybe ())
ghci> runMaybeT mgreet
What is your name? user5402
Hello, user5402
Just ()
也试试 运行ning:
runMaybeT (forever mgreet)
您需要使用 Ctrl-C 来跳出循环。
到目前为止 mgreet
没有做任何我们在 IO 中可以做的事情。
现在我们将通过一个例子来展示混合的力量
带有 IO 的 Maybe monad。
为 IO 添加 Maybe 的权力
我们将从提出一些问题的程序开始:
askfor :: String -> IO String
askfor prompt = do
putStr $ "What is your " ++ prompt ++ "? "
getLine
survey :: IO (String,String)
survey = do n <- askfor "name"
c <- askfor "favorite color"
return (n,c)
现在假设我们想让用户能够结束调查 尽早输入 END 来回答问题。我们可能会处理它 这样:
askfor1 :: String -> IO (Maybe String)
askfor1 prompt = do
putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- getLine
if r == "END"
then return Nothing
else return (Just r)
survey1 :: IO (Maybe (String, String))
survey1 = do
ma <- askfor1 "name"
case ma of
Nothing -> return Nothing
Just n -> do mc <- askfor1 "favorite color"
case mc of
Nothing -> return Nothing
Just c -> return (Just (n,c))
问题是 survey1
有熟悉的楼梯问题
如果我们添加更多问题,则不会扩展。
我们可以使用 MaybeT monad 转换器来帮助我们。
askfor2 :: String -> MaybeT IO String
askfor2 prompt = do
liftIO $ putStr $ "What is your " ++ prompt ++ " (type END to quit)? "
r <- liftIO getLine
if r == "END"
then MaybeT (return Nothing) -- has type: MaybeT IO String
else MaybeT (return (Just r)) -- has type: MaybeT IO String
请注意 askfor2
中的所有 statemens 如何具有相同的 monad 类型。
我们使用了一个新功能:
MaybeT :: IO (Maybe a) -> MaybeT IO a
以下是类型的计算方式:
Nothing :: Maybe String
return Nothing :: IO (Maybe String)
MaybeT (return Nothing) :: MaybeT IO String
Just "foo" :: Maybe String
return (Just "foo") :: IO (Maybe String)
MaybeT (return (Just "foo")) :: MaybeT IO String
这里return
来自IO-monad。
现在我们可以这样写我们的调查函数了:
survey2 :: IO (Maybe (String,String))
survey2 =
runMaybeT $ do a <- askfor2 "name"
b <- askfor2 "favorite color"
return (a,b)
尝试 运行ning survey2
并通过键入 END 作为对任一问题的答复来提前结束问题。
捷径
我知道如果我不提及以下快捷方式,我会收到人们的评论。
表达式:
MaybeT (return (Just r)) -- return is from the IO monad
也可以简单写成:
return r -- return is from the MaybeT IO monad
另外,MaybeT (return Nothing)
的另一种写法是:
mzero
此外,两个连续的 liftIO
语句总是可以组合成一个 liftIO
,例如:
do liftIO $ statement1
liftIO $ statement2
等同于:
liftIO $ do statement1
statement2
通过这些更改,我们的 askfor2
函数可以写成:
askfor2 prompt = do
r <- liftIO $ do
putStr $ "What is your " ++ prompt ++ " (type END to quit)?"
getLine
if r == "END"
then mzero -- break out of the monad
else return r -- continue, returning r
从某种意义上说,mzero
成为一种突破 monad 的方式——就像抛出异常一样。
另一个例子
考虑这个简单的密码询问循环:
loop1 = do putStr "Password:"
p <- getLine
if p == "SECRET"
then return ()
else loop1
这是一个(尾部)递归函数,工作正常。
在传统语言中,我们可以将其写成带有 break 语句的无限 while 循环:
def loop():
while True:
p = raw_prompt("Password: ")
if p == "SECRET":
break
使用 MaybeT 我们可以用与 Python 代码相同的方式编写循环:
loop2 :: IO (Maybe ())
loop2 = runMaybeT $
forever $
do liftIO $ putStr "Password: "
p <- liftIO $ getLine
if p == "SECRET"
then mzero -- break out of the loop
else return ()
最后的 return ()
继续执行,并且由于我们处于 forever
循环中,控制权返回到 do 块的顶部。请注意,loop2
可以 return 的唯一值是 Nothing
,它对应于跳出循环。
根据具体情况,您可能会发现编写 loop2
比递归 loop1
.