【问题标题】:How do you identify monadic design patterns?你如何识别一元设计模式?
【发布时间】:2012-02-05 07:50:09
【问题描述】:

我学习 Haskell 的方式我开始掌握 monad 概念并开始在我的代码中使用已知的 monad,但从设计人员的角度来看,我仍然难以接近 monad。在 OO 中有几条规则,例如对象的“识别名词”、监视某种状态和接口……但我无法为 monad 找到等效的资源。

那么,您如何将问题识别为本质上的一元问题?一元设计的好的设计模式是什么?当您意识到将某些代码重构为 monad 会更好时,您的方法是什么?

【问题讨论】:

  • 不是“这个问题应该用Monadically解决”,而是“这个问题应该用[某种数据类型]解决,嘿!多么方便,[那个数据类型]是Monad的一个实例,给了我大量的可组合性。”
  • @DanBurton:这当然与任何其他类型的设计模式的应用方式相同,无论是面向对象、过程、功能、回溯逻辑、串联还是任何其他一种语言。

标签: haskell functional-programming monads


【解决方案1】:

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

  • 也许:偏心(使用:可能失败的计算)
  • 任一:短路错误(用途:错误/异常处理)
  • [](列表单子):不确定性(使用:列表生成、过滤……)
  • 状态:单个可变引用(使用:状态)
  • 阅读器:共享环境(使用:变量绑定、公共信息……)
  • Writer:“侧通道”输出或累积(用途:记录、维护只写计数器……)
  • 续: 非本地控制流(用途:不胜枚举)

通常,您通常应该通过将标准 Monad Transformer Library 中的 monad 转换器分层来设计您的 monad,这样您就可以将上述效果组合成一个 monad。它们一起处理您可能想要使用的大多数 monad。 MTL 中没有包含一些额外的 monad,例如 probabilitysupply monad。

至于对新定义的类型是否为 monad 及其行为方式的直觉,您可以通过从 Functor 上升到 Monad 来思考它:

  • Functor 可让您使用纯函数转换值。
  • Applicative 可让您嵌入纯值并表达应用程序 - (<*>) 可让您从嵌入式函数及其嵌入式参数转到嵌入式结果。
  • Monad 让嵌入式计算的结构依赖于先前计算的

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

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

这意味着如果您有一个嵌入式计算,其结果是一个嵌入式计算,您可以创建一个计算来执行该计算的结果。因此,您可以使用单子效应基于先前计算的值创建新的计算,并将控制流转移到该计算。

有趣的是,这可能是一元结构事物的弱点:使用Applicative,计算结构是静态的(即给定的Applicative 计算具有一定的效果结构,不能根据中间值更改),而 Monad 它是动态的。这会限制您可以进行的优化;例如,应用解析器不如单子解析器强大(好吧,这不是strictly true,但实际上是),但它们可以得到更好的优化。

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

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

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

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

所以,你不应该过多地考虑“识别单子”;这些问题更像是:

  • 能否以更简单的一元形式表示此代码?使用哪个 monad?
  • 这种类型是我刚刚定义的单子吗?我可以利用 monad 上的标准函数编码的哪些通用模式?

【讨论】:

  • 哦,非常好。如果我不睡觉,我会写的答案差不多,哈哈。伙计,我几乎跟不上你们了……
  • @C.A.McCann:有声望你怎么能睡?! :)
  • @ehird +1 你最近已经提出了很多很棒的答案。
【解决方案2】:

按照类型。

如果你发现你已经编写了所有这些类型的函数

  • (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可能是一个单子。 (我说“可能”是因为函数也必须遵守单子定律。)

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

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

  • YourType
  • YourType -> YourType -> YourType

并且他们遵守幺半群定律,你有一个幺半群!这也可能很有价值。其他类型类也是如此,最重要的是 Functor。

【讨论】:

    【解决方案3】:

    还有monads的效果图:

    • 也许 - 偏心/故障短路
    • 要么 - 错误报告/短路(如可能提供更多信息)
    • Writer - 只写“状态”,通常是日志记录
    • Reader - 只读状态,通常是环境传递
    • 状态 - 读/写状态
    • 恢复 - 可暂停计算
    • 列表 - 多次成功

    一旦您熟悉了这些效果,就很容易将它们与 monad 转换器结合起来构建 monad。请注意,组合一些 monad 需要特别小心(特别是 Cont 和任何带有回溯的 monad)。

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

    编辑 - 另请注意,您堆叠 monad 转换器的顺序会导致不同的 monad:

    如果你将 ErrorT (transformer) 添加到 Writer monad,你会得到这个 monad Either err (log,a) - 如果你没有错误,你只能访问日志。

    如果你将 WriterT (transfomer) 添加到 Error monad,你会得到这个 monad (log, Either err a),它总是可以访问日志。

    【讨论】:

      【解决方案4】:

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

      别误会,我并不是在鼓励你永远不要自学。恰恰相反,与 Haskell 社区互动是最好的学习方式之一。 LYAHRWH,两本可在线免费获得的 Haskell 书籍,强烈推荐。

      哦,别忘了玩,玩,玩!当你玩弄单子代码时,你会开始感觉到单子的“形状”是什么,什么时候一元组合器可能很有用。如果您正在滚动自己的 monad,那么类型系统通常会引导您找到一个明显、简单的解决方案。但老实说,您应该很少需要推出自己的 Monad 实例,因为 Haskell 库提供了大量有用的东西,正如其他回答者所提到的那样。

      【讨论】:

        【解决方案5】:

        在许多编程语言中都有一个“传染性函数标签”的常见概念——函数的一些特殊行为也必须扩展到其调用者。

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

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

        因此,拥有标签的好处是,您可以公开一组特殊的原语,这些原语被赋予了这个标签,并且任何使用这些原语的函数在其签名中都清楚地表明了这一点。

        假设您是一名语言设计师并且您认识到这种模式,并且您决定要允许用户定义的标签。假设用户定义了一个标签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 -&gt; m b) 来获得标记结果 (m b):

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

        当然,这正是 monad 的定义! addTag对应returnembed对应(&gt;&gt;=)

        现在很明显,“标记函数”只是一种 monad。因此,每当您发现可以应用“功能标签”的地方时,您就有可能找到适合 monad 的地方。

        附:关于我在这个答案中提到的标签:Haskell 用IO monad 模拟杂质,用Maybe monad 模拟偏心。大多数语言都相当透明地实现 async/promises,并且似乎有一个名为 promise 的 Haskell 包模仿了这个功能。 Err monad 等价于 Either String monad。我不知道有任何语言可以单元地模拟内存不安全性,这是可以做到的。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2010-11-12
          • 1970-01-01
          • 1970-01-01
          • 2020-07-08
          • 1970-01-01
          • 2014-07-08
          • 1970-01-01
          • 2021-08-24
          相关资源
          最近更新 更多