以下Haskell代码是一个简单的“控制台IO”DSL:
data Ap a = Ap { runAp :: ApStep a }
data ApStep a =
ApRead (String -> Ap a)
| ApReturn a
| ApWrite String (Ap a)
ioRead k = Ap $ ApRead k
ioReturn a = Ap $ ApReturn a
ioWrite s k = Ap $ ApWrite s k
ioWriteLn s = ioWrite (s ++ "\n")
apTest =
ioWriteLn "Hello world!" $
ioRead $ \i ->
ioWriteLn ("You wrote [" ++ i ++ "]") $
ioReturn 10
uiRun ap =
case runAp ap of
ApRead k -> uiRun (k "Some input")
ApReturn a -> return a
ApWrite s k -> putStr s >> uiRun k
run = uiRun apTest
它工作正常然而我想使用monad编写“应用程序”apTest 而不是使用$。换句话说就是这样:
apTest = do
ioWriteLn "Hello world!"
i <- ioRead
ioWriteLn $ "You wrote [" ++ i ++ "]"
return 10
问题在于代码抵制了我将“功能样式”DSL转换为monad的所有尝试。所以问题是如何为这个DSL实现monad实例,允许你编写apTest monad样式而不是“$”样式?
答案 0 :(得分:10)
当然这是一个单子。事实上,将它表达为一个免费的monad会更简单[1],但我们可以使用你所拥有的东西。
以下是我们如何知道它是monad:如果你有一个类型data Foo a = ...
,其中Foo
表示某种递归树结构,其中a
仅出现在树叶上,那么你有一个单子。 return a
是“给我一棵由a
标记的一片叶子组成的树”,>>=
是“叶子上的替代”。
在您的情况下,Ap
是一个树结构
ApReturn a
是一片叶子有两种内部节点
ApRead
是一个没有标签的节点,并且每个类型为String
ApWrite
是一个标有String
并且只有一个后代脱离的节点我已将monad实例添加到您的代码中。 return
只是AppReturn
(加上Ap
包装器)。 >>=
只是递归地应用>>=
并在叶子处替换。
未来的一些提示
Ap
包装器是不必要的。考虑删除它。享受!
data Ap a = Ap { runAp :: ApStep a }
data ApStep a =
ApRead (String -> Ap a)
| ApReturn a
| ApWrite String (Ap a)
ioRead k = Ap $ ApRead k
ioReturn a = Ap $ ApReturn a
ioWrite s k = Ap $ ApWrite s k
ioWriteLn s = ioWrite (s ++ "\n")
apTest =
ioWriteLn "Hello world!" $
ioRead $ \i ->
ioWriteLn ("You wrote [" ++ i ++ "]") $
ioReturn 10
uiRun ap =
case runAp ap of
ApRead k -> uiRun (k "Some input")
ApReturn a -> return a
ApWrite s k -> putStr s >> uiRun k
run = uiRun apTest
instance Monad Ap where
return = Ap . ApReturn
Ap (ApReturn a) >>= f = f a
Ap (ApRead r) >>= f = Ap (ApRead (\s -> r s >>= f))
Ap (ApWrite s a) >>= f = Ap (ApWrite s (a >>= f))
monadRead = Ap (ApRead (\s -> return s))
monadWrite s = Ap (ApWrite s (return ()))
monadWriteLn = monadWrite . (++ "\n")
apTestMonad = do
monadWriteLn "Hello world!"
i <- monadRead
monadWriteLn $ "You wrote [" ++ i ++ "]"
return 10
monadRun = uiRun apTestMonad
[1] http://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html
答案 1 :(得分:6)
我没有任何具体的帮助,但我有一些总体指导,这对评论来说太长了。当我的直觉告诉我,我想要制作一个Monad
的实例时,我做的第一件事就是用笔和一张纸坐下来,我问自己,
但我的东西真的是单身吗?
事实证明,很多时候事实并非如此 - 只是我的直觉想要加速赶时髦。如果你的东西不是monad,你不能很好地为你的东西创建一个Monad
实例。这是我在将我的东西称为monad之前需要涵盖的三件事的清单。
当我确定我的东西是一个monad时,我通常也会在这个过程中偶然想出为我的东西创建一个monad实例所需的一切,所以这不是没用的严谨地运动。这实际上将为您提供为您的事物创建monad实例所需的两个操作的实现。
为了成为一个monad,它需要有两个操作。在Haskell世界中,这些通常被称为return
和(>>=)
(发音为“bind”。)monad可以被视为具有某种“上下文”的计算。在IO
的情况下,上下文是副作用。在Maybe
的情况下,上下文无法提供值,依此类推。所以monad是有价值的东西,但不仅仅是价值。由于缺乏更好的词语,这一点通常被称为“背景”。
无论如何,涉及的签名是
return :: Monad m => a -> m a
(>>=) :: Monad m => m a -> (a -> m b) -> m b
这意味着return
接受任何旧值a
并以某种方式将其放入monad的上下文中。这通常是一个相当容易实现的函数(没有太多方法可以将任何a
值放入上下文中。)
有趣的是(>>=)
。它在monad上下文中的值a
和从任何值a
到新值b
的函数,但在monad上下文中。然后它返回带有上下文的b
值。在考虑为您的事物制作Monad
实例之前,您需要对此进行合理的实现。没有(>>=)
,你的东西肯定不是单子。
但是,仅return
和(>>=)
是不够的!我说实施需要合情合理。这也意味着您的东西必须具有符合monad法则的return
和(>>=)
的实现。它们如下:
return a >>= f
应与f a
m >>= return
应与m
(m >>= f) >>= g
应与m >>= (\x -> f x >>= g)
这些很有意义*(前两个是微不足道的,第三个只是一个相关性法则),我们需要所有monad遵守它们。编译器不会对此进行检查(但可能会认为它们会保留),因此您有责任确保它们成立。
如果你的monad遵守这些法律,你会得到一个monad!恭喜!其余的只是文书工作,即将实例定义为
instance Monad MyThing where
return a = {- definition -}
m >>= f = {- definition -}
然后您就可以使用do
语法了!
*有关the Haskell wiki page on monad laws的更多信息。
答案 2 :(得分:5)
我认为这就是你的目标。我所做的唯一更改是将Ap
和ApStep
压缩为单一类型。
data Ap a =
ApRead (String -> Ap a)
| ApWrite String (Ap a)
| ApReturn a
instance Monad Ap where
return = ApReturn
m >>= f = case m of
ApRead k -> ApRead (\x -> k x >>= f)
ApWrite str m' -> ApWrite str (m' >>= f)
ApReturn r -> f r
ioWriteLn :: String -> Ap ()
ioWriteLn str = ApWrite str (ApReturn ())
ioRead :: Ap String
ioRead = ApRead ApReturn
apTest :: Ap Int
apTest = do
ioWriteLn "Hello world!"
i <- ioRead
ioWriteLn ("You wrote [" ++ i ++ "]")
return 10
尽管使用do
表示法以monadic样式编写,apTest
与以下手写的构造函数链相同:
apTest :: Ap Int
apTest =
ApWrite "Hello, world!" $
ApRead $ \i ->
ApWrite ("You wrote [" ++ i ++ "]") $
ApReturn 10
这是一个免费monad的特例,所以只需编写代码就可以大大简化代码:
{-# LANGUAGE DeriveFunctor #-}
import Control.Monad.Free
data ApF next = Read (String -> next) | Write String next deriving (Functor)
type Ap = Free ApF
ioWriteLn :: String -> Ap ()
ioWriteLn str = liftF (Write str ())
ioRead :: Ap String
ioRead = liftF (Read id)
要了解有关免费monad的更多信息,您可以阅读我的post on free monads,其中详细介绍了如何将DSL转换为免费monad并建立直观的工作方式。