如何在Haskell中将功能DSL转换为Monad?

时间:2013-11-30 09:53:57

标签: haskell monads dsl

以下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样式而不是“$”样式?

3 个答案:

答案 0 :(得分:10)

当然这是一个单子。事实上,将它表达为一个免费的monad会更简单[1],但我们可以使用你所拥有的东西。

以下是我们如何知道它是monad:如果你有一个类型data Foo a = ...,其中Foo表示某种递归树结构,其中a仅出现在树叶上,那么你有一个单子。 return a是“给我一棵由a标记的一片叶子组成的树”,>>=是“叶子上的替代”。

在您的情况下,Ap是一个树结构

  • ApReturn a是一片叶子
  • 有两种内部节点

    1. ApRead是一个没有标签的节点,并且每个类型为String
    2. 的值都有一个后代
    3. ApWrite是一个标有String并且只有一个后代脱离的节点

我已将monad实例添加到您的代码中。 return只是AppReturn(加上Ap包装器)。 >>=只是递归地应用>>=并在叶子处替换。

未来的一些提示

  1. 在顶级的所有内容上放置类型签名。您的同事,Stack Overflow评论者和您未来的自我,谢谢。
  2. Ap包装器是不必要的。考虑删除它。
  3. 享受!

    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实例所需的两个操作的实现。

什么是monads?

为了成为一个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(>>=)的实现。它们如下:

  1. return a >>= f应与f a
  2. 相同
  3. m >>= return应与m
  4. 相同
  5. (m >>= f) >>= g应与m >>= (\x -> f x >>= g)
  6. 相同

    这些很有意义*(前两个是微不足道的,第三个只是一个相关性法则),我们需要所有monad遵守它们。编译器不会对此进行检查(但可能会认为它们会保留),因此您有责任确保它们成立。

    如果你的monad遵守这些法律,你会得到一个monad!恭喜!其余的只是文书工作,即将实例定义为

    instance Monad MyThing where
      return a = {- definition -}
      m >>= f  = {- definition -}
    

    然后您就可以使用do语法了!


    *有关the Haskell wiki page on monad laws的更多信息。

答案 2 :(得分:5)

我认为这就是你的目标。我所做的唯一更改是将ApApStep压缩为单一类型。

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并建立直观的工作方式。