10

前几天我在谈论函数式编程——尤其是 Haskell 和一些 Java/Scala 人,他们问我什么是 Monad,它们在哪里是必要的。

好吧,定义和示例并不那么难 - Maybe MonadIO Monad等等State Monad,所以每个人至少部分地同意我说 Monads 是一件好事。

但是在哪里需要 Monad -Maybe可以通过-1设置 ofInteger""设置中的魔法值来避免String。我写了一个没有StateMonad 的游戏,这一点都不好,但初学者会这样做。

所以我的问题是:Monads 在哪里是必要的?——而且根本无法避免。(并且没有混淆 - 我喜欢 Monads 并使用它们,我只是想知道)。

编辑

我想我必须澄清一下,我不认为使用“魔术值”是一个好的解决方案,但是很多程序员都在使用它们,尤其是在C等低级语言中或在 SHell 脚本中,通常通过返回来暗示错误-1

我已经很清楚不使用 monads 不是一个好主意。抽象通常很有帮助,但也很复杂,因此很多人都在为 monad 的概念而苦恼。

我的问题的核心是,例如IO,是否有可能在没有 monad 的情况下仍然是纯粹的和功能性的。我知道将一个已知的好的解决方案搁置一旁,以及用打火石和火种而不是使用打火机生火,这将是乏味和痛苦的。

@Antal SZ 提到的文章很棒,你可以发明 monads,我略读了一遍,当我有更多时间时一定会阅读它。更具启发性的答案隐藏在@Antal SZ 引用的博客文章的评论中,我记得在 monads 之前的时间,这是我问这个问题时正在寻找的东西。

4

3 回答 3

12

我认为你永远不需要单子。它们只是当您使用某些类型的功能时自然出现的一种模式。我见过的对这种观点的最佳解释是 Dan Piponi (sigfpe) 的优秀博客文章“你本可以发明单子!(也许你已经有了。)”,这个答案的灵感来自。

你说你写了一个没有使用状态单子的游戏。它看起来像什么?很有可能您最终使用的函数类型类似于openChest :: Player -> Location -> (Item,Player)(打开一个箱子,可能用陷阱伤害玩家,并返回找到的物品)。一旦你需要组合它们,你可以手动(let (item,player') = openChest player loc ; (x,player'') = func2 player' y in ...)或重新实现 state monad 的>>=操作符。

或者假设我们正在使用带有哈希映射/关联数组的语言,而我们没有使用 monad。我们需要查找一些项目并使用它们;也许我们正试图在两个用户之间发送消息。

send username1 username2 = {
    user1 = users[username1]
    user2 = users[username2]
    sendMessage user1 user2 messageBody
}

但是等等,这行不通;username1并且username2可能会丢失,在这种情况下,它们将是nilor-1或其他东西而不是所需的值。或者也许在关联数组中查找一个键会返回一个类型的值Maybe a,所以这甚至会是一个类型错误。相反,我们必须写一些类似的东西

send username1 username2 = {
    user1 = users[username1]
    if (user1 == nil) return
    user2 = users[username2]
    if (user2 == nil) return
    sendMessage user1 user2 messageBody
}

或者,使用Maybe

send username1 username2 =
    case users[username1] of
      Just user1 -> case users[username2] of
                      Just user2 -> Just $ sendMessage user1 user2 messageBody
                      Nothing    -> Nothing
      Nothing    -> Nothing

哎呀!这是混乱和过度嵌套的。所以我们定义了某种函数,它结合了可能失败的动作。也许像

(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
f >>= Just x  = f x
f >>= Nothing = Nothing

所以你可以写

send username1 username2 =
  users[username1] >>= $ \user1 ->
  users[username2] >>= $ \user2 ->
  Just (sendMessage user1 user2 messageBody)

如果你真的不想使用Maybe,那么你可以实现

f >>= x = if x == nil then nil else f x

同样的原则也适用。

不过,真的,我建议阅读“你本可以发明单子!” 这是我对单子的直觉,并更好,更详细地解释它的地方。当处理某些类型时,Monads 会自然而然地出现。有时你把这个结构明确化,有时你没有,但仅仅因为你克制它并不意味着它不存在。从不需要使用特定结构的意义上说,您永远不需要使用 monad ,但通常这是很自然的事情。并且认识到这里的通用模式,就像在许多其他事情中一样,可以让你编写一些很好的通用代码。

(另外,正如我使用的第二个示例所示,请注意,您已经通过替换Maybe魔法值将婴儿与洗澡水一起扔了出去。仅仅因为Maybe是单子并不意味着您必须像使用单子一样使用它;列表也是单子,函数(形式r ->)也是如此,但你不建议摆脱它们!:-))

于 2012-08-23T18:27:11.653 回答
4

我可以接受“ X在哪里/在哪里是必要和不可避免的?”这句话。其中X是计算中的任何东西;重点是什么?

相反,我认为问“ X提供什么/提供了什么价值?”更有价值。

最基本的答案是,计算中的大多数X都提供了一个有用的抽象,它使得将代码放在一起变得更容易、更不乏味且更不容易出错。

好的,但你不需要抽象,对吧?我的意思是,我可以手动输入一个做同样事情的小代码,对吧?是的,当然,这只是一堆 0 和 1,所以让我们看看谁能更快地编写 XML 解析器,我使用 Java/Haskell/C 或者你使用图灵机。


Re monads:由于 monads 通常处理有效的计算,因此这种抽象在组合有效函数时最有用。

我对你的“魔法值也许单子”有异议。这种方法为程序员提供了一个非常不同的抽象,并且比实际 Maybe的monad 更不安全、更乏味且更容易出错。此外,阅读这样的代码,程序员的意图会不太清楚。换句话说,它忽略了真正的 monad 的全部意义,即提供抽象。

我还想指出,monad 不是 Haskell 的基础:

  1. do-notation 只是语法糖,可以完全被替换,>>=并且>>不会失去任何表现力

  2. 它们(以及它们的组合子,例如join, >>=,mapM等)可以用Haskell编写

  3. 它们可以用任何支持高阶函数的语言编写,甚至可以使用对象用 Java 编写。因此,如果您必须使用没有 monad 的 Lisp,您可以自己在该 Lisp 中实现它们,而不会有太多麻烦

于 2012-08-23T18:29:34.320 回答
0

因为 monad 类型返回相同类型的答案,所以该 monad 类型的实现可以强制和保留语义。然后,在您的代码中,您可以使用该类型链接操作并让它执行其规则,而不管它包含的类型如何。

例如,Java 8 中的 Optional 类强制规定所包含的值要么存在且非空,要么不存在。只要您使用 Optional 类,无论是否使用 flatMap,您都会将该规则包装在包含的数据类型周围。没有人可以作弊或忘记并添加 value=null 和 present=true。

因此,在代码之外声明 -1 将是一个标记值并表示某某是可以的,但您仍然依赖自己和代码中工作的其他人来尊重该语义。如果一个新人加入并开始使用 -1000000 来表示同样的事情,那么语义需要在代码之外(可能使用铅管?)而不是通过代码机制来执行。

因此,不必在您的程序中一致地应用某些规则,您可以信任 monad 来保留该规则(或其他语义)——而不是任意类型。

通过这种方式,您可以通过在类型周围包装语义来扩展类型的功能,而不是在代码库中的每个类型中添加“isPresent”。

众多单子实用程序类型的存在表明,这种用语义包装类型的机制是一个非常有用的技巧。如果您有自己想要添加的语义,您可以通过使用 monad 模式编写自己的类来实现,然后将字符串、浮点数或整数或其他任何内容注入其中。

但简短的回答是,monad 是将常见类型包装在流畅或可链接容器中以添加规则和用法的好方法,而不必对底层类型的实现大惊小怪。

于 2015-11-18T23:23:51.340 回答