你如何识别monadic设计模式?

时间:2012-01-08 11:46:46

标签: haskell functional-programming monads

我学习Haskell的方法我开始掌握monad概念并开始在我的代码中使用已知的monad但是从设计师的角度来看我仍然很难接近monad。在OO中有几个规则,比如“识别名词”对象,注意某种状态和界面......但是我无法找到monad的等效资源。

那么你如何将问题确定为monadic? monadic设计有哪些好的设计模式?当你意识到某些代码会更好地重构为monad时,你的方法是什么?

5 个答案:

答案 0 :(得分:58)

一个有用的经验法则是当您在上下文中看到值; monads可以看作是对“效果”的分层:

  • 可能:偏好(使用:可能失败的计算)
  • 要么:短路错误(使用:错误/异常处理)
  • [](列表monad): nondeterminism(使用:列表生成,过滤,...)
  • 状态:一个可变引用(使用:state)
  • 读者:共享环境(使用:变量绑定,公共信息,......)
  • 作家:“旁道”输出或累积(使用:记录,维护只写计数器,......)
  • 续:非本地控制流程(使用:数量太多无法列出)

通常,你应该通过在标准Monad Transformer Library上对monad变换器进行分层来设计你的monad,这样你就可以将上面的效果组合成一个monad。它们共同处理您可能想要使用的大多数monad。 MTL中还包含一些其他monad,例如probabilitysupply monad。

就新定义的类型是否为monad以及它如何表现为一个直觉而言,你可以通过从Functor上升到Monad来考虑它:

  • Functor 可让您使用纯函数转换值。
  • 适用允许您嵌入纯值并表达应用程序 - (<*>)允许您从嵌入式函数及其嵌入式参数转换为嵌入式结果。
  • Monad 让嵌入式计算的结构取决于以前计算的

理解这一点的最简单方法是查看join的类型:

join :: (Monad m) => m (m a) -> m a

这意味着如果您的嵌入式计算结果是 new 嵌入式计算,则可以创建执行该计算结果的计算。因此,您可以使用monadic效果基于先前计算的值创建新计算,并将传输控制流程转换为该计算。

有趣的是,这可能是单独构造事物的弱点:使用Applicative,计算的结构是静态的(即给定的Applicative计算具有一定的结构不能基于中间值改变的效果),而Monad则是动态的。这可能会限制您可以执行的优化;例如,应用解析器不如monadic解析器(嗯,这不是strictly true,但它实际上是有效的),但它们可以更好地进行优化。

请注意,(>>=)可以定义为

m >>= f = join (fmap f m)

所以可以使用returnjoin简单地定义monad(假设它是Functor;所有monad都是适用的函子,但不幸的是Haskell的类型类层次结构不需要historical reasons)。

作为补充说明,你可能不应该过分关注monad,无论他们从被误导的非Haskeller那里得到什么样的嗡嗡声。有许多类型类代表有意义和强大的模式,并不是所有的东西都被最好地表达为monad。 ApplicativeMonoidFoldable ...使用哪种抽象完全取决于您的情况。而且,当然,仅仅因为某些东西是monad并不意味着它也不能成为其他东西;作为一个单子只是一种类型的另一种财产。

所以,你不应该过多考虑“识别monads”;问题更像是:

  • 这段代码可以用更简单的monadic形式表达吗?哪个monad?
  • 这种类型我刚刚定义了一个monad吗?我可以利用monad上标准函数编码的通用模式吗?

答案 1 :(得分:15)

按照类型进行操作。

如果您发现所有这些类型的书面函数

  • (a -> b) -> YourType a -> YourType b
  • a -> YourType a
  • YourType (YourType a) -> YourType a

或所有这些类型

  • a -> YourType a
  • YourType a -> (a -> YourType b) -> YourType b

然后YourType 可能成为monad。 (我说“可能”,因为这些功能也必须遵守monad法则。)

(请记住,您可以重新排序参数,例如YourType a -> (a -> b) -> YourType b只是伪装成(a -> b) -> YourType a -> YourType b。)

不要只注意单子!如果你有所有这些类型的功能

  • YourType
  • YourType -> YourType -> YourType

他们遵守幺半群定律,你有一个幺半群!这也很有价值。类似地,对于其他类型类,最重要的是Functor。

答案 2 :(得分:7)

有monad的效果视图:

  • 可能 - 偏向/失败短路
  • 要么 - 错误报告/短路(比如可能有更多信息)
  • 作家 - 只写&#34;州&#34;,通常记录
  • 读者 - 只读状态,通常是环境传递
  • 状态 - 读/写状态
  • 恢复 - 可计算的计算
  • 列表 - 多次成功

一旦熟悉了这些效果,就可以轻松构建monad,将它们与monad变换器相结合。请注意,组合一些monad需要特别小心(特别是Cont和任何带回溯的monad)。

有一点需要注意的是,没有很多单子。在标准库中有一些奇特的东西,例如概率monad和Cont monad的变体,如Codensity。但是,除非你正在做一些数学上的东西,否则你不可能发明(或发现)一个新的monad,但是如果你使用Haskell的时间足够长,你将构建许多不同组合的monad。

编辑 - 另请注意,堆叠monad变换器的顺序会产生不同的monad:

如果将ErrorT(变换器)添加到Writer monad,则会获得此monad Either err (log,a) - 如果没有错误,则只能访问日志。

如果将WriterT(transfomer)添加到Error monad,则会获得此monad (log, Either err a),它始终可以访问日志。

答案 3 :(得分:4)

这是一种非答案,但我觉得无论如何都要说。 请问! StackOverflow,/ r / haskell和#haskell irc频道都是从聪明人那里获得快速反馈的好地方。如果你正在解决一个问题,并且你怀疑有一些可以让它更容易的monadic魔法,那就问问吧! Haskell社区喜欢解决问题,并且非常友好。

不要误解,我不鼓励你永远不要为自己学习。恰恰相反,与Haskell社区的互动是最佳学习方式之一。 LYAHRWH,强烈推荐2本可在线免费获取的Haskell书籍。

哦,不要忘记玩,玩,玩!当你玩monadic代码时,你会开始感受到monad的“形状”,以及monadic组合器可能很有用。如果您正在推出自己的monad,那么通常类型系统将引导您找到一个明显,简单的解决方案。但说实话,你应该很少需要推出自己的Monad实例,因为Haskell库提供了其他回答者提到的大量有用的东西。

答案 4 :(得分:0)

有一种普遍的观念,即人们在许多编程语言中都看到“传染性功能标签”,这是一种对功能的特殊行为,必须同时扩展到其调用者。

  • Rust函数可以为unsafe,这意味着它们执行的操作有可能违反内存不安全性。 unsafe函数可以调用普通函数,但是任何调用unsafe函数的函数也必须是unsafe
  • Python函数可以是async,这意味着它们返回的是承诺而不是实际值。 async函数可以调用普通函数,但是只能通过另一个async函数来调用await函数(通过async)。
  • Haskell函数可以是不纯,这意味着它们返回IO a而不是a。不纯函数可以调用纯函数,但是不纯函数只能由其他不纯函数调用。
  • 数学函数可以是 partial ,这意味着它们不会将其域中的每个值都映射到输出。局部函数的定义可以引用整体函数,但是如果整体函数将其某些域映射到局部函数,它也将成为局部函数。

虽然可能有从未标记函数调用标记函数的方法,但没有 general 方法,这样做通常很危险,并且有可能破坏该语言试图提供的抽象。

那么,拥有标签的好处是,您可以公开给此标签指定的一组特殊基元,并且任何使用这些基元的功能都可以在其签名中清楚地说明这一点。

假设您是语言设计师,并且意识到这种模式,然后决定要允许用户定义的标签。假设用户定义了一个标签Err,该标签表示可能会引发错误的计算。使用Err的函数可能如下所示:

function div <Err> (n: Int, d: Int): Int
    if d == 0
        throwError("division by 0")
    else
        return (n / d)

如果我们想简化事情,我们可能会发现接受参数并没有错误-它正在计算可能出现问题的返回值。因此,我们可以将标记限制为不带参数的函数,并让div返回一个闭包而不是实际值:

function div(n: Int, d: Int): <Err> () -> Int
    () =>
        if d == 0
            throwError("division by 0")
        else
            return (n / d)

在像Haskell这样的惰性语言中,我们不需要闭包,而可以直接返回一个惰性值:

div :: Int -> Int -> Err Int
div _ 0 = throwError "division by 0"
div n d = return $ n / d

现在很明显,在Haskell中,标记不需要特殊的语言支持-它们是普通的类型构造函数。让我们为他们创建一个类型类!

class Tag m where

我们希望能够从标记函数中调用未标记函数,这等效于将未标记值(a)转换为标记值(m a)。

    addTag :: a -> m a

我们还希望能够获取标记的值(m a)并应用标记的函数(a -> m b)以获得标记的结果(m b):

    embed :: m a -> (a -> m b) -> m b

这当然是monad的定义! addTag对应于returnembed对应于(>>=)

现在很明显,“标记函数”仅仅是monad的一种。这样,每当您发现可以应用“功能标签”的地方时,就有可能有一个适合于monad的地方。

P.S。关于我在此答案中提到的标签:Haskell用IO单子模拟杂质,并用Maybe单子模拟偏性。大多数语言都相当透明地实现了异步/承诺,并且似乎有一个名为promise的Haskell软件包可以模仿此功能。 Err单子等效于Either String单子。我不知道有任何一种语言可以一味地模拟内存安全性,可以做到这一点。