您如何识别一元设计模式?

发布于 2024-12-25 07:12:03 字数 213 浏览 4 评论 0 原文

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

那么,如何将问题识别为一元问题呢?对于一元设计来说,什么是好的设计模式?当您意识到某些代码最好重构为 monad 时,您的方法是什么?

I my way to learn Haskell I'm starting to grasp the monad concept and starting to use the known monads in my code but I'm still having difficulties approaching monads from a designer point of view. In OO there are several rules like, "identify nouns" for objects, watch for some kind of state and interface... but I'm not able to find equivalent resources for monads.

So how do you identify a problem as monadic in nature? What are good design patterns for monadic design? What's your approach when you realize that some code would be better refactored into a monad?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

黎夕旧梦 2025-01-01 07:12:03

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

  • 也许: 偏爱(使用:可能失败的计算)
  • 任一: 短路错误(使用:错误/异常处理)
  • [](列表单子):非确定性(使用:列表生成、过滤等)
  • 状态:单个可变引用(使用:状态)
  • Reader:共享环境(使用:变量绑定、公共信息等)
  • Writer:“旁路”输出或积累(使用:日志记录、维护只写计数器,...)
  • 继续:非本地控制流(用途:太多,无法列出)

通常,您通常应该通过在标准 < 的 monad 转换器上分层来设计您的 monad一个href="http://hackage.haskell.org/package/mtl" rel="nofollow noreferrer">Monad Transformer Library,它可以让您将上述效果组合到一个 monad 中。这些一起处理您可能想要使用的大多数 monad。还有一些其他 monad 未包含在 MTL 中,例如 概率供应 单子。

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

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

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

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

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

有趣的是,这可能是单子结构的一个弱点:对于 Applicative,计算的结构是静态的(即给定的 Applicative 计算具有某种效果结构不能根据中间值而改变),而对于 Monad 它是动态的。这会限制您可以进行的优化;例如,应用解析器不如单子解析器强大(嗯,这不是 严格正确,但实际上是这样),但它们可以更好地优化。

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

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

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

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

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

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

A helpful rule of thumb is when you see values in a context; monads can be seen as layering "effects" on:

  • Maybe: partiality (uses: computations that can fail)
  • Either: short-circuiting errors (uses: error/exception handling)
  • [] (the list monad): nondeterminism (uses: list generation, filtering, ...)
  • State: a single mutable reference (uses: state)
  • Reader: a shared environment (uses: variable bindings, common information, ...)
  • Writer: a "side-channel" output or accumulation (uses: logging, maintaining a write-only counter, ...)
  • Cont: non-local control-flow (uses: too numerous to list)

Usually, you should generally design your monad by layering on the monad transformers from the standard Monad Transformer Library, which let you combine the above effects into a single monad. Together, these handle the majority of monads you might want to use. There are some additional monads not included in the MTL, such as the probability and supply monads.

As far as developing an intuition for whether a newly-defined type is a monad, and how it behaves as one, you can think of it by going up from Functor to Monad:

  • Functor lets you transform values with pure functions.
  • Applicative lets you embed pure values and express application — (<*>) lets you go from an embedded function and its embedded argument to an embedded result.
  • Monad lets the structure of embedded computations depend on the values of previous computations.

The easiest way to understand this is to look at the type of join:

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

This means that if you have an embedded computation whose result is a new embedded computation, you can create a computation that executes the result of that computation. So you can use monadic effects to create a new computation based on values of previous computations, and transfer control flow to that computation.

Interestingly, this can be a weakness of structuring things monadically: with Applicative, the structure of the computation is static (i.e. a given Applicative computation has a certain structure of effects that cannot change based on intermediate values), whereas with Monad it is dynamic. This can restrict the optimisation you can do; for instance, applicative parsers are less powerful than monadic ones (well, this isn't strictly true, but it effectively is), but they can be optimised better.

Note that (>>=) can be defined as

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

and so a monad can be defined simply with return and join (assuming it's a Functor; all monads are applicative functors, but Haskell's typeclass hierarchy unfortunately doesn't require this for historical reasons).

As an additional note, you probably shouldn't focus too heavily on monads, no matter what kind of buzz they get from misguided non-Haskellers. There are many typeclasses that represent meaningful and powerful patterns, and not everything is best expressed as a monad. Applicative, Monoid, Foldable... which abstraction to use depends entirely on your situation. And, of course, just because something is a monad doesn't mean it can't be other things too; being a monad is just another property of a type.

So, you shouldn't think too much about "identifying monads"; the questions are more like:

  • Can this code be expressed in a simpler monadic form? With which monad?
  • Is this type I've just defined a monad? What generic patterns encoded by the standard functions on monads can I take advantage of?
命硬 2025-01-01 07:12:03

遵循类型。

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

  • (a -> b) ->您输入 a -> YourType b
  • a -> YourType a
  • YourType (YourType a) -> YourType a

或所有这些类型

  • a -> YourType a
  • YourType a -> (a -> 你的类型 b) -> YourType b

那么 YourType 可能是一个 monad。 (我说“可能”是因为函数也必须遵守 monad 法则。)

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

不要只关注 monad!如果您有所有这些类型的函数

  • YourType
  • YourType ->您的类型 -> YourType

并且它们遵守幺半群法则,你就有了一个幺半群!这也很有价值。对于其他类型类也是如此,最重要的是 Functor。

Follow the types.

If you find you have written functions with all of these types

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

or all of these types

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

then YourType may be a monad. (I say “may” because the functions must obey the monad laws as well.)

(Remember you can reorder arguments, so e.g. YourType a -> (a -> b) -> YourType b is just (a -> b) -> YourType a -> YourType b in disguise.)

Don't look out only for monads! If you have functions of all of these types

  • YourType
  • YourType -> YourType -> YourType

and they obey the monoid laws, you have a monoid! That can be valuable too. Similarly for other typeclasses, most importantly Functor.

叫嚣ゝ 2025-01-01 07:12:03

monad 的效果视图如下:

  • Maybe - 偏爱/失败短路
  • Either - 错误报告/短路(就像 Maybe 带有更多信息)
  • Writer - 只写“状态”,通常记录
  • Reader - 只读状态,通常环境传递
  • 状态 - 读/写状态
  • 恢复 - 可暂停计算
  • 列表 - 多次成功

一旦您熟悉了这些效果,就可以轻松地将它们与 monad 转换器结合起来构建 monad。请注意,组合某些 monad 需要特别小心(特别是 Cont 和任何具有回溯功能的 monad)。

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

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

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

如果将 WriterT(转换器)添加到 Error monad,您将获得此 monad (log, Either err a),它始终提供对日志的访问权限。

There's the effect view of monads:

  • Maybe - partiality / failure short-circuiting
  • Either - error reporting / short-circuiting (like Maybe with more information)
  • Writer - write only "state", commonly logging
  • Reader - read-only state, commonly environment passing
  • State - read / write state
  • Resumption - pausable computation
  • List - multiple successes

Once you are familiar with these effects its easy to build monads combining them with monad transformers. Note that combining some monads needs special care (particularly Cont and any monads with backtracking).

One thing important to note is there aren't many monads. There are some exotic ones that aren't in the standard libraries e.g the probability monad and variations of the Cont monad like Codensity. But unless you are doing something mathematical its unlikely you will invent (or discover) a new monad, however if you use Haskell long enough you'll build many monads that are different combinations of the standard ones.

Edit - Also note that the order you stack monad transformers results in different monads:

If you add ErrorT (transformer) to a Writer monad, you get this monad Either err (log,a) - you can only access the log if you have no error.

If you add WriterT (transfomer) to an Error monad, you get this monad (log, Either err a) which always gives access to the log.

铃予 2025-01-01 07:12:03

这是一个没有答案的问题,但我觉得无论如何说出来还是很重要的。 尽管提问! StackOverflow、/r/haskell 和 #haskell irc 频道都是从聪明人那里获得快速反馈的好地方。如果您正在解决一个问题,并且您怀疑有一些一元魔法可以使问题变得更容易,请询问! Haskell 社区喜欢解决问题,而且非常友好。

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

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

This is sort of a non-answer, but I feel it is important to say anyways. Just ask! StackOverflow, /r/haskell, and the #haskell irc channel are all great places to get quick feedback from smart people. If you are working on a problem, and you suspect that there's some monadic magic that could make it easier, just ask! The Haskell community loves to solve problems, and is ridiculously friendly.

Don't misunderstand, I'm not encouraging you to never learn for yourself. Quite the contrary, interacting with the Haskell community is one of the best ways to learn. LYAH and RWH, 2 Haskell books that are freely available online, come highly recommended as well.

Oh, and don't forget to play, play, play! As you play around with monadic code, you'll start to get the feel of what "shape" monads have, and when monadic combinators can be useful. If you're rolling your own monad, then usually the type system will guide you to an obvious, simple solution. But to be honest, you should rarely need to roll your own instance of Monad, since Haskell libraries provide tons of useful things as mentioned by other answerers.

夜巴黎 2025-01-01 07:12:03

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

  • Rust 函数可能是不安全,这意味着它们执行的操作可能会违反内存不安全性。 不安全函数可以调用普通函数,但任何调用不安全函数的函数也必须是不安全
  • Python 函数可以是异步的,这意味着它们返回一个承诺而不是实际值。 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 -> m b) 来获取标记结果 (m b ):

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

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

现在很清楚,“标记函数”只是一种 monad。因此,每当你发现一个可以应用“函数标签”的地方时,很可能你就有了一个适合 monad 的地方。

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

There's a common notion that one sees in many programming languages of an "infectious function tag" -- some special behavior for a function that must extend to its callers as well.

  • Rust functions can be unsafe, meaning they perform operations that can potentially violate memory unsafety. unsafe functions can call normal functions, but any function that calls an unsafe function must be unsafe as well.
  • Python functions can be async, meaning they return a promise rather than an actual value. async functions can call normal functions, but invocation of an async function (via await) can only be done by another async function.
  • Haskell functions can be impure, meaning they return an IO a rather than an a. Impure functions can call pure functions, but impure functions can only be called by other impure functions.
  • Mathematical functions can be partial, meaning they don't map every value in their domain to an output. The definitions of partial functions can reference total functions, but if a total function maps some of its domain to a partial function, it becomes partial as well.

While there may be ways to invoke a tagged function from an untagged function, there is no general way, and doing so can often be dangerous and threatens to break the abstraction the language tries to provide.

The benefit, then, of having tags is that you can expose a set of special primitives that are given this tag and have any function that uses these primitives make that clear in its signature.

Say you're a language designer and you recognize this pattern, and you decide that you want to allow user-defined tags. Let's say the user defined a tag Err, representing computations that may throw an error. A function using Err might look like this:

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

If we wanted to simplify things, we might observe that there's nothing erroneous about taking arguments - it's computing the return value where problems might arise. So we can restrict tags to functions that take no arguments, and have div return a closure rather than the actual value:

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

In a lazy language such as Haskell, we don't need the closure, and can just return a lazy value directly:

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

It is now apparent that, in Haskell, tags need no special language support - they are ordinary type constructors. Let's make a typeclass for them!

class Tag m where

We want to be able to call an untagged function from a tagged function, which is equivalent to turning an untagged value (a) into a tagged value (m a).

    addTag :: a -> m a

We also want to be able to take a tagged value (m a) and apply a tagged function (a -> m b) to get a tagged result (m b):

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

This, of course, is precisely the definition of a monad! addTag corresponds to return, and embed corresponds to (>>=).

It is now clear that "tagged functions" are merely a type of monad. As such, whenever you spot a place where a "function tag" could apply, chances are you've got a place suitable for a monad.

P.S. Regarding the tags I've mentioned in this answer: Haskell models impurity with the IO monad and partiality with the Maybe monad. Most languages implement async/promises fairly transparently, and there seems to be a Haskell package called promise that mimics this functionality. The Err monad is equivalent to the Either String monad. I'm not aware of any language that models memory unsafety monadically, it could be done.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文