与 Functor 不同,Monad 可以改变形状?
我一直很喜欢以下关于单子相对于函子的力量的直观解释:单子可以改变形状;函子不能。
例如:length $ fmap f [1,2,3]
始终等于 3
。
然而,对于 monad,length $ [1,2,3] >>= g
通常不等于 3
。例如,如果 g
定义为:
g :: (Num a) => a -> [a]
g x = if x==2 then [] else [x]
则 [1,2,3] >>= g
等于 [1,3].
让我有点困扰的是 g
的类型签名。使用通用的一元类型来定义一个改变输入形状的函数似乎是不可能的,例如:
h :: (Monad m, Num a) => a -> m a
MonadPlus 或 MonadZero 类型类具有相关的零元素,可以用来代替 []
,但现在我们拥有的不仅仅是单子。
我说得对吗?如果是这样,有没有办法向 Haskell 新手表达这种微妙之处。我想让我心爱的“单子可以改变形状”这句话变得更诚实一些;如果需要的话。
I've always enjoyed the following intuitive explanation of a monad's power relative to a functor: a monad can change shape; a functor cannot.
For example: length $ fmap f [1,2,3]
always equals 3
.
With a monad, however, length $ [1,2,3] >>= g
will often not equal 3
. For example, if g
is defined as:
g :: (Num a) => a -> [a]
g x = if x==2 then [] else [x]
then [1,2,3] >>= g
is equal to [1,3]
.
The thing that troubles me slightly, is the type signature of g
. It seems impossible to define a function which changes the shape of the input, with a generic monadic type such as:
h :: (Monad m, Num a) => a -> m a
The MonadPlus or MonadZero type classes have relevant zero elements, to use instead of []
, but now we have something more than a monad.
Am I correct? If so, is there a way to express this subtlety to a newcomer to Haskell. I'd like to make my beloved "monads can change shape" phrase, just a touch more honest; if need be.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(8)
顺便说一句,你在这里错过了一些微妙之处。为了术语方便,我将 Haskell 意义上的 Functor 分为三个部分: 由类型参数确定并由 fmap 操作的参数组件,不变的组件例如
State
中的元组构造函数,以及其他任何内容的“形状”,例如构造函数之间的选择(例如,Nothing
与Just) 或涉及其他类型参数的部分(例如,
Reader
中的环境)。当然,函子本身仅限于将函数映射到参数部分。
Monad
可以根据参数部分的值创建新的“形状”,这不仅仅是改变形状。复制列表中的每个元素或删除前五个元素都会更改形状,但过滤列表需要检查元素。这本质上就是 Applicative 在它们之间的配合方式——它允许您独立组合两个函子的形状和参数值,而不让后者影响前者。
也许您在这里寻找的微妙之处在于您并没有真正“改变”任何东西。
Monad
中没有任何内容可以让您明确地弄乱形状。它可以让您根据每个参数值创建新形状,并将这些新形状重新组合成新的复合形状。因此,您将始终受到创建形状的可用方法的限制。对于完全通用的
Monad
,您所拥有的就是return
,根据定义,它会创建任何必要的形状,以便(>>= return)
是恒等函数。Monad
的定义告诉您在给定某些类型的函数的情况下您可以做什么;它不为您提供这些功能。You're missing a bit of subtlety here, by the way. For the sake of terminology, I'll divide a
Functor
in the Haskell sense into three parts: The parametric component determined by the type parameter and operated on byfmap
, the unchanging parts such as the tuple constructor inState
, and the "shape" as anything else, such as choices between constructors (e.g.,Nothing
vs.Just
) or parts involving other type parameters (e.g., the environment inReader
).A
Functor
alone is limited to mapping functions over the parametric portion, of course.A
Monad
can create new "shapes" based on the values of the parametric portion, which allows much more than just changing shapes. Duplicating every element in a list or dropping the first five elements would change the shape, but filtering a list requires inspecting the elements.This is essentially how
Applicative
fits between them--it allows you to combine the shapes and parametric values of twoFunctors
independently, without letting the latter influence the former.Perhaps the subtlety you're looking for here is that you're not really "changing" anything. Nothing in a
Monad
lets you explicitly mess with the shape. What it lets you do is create new shapes based on each parametric value, and have those new shapes recombined into a new composite shape.Thus, you'll always be limited by the available ways to create shapes. With a completely generic
Monad
all you have isreturn
, which by definition creates whatever shape is necessary such that(>>= return)
is the identity function. The definition of aMonad
tells you what you can do, given certain kinds of functions; it doesn't provide those functions for you.Monad
的操作可以“改变值的形状”,即>>=
函数替换“树”中的叶节点(即原始值)具有从节点值派生的新子结构(对于“树”的适当一般概念 - 在列表情况下,“树”是关联的)。在您的列表示例中,发生的情况是每个数字(叶子)都被应用
g
到该数字时产生的新列表所替换。如果您知道要查找的内容,仍然可以看到原始列表的整体结构;g
的结果仍然按顺序存在,它们只是被粉碎在一起,因此除非您已经知道,否则您无法分辨一个在哪里结束,下一个从哪里开始。一个更有启发性的观点可能是考虑
fmap
和join
而不是>>=
。与return
一起,任何一种方式都给出了 monad 的等效定义。不过,在fmap
/join
视图中,这里发生的情况更加清晰。继续您的列表示例,第一个g
在列表上进行fmap
ped,产生[[1],[],[3]]
。然后该列表被join
ed,对于列表来说只是concat
。Monad
's operations can "change the shape" of values to the extent that the>>=
function replaces leaf nodes in the "tree" that is the original value with a new substructure derived from the node's value (for a suitably general notion of "tree" - in the list case, the "tree" is associative).In your list example what is happening is that each number (leaf) is being replaced by the new list that results when
g
is applied to that number. The overall structure of the original list still can be seen if you know what you're looking for; the results ofg
are still there in order, they've just been smashed together so you can't tell where one ends and the next begins unless you already know.A more enlightening point of view may be to consider
fmap
andjoin
instead of>>=
. Together withreturn
, either way gives an equivalent definition of a monad. In thefmap
/join
view, though, what is happening here is more clear. Continuing with your list example, firstg
isfmap
ped over the list yielding[[1],[],[3]]
. Then that list isjoin
ed, which for list is justconcat
.仅仅因为 monad 模式包含一些允许形状更改的特定实例,并不意味着每个实例都可以进行形状更改。例如,
Identity
monad 中只有一个可用的“shape”:事实上,我不清楚很多 monad 都有有意义的“shape”:例如,shape 在
State
、Reader
、Writer
、ST
、STM
或IO 单子?
Just because the monad pattern includes some particular instances that allow shape changes doesn't mean every instance can have shape changes. For example, there is only one "shape" available in the
Identity
monad:In fact, it's not clear to me that very many monads have meaningful "shape"s: for example, what does shape mean in the
State
,Reader
,Writer
,ST
,STM
, orIO
monads?单子的键组合符是
(>>=)
。知道它由两个单子值组成并读取其类型签名,单子的力量变得更加明显:未来的操作可以完全取决于第一个操作的结果,因为它是其结果的函数。不过,这种能力是有代价的:Haskell 中的函数是完全不透明的,因此您无法在不实际运行组合操作的情况下获取有关组合操作的任何信息。附带说明一下,这就是箭头发挥作用的地方。
The key combinator for monads is
(>>=)
. Knowing that it composes two monadic values and reading its type signature, the power of monads becomes more apparent:The future action can depend entirely on the outcome of the first action, because it is a function of its result. This power comes at a price though: Functions in Haskell are entirely opaque, so there is no way for you to get any information about a composed action without actually running it. As a side note, this is where arrows come in.
具有像
h
这样的签名的函数除了对其参数执行一些算术之外确实不能做许多有趣的事情。所以,你有正确的直觉。但是,查看 具有相似签名的函数。您会发现,正如您所期望的,最通用的执行通用 monad 操作,例如
return
、liftM
或join
。此外,当您使用 liftM 或 fmap 将普通函数提升为一元函数时,通常会得到类似的通用签名,这对于集成纯函数非常方便具有一元代码的函数。为了使用特定 monad 提供的结构,您不可避免地需要使用有关您所在的特定 monad 的一些知识来在该 monad 中构建新的有趣的计算。考虑状态单子,
(s -> (a, s))
。如果不知道该类型,我们就无法编写get = \s ->; (s, s)
,但是如果无法访问状态,那么处于 monad 中就没有多大意义。A function with a signature like
h
indeed cannot do many interesting things beyond performing some arithmetic on its argument. So, you have the correct intuition there.However, it might help to look at commonly used libraries for functions with similar signatures. You'll find that the most generic ones, as you'd expect, perform generic monad operations like
return
,liftM
, orjoin
. Also, when you useliftM
orfmap
to lift an ordinary function into a monadic function, you typically wind up with a similarly generic signature, and this is quite convenient for integrating pure functions with monadic code.In order to use the structure that a particular monad offers, you inevitably need to use some knowledge about the specific monad you're in to build new and interesting computations in that monad. Consider the state monad,
(s -> (a, s))
. Without knowing that type, we can't writeget = \s -> (s, s)
, but without being able to access the state, there's not much point to being in the monad.满足我能想象的要求的最简单的函数类型是这样的:
可以通过以下方式之一实现它:
这是一个非常简单的更改 -
enigma2
只是丢弃形状并将其替换为琐碎的一件事。另一种通用更改是将两个形状组合在一起:foo
的结果可以具有与a
和b
不同的形状。第三个明显的形状变化(需要 monad 的全部功能)是 a
join x
的形状通常与x
本身不同。结合这些原始的形状变化,我们可以得出一些不平凡的东西,例如
sequence
、foldM
等。The simplest type of a function satisfying the requirement I can imagine is this:
One can implement it in one of the following ways:
This was a very simple change --
enigma2
just discards the shape and replaces it with the trivial one. Another kind of generic change is combining two shapes together:The result of
foo
can have shape different from botha
andb
.A third obvious change of shape, requiring the full power of the monad, is a
The shape of
join x
is usually not the same as ofx
itself.Combining those primitive changes of shape, one can derive non-trivial things like
sequence
,foldM
and alike.吗
适合您的需求 ?例如,
Does
suit your needs? For example,
这不是完整的答案,但关于您的问题,我有一些话要说,但这些话并不适合评论。
首先,
Monad
和Functor
是类型类;他们对类型进行分类。因此,说“单子可以改变形状;函子不能”是很奇怪的。我相信你试图谈论的是一个“Monadic value”或者可能是一个“monadic action”:一个类型为m a
的值,用于某种Monad m
<代码>* -> * 和一些其他类型的*
。我不完全确定如何称呼 Functor f :: f a,我想我会称其为“函子中的值”,尽管这不是对的最佳描述IO String
(IO
是一个函子)。其次,请注意,所有 Monad 都必然是函子 (
fmap = liftM
),所以我想说您观察到的差异在于fmap
和>>= 之间
,甚至在f
和g
之间,而不是在Monad
和Functor
之间。This isn't a full answer, but I have a few things to say about your question that don't really fit into a comment.
Firstly,
Monad
andFunctor
are typeclasses; they classify types. So it is odd to say that "a monad can change shape; a functor cannot." I believe what you are trying to talk about is a "Monadic value" or perhaps a "monadic action": a value whose type ism a
for someMonad m
of kind* -> *
and some other type of kind*
. I'm not entirely sure what to callFunctor f :: f a
, I suppose I'd call it a "value in a functor", though that's not the best description of, say,IO String
(IO
is a functor).Secondly, note that all Monads are necessarily Functors (
fmap = liftM
), so I'd say the difference you observe is betweenfmap
and>>=
, or even betweenf
andg
, rather than betweenMonad
andFunctor
.