我以我的方式学习 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?
发布评论
评论(5)
一个有用的经验法则是当您在上下文中看到值时; monad 可以被视为对以下方面的分层“效果”:
通常,您通常应该通过在标准 < 的 monad 转换器上分层来设计您的 monad一个href="http://hackage.haskell.org/package/mtl" rel="nofollow noreferrer">Monad Transformer Library,它可以让您将上述效果组合到一个 monad 中。这些一起处理您可能想要使用的大多数 monad。还有一些其他 monad 未包含在 MTL 中,例如 概率 和 供应 单子。
至于对新定义的类型是否是 monad 以及它的行为方式形成一种直觉,您可以通过从
Functor
上升到Monad
来思考它:(<*>)
可让您从嵌入式函数及其嵌入式参数转到嵌入式结果。理解这一点的最简单方法是查看
join
的类型:这意味着如果您有一个嵌入式计算,其结果是一个新嵌入式计算,您可以创建一个执行该计算结果的计算。因此,您可以使用一元效应根据先前计算的值创建新计算,并将控制流转移到该计算。
有趣的是,这可能是单子结构的一个弱点:对于 Applicative,计算的结构是静态的(即给定的 Applicative 计算具有某种效果结构不能根据中间值而改变),而对于
Monad
它是动态的。这会限制您可以进行的优化;例如,应用解析器不如单子解析器强大(嗯,这不是 严格正确,但实际上是这样),但它们可以更好地优化。请注意,
(>>=)
可以定义为,因此可以简单地使用
return
和join
定义 monad(假设它是一个Functor
; 所有 monad 都是应用函子,但不幸的是 Haskell 的类型类层次结构并不要求 历史原因)。作为补充说明,您可能不应该过分关注 monad,无论它们从被误导的非 Haskeller 那里得到什么样的嗡嗡声。有许多类型类代表有意义且强大的模式,但并非所有内容都最好表达为 monad。 应用,Monoid,可折叠...使用哪个抽象完全取决于你的情况。当然,仅仅因为某个东西是单子并不意味着它不能也是其他东西;它只是一个单子。作为一个 monad 只是类型的另一个属性。
所以,你不应该过多考虑“识别单子”;问题更像是:
A helpful rule of thumb is when you see values in a context; monads can be seen as layering "effects" on:
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
toMonad
:(<*>)
lets you go from an embedded function and its embedded argument to an embedded result.The easiest way to understand this is to look at the type of
join
: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 givenApplicative
computation has a certain structure of effects that cannot change based on intermediate values), whereas withMonad
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 asand so a monad can be defined simply with
return
andjoin
(assuming it's aFunctor
; 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:
遵循类型。
如果您发现您编写了所有这些类型的函数
(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.
monad 的效果视图如下:
一旦您熟悉了这些效果,就可以轻松地将它们与 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:
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.这是一个没有答案的问题,但我觉得无论如何说出来还是很重要的。 尽管提问! StackOverflow、/r/haskell 和 #haskell irc 频道都是从聪明人那里获得快速反馈的好地方。如果您正在解决一个问题,并且您怀疑有一些一元魔法可以使问题变得更容易,请询问! Haskell 社区喜欢解决问题,而且非常友好。
别误会,我并不是鼓励你永远不要自己学习。恰恰相反,与 Haskell 社区互动是最好的学习方式之一。 LYAH 和 RWH< /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.
在许多编程语言中都有一个常见的概念“传染性函数标签”——函数的一些特殊行为也必须扩展到其调用者。
不安全
,这意味着它们执行的操作可能会违反内存不安全性。不安全
函数可以调用普通函数,但任何调用不安全
函数的函数也必须是不安全
。async
函数可以调用普通函数,但调用async
函数(通过await
)只能由另一个async
来完成代码>函数。IO a
而不是a
。非纯函数可以调用纯函数,但非纯函数只能被其他非纯函数调用。虽然可能有多种方法可以从未标记的函数调用标记的函数,但没有通用的方法,而且这样做通常很危险,并且可能会破坏语言试图提供的抽象。
那么,拥有标签的好处是,您可以公开一组被赋予此标签的特殊原语,并且使用这些原语的任何函数都可以在其签名中清楚地表明这一点。
假设您是一名语言设计师并且您认识到这种模式,并且您决定允许用户定义的标签。假设用户定义了一个标签
Err
,表示可能引发错误的计算。使用Err
的函数可能如下所示:如果我们想简化事情,我们可能会发现接受参数没有任何错误 - 它正在计算可能出现问题的返回值。因此,我们可以将标签限制为不带参数的函数,并且让
div
返回闭包而不是实际值:在像 Haskell 这样的惰性语言中,我们不需要闭包,并且可以直接返回一个惰性值:
现在很明显,在 Haskell 中,标签不需要特殊的语言支持 - 它们是普通的类型构造函数。让我们为它们创建一个类型类!
我们希望能够从标记函数调用未标记函数,这相当于将未标记值 (
a
) 转换为标记值 (m a
)。我们还希望能够获取标记值 (
m a
) 并应用标记函数 (a -> m b
) 来获取标记结果 (m b
):当然,这正是 monad 的定义!
addTag
对应于return
,embed
对应于(>>=)
。现在很清楚,“标记函数”只是一种 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.
unsafe
, meaning they perform operations that can potentially violate memory unsafety.unsafe
functions can call normal functions, but any function that calls anunsafe
function must beunsafe
as well.async
, meaning they return a promise rather than an actual value.async
functions can call normal functions, but invocation of anasync
function (viaawait
) can only be done by anotherasync
function.IO a
rather than ana
. Impure functions can call pure functions, but impure functions can only be called by other impure functions.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 usingErr
might look like this: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:In a lazy language such as Haskell, we don't need the closure, and can just return a lazy value directly:
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!
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
).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
):This, of course, is precisely the definition of a monad!
addTag
corresponds toreturn
, andembed
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 theMaybe
monad. Most languages implement async/promises fairly transparently, and there seems to be a Haskell package called promise that mimics this functionality. TheErr
monad is equivalent to theEither String
monad. I'm not aware of any language that models memory unsafety monadically, it could be done.