与Functor不同,Monad可以改变形状?

时间:2011-12-09 13:37:56

标签: haskell monads

我总是喜欢以下关于monad相对于仿函数的力量的直观解释: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的类型签名。似乎不可能定义一个改变输入形状的函数,使用通用的monadic类型,例如:

h :: (Monad m, Num a) => a -> m a

MonadPlus或MonadZero类型具有相关的零元素,而不是[],但现在我们拥有的不仅仅是monad。

我说错了吗?如果是这样,有没有办法向Haskell的新人表达这种微妙之处。我想让我心爱的“monad可以改变形状”这句话,只需要更加诚实;如果需要的话。

8 个答案:

答案 0 :(得分:16)

  

我总是喜欢以下关于monad相对于仿函数的力量的直观解释:monad可以改变形状;算子不能。

顺便说一下,你在这里错过了一点微妙。为了术语,我将Haskell意义上的Functor分为三部分:参数组件由类型参数确定并由fmap操作,不变的部分如元组构造函数在State中,“形状”与其他任何东西一样,例如构造函数之间的选择(例如,NothingJust)或涉及其他类型参数的部分(例如{ {1}})。

当然,仅Reader仅限于在参数部分上映射函数。

Functor可以根据参数部分的值创建新的“形状”,这不仅可以改变形状。复制列表中的每个元素或删除前五个元素都会改变形状,但过滤列表需要检查元素。

这基本上是Monad如何适合它们 - 它允许您独立地组合两个Applicative的形状和参数值,而不会让后者影响前者。

  

我说错了吗?如果是这样,有没有办法向Haskell的新人表达这种微妙之处。我想让我心爱的“monad可以改变形状”这句话,只需要更加诚实;如果需要的话。

也许你在这里寻找的微妙之处在于你并没有真正“改变”任何东西。 Functors中没有任何内容可以让您明确地弄乱形状。它允许你做的是根据每个参数值创建新的形状,并将这些新形状重新组合成一个新的复合形状。

因此,您将始终受到创建形状的可用方法的限制。使用完全通用的Monad,您只需Monad,根据定义,可以创建任何必要的形状,以便return是标识函数。 (>>= return)的定义告诉你在给定某些函数的情况下你可以做什么;它没有为您提供这些功能。

答案 1 :(得分:8)

Monad的操作可以“改变值的形状”,以使>>=函数替换“树”中的叶节点,该树节点是原始值,其中新的子结构派生自节点的值(对于“树”的适当的一般概念 - 在列表的情况下,“树”是关联的)。

在列表示例中,正在发生的事情是每个数字(叶子)都被新列表替换,该列表会在g应用于该数字时生成。如果你知道你在寻找什么,那么原始列表的整体结构仍然可以看到; g的结果仍然按顺序存在,它们刚刚被粉碎在一起,所以除非你已经知道,否则你无法判断一个人的结局和下一个结束的时间。

更具启发性的观点可能是考虑fmapjoin而不是>>=。与return一起,无论哪种方式都给出了monad的等价定义。但是,在fmap / join视图中,这里发生的事情更加明确。继续列表示例,首先g fmap位于[[1],[],[3]]的列表上。然后该列表为join,列表仅为concat

答案 2 :(得分:7)

仅仅因为monad模式包含一些允许形状变化的特定实例并不意味着每个实例都可以进行形状变化。例如,只有一个"形状"可在Identity monad中找到:

newtype Identity a = Identity a
instance Monad Identity where
    return = Identity
    Identity a >>= f = f a

事实上,我不清楚很多单子都有意义的形状:例如,形状在State中意味着什么,{{1} },ReaderWriterSTSTM monad?

答案 3 :(得分:4)

monad的关键组合子是(>>=)。知道它组成两个monadic值并读取它的类型签名,monad的力量变得更加明显:

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

未来行动完全取决于第一个行动的结果,因为它是其结果的函数。然而,这种能力是有代价的:Haskell中的函数完全不透明,所以没有实际运行它就无法获得有关组合操作的任何信息。作为旁注,这是箭头进入的地方。

答案 4 :(得分:3)

具有类似h的签名的函数除了对其参数执行某些算术之外,确实无法做很多有趣的事情。所以,你有正确的直觉。

但是,查看常用的functions with similar signatures库可能会有所帮助。您会发现,正如您所期望的那样,最通用的操作可以执行returnliftMjoin等通用monad操作。此外,当您使用liftMfmap将普通函数提升为monadic函数时,通常会使用类似的通用签名,这对于将纯函数与monadic代码集成非常方便。

为了使用特定monad提供的结构,你不可避免地需要使用一些关于你所在的特定monad的知识来在该monad中构建新的和有趣的计算。考虑州monad,(s -> (a, s))。在不知道这种类型的情况下,我们不能写get = \s -> (s, s),但是如果没有能够访问该状态,那么在monad中没有太多意义。

答案 5 :(得分:1)

满足我能想象的要求的最简单的函数类型是:

enigma :: Monad m => m () -> m ()

可以通过以下方式之一实现它:

enigma1 m = m -- not changing the shape

enigma2 _ = return () -- changing the shape

这是一个非常简单的变化 - enigma2只是丢弃形状并用琐碎的形状取而代之。另一种通用变化是将两种形状组合在一起:

foo :: Monad m => m () -> m () -> m ()
foo a b = a >> b

foo的结果可能与ab的形状不同。

形状的第三个明显变化,需要monad的全部力量,是

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

join x的形状通常与x本身的形状不同。

结合形状的这些原始变化,可以得出sequencefoldM等类似的非平凡事物。

答案 6 :(得分:0)

确实

h :: (Monad m, Num a) => a -> m a
h 0 = fail "Failed."
h a = return a

适合您的需求?例如,

> [0,1,2,3] >>= h
[1,2,3]

答案 7 :(得分:0)

这不是一个完整的答案,但我对你的问题有一些说法,而这些问题并不适合评论。

首先,MonadFunctor是类型类;他们对类型进行了分类。所以很奇怪," monad可以改变形状;算子不能。"我相信你想要谈论的是一个" Monadic价值"或者是一个" monadic动作":类型为m a的{​​{1}}种类Monad m以及其他类型* -> *的值。我并不完全确定要拨打*的内容,我想我称之为“仿函数”中的价值,尽管这不是最佳描述比方说,Functor f :: f aIO String是一个仿函数)。

其次,请注意所有Monad都必须是Functors(IO),所以我说你观察到的区别在fmap = liftMfmap之间,甚至在{{{}之间1}}和>>=,而不是fg之间。