一个单子中是否可以有多个flatMap方法?

6
有必要在Monad中定义多个flatMap方法(或者在Haskell中使用>>= / bind)吗?实际上我只用到了非常少的几个monads(OptionTryEither投影),它们只定义了一个flatMap方法。
例如,如果在Option上定义一个接受产生Try的函数的flatMap方法,这样Option[Try[User]]就可以被展平为Option[User]。这种情况下丢失异常不是问题的话,是否有意义呢?
还是说一个monad应该只定义一个flatMap方法,接受产生相同类型monad的函数?我猜这种情况下Either投影将不再是monads?它们是吗?

@om-nom-nom,List 是一个 Monad 吗?顺便说一下,我找不到 List[Option[_]] 的工作原理,因为 Option 不是 GenTraversableOnce。 - Sebastien Lorber
“Either”是一个覆盖其类型变量的单子。这是你在询问的吗? - Gabriella Gonzalez
4个回答

5

我曾经认真思考过这个问题。事实证明,这样的结构(除了失去所有单子能力)并不是很有趣,因为只需要提供从内部容器到外部容器的转换即可:

joinWith :: (Functor m, Monad m) => (n a -> m a) -> m (n a) -> m a
joinWith i = join . (fmap i)

bindWith :: (Functor m, Monad m) => (n a -> m a) -> m a -> (a -> n a) -> m a
bindWith i x f = joinWith i $ fmap f x

*Main>  let maybeToList = (\x -> case x of Nothing -> []; (Just y) -> [y])
*Main>  bindWith maybeToList [1..9] (\x -> if even x then Just x else Nothing)
[2,4,6,8]

2
这是一个好答案。我的理解是,它也很符合理论:n a -> m a 函数表示 nm 函子之间的自然变换。假设它尊重单子操作,它还表示 nm 之间的单子同态。 - Tikhon Jelvis
抱歉,尽管您的回答很聪明,但它并没有回答问题,“一个单子中有多个flatMap方法?”当然,我们可以找到一种方法将两个不同的单子融合成一个选择的单子,但这样做是否会导致一个单子呢?我不明白您的回答如何帮助他理解为什么这不是一个单子。 - zurgl
这个答案在我看来几乎完美。我想指出的另一件事是,如果你有一个遵守单子定律的函数 m a -> (a -> n b) -> n b,你可以通过将其作为第二个参数传递给 return 来构造一个单子态射 m a -> n b。同样,像 m a -> (a -> n b) -> m b 这样的函数可以通过稍微更多的工作转化为单子态射 n a -> m b - Philip JF
@zurgl 为什么 bindWith 没有提供所要求的多个 flatMap 方法呢?对于每个兼容的内部类型,您会获得一个像 OP 所想的那样运作的函数(如果我正确解释了问题的话)。尤其是,bind == bindWith id。它不是一个单子,只是因为它不遵守单子的定义。甚至内部类型都不需要是单子。 - phipsgabler
好的,你说得对。我唯一可能不同意你的地方就是关于我(或你)如何解释OP提出的问题。那么这就是一个无用的辩论,只有OP才能澄清。无论如何,就像你所说的,你的回答真的很有趣 =) - zurgl
我必须承认,我的解释可能会受到我之前在思考类似问题时发现的偏见影响。当我最终发现事情就像我上面写的那样简单时,我甚至想发布一个等价于这个问题的问题。 - phipsgabler

1
作为我所知,对于特定的数据类型,只能有一个bind定义。这没有意义。
在Haskell中,Monad是以下类型类:
instance Monad m where  
    return :: a -> m a
    bind   :: m a -> (a -> m b) -> m b

具体而言,对于列表单子,我们有:
instance Monad [] where
    return :: a -> [] a
    (>>=)   :: [] a -> (a -> [] b) -> [] b

现在让我们考虑一个单子函数 as。
actOnList :: a -> [] b 
 ....

一个使用案例来说明:
$ [1,2,3] >>= actOnList

在函数actOnList中,我们看到列表是由另一种类型(这里是[])约束的多态类型。然后当我们谈论列表单子的绑定操作符时,我们谈论的是由[] a -> (a -> [] b) -> [] b定义的绑定操作符。
你想要实现一个像[] Maybe a -> (a -> [] b) -> [] b这样的绑定操作符,这不是第一个的专门版本,而是另一个函数,就它的类型签名而言,我真的怀疑它能否成为任何单子的bind操作符,因为你没有返回你所使用的内容。你肯定使用一个函数从一个单子转移到另一个单子,但这个函数绝对不是列表的bind操作符的另一个版本。
这就是为什么我说过,对于特定的数据类型,据我所知,你只能有一个bind定义。

1
在您的Option[Try[ ]]示例中,flatMap(>>=)无法通过类型检查。使用伪Haskell符号表示。
type OptionTry x = Option (Try x)

instance Monad OptionTry where
  (>>=) :: OptionTry a -> (a -> OptionTry b) -> OptionTry b
  ...

我们需要bind/flatMap返回一个与输入值相同上下文包装的值。
通过查看Monad的等效return/join实现,我们也可以看到这一点。对于OptionTryjoin具有专门的类型。
instance Monad OptionTry where
  join :: OptionTry (OptionTry a) -> OptionTry a
  ...

稍微眯一下眼睛就可以看清楚,flatMap的“平坦”部分是join(或者如果是列表,则是由其名称派生的concat)。
现在,单个数据类型可能具有多个不同的bind。在数学上,Monad实际上是数据类型(或者真正的说,Monad由值组成的集合)连同特定的bindreturn操作。不同的操作会导致不同的(数学上的)Monads。

1

这要看"make sense"的意思是什么。

如果你是指是否符合单子定律,那么我不太清楚这个问题是否完全有意义。我必须看到具体的提议才能告诉你。如果你按照我想象中的方式做,你可能最终会在某些角落情况下违反组合。

如果你是指它是否有用,那么当然,你总是可以找到这种东西有用的案例。问题在于,如果你开始违反单子定律,你就为那些不谨慎的函数式(范畴论)推理者留下了陷阱。最好让类似单子的事情实际上成为单子(只有一个,尽管你可以提供一种显式的方式来切换,比如Either--但你说得对,按照现有的定义,LeftProjectionRightProjection严格来讲并不是单子)。或者编写非常清晰的文档,解释它不是它看起来那样。否则,有人会愉快地假设法则成立,并*splat*。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接