【问题标题】:Why does haskell's bind function take a function from non-monadic to monadic为什么haskell的bind函数要把一个函数从非monadic变成monadic
【发布时间】:2017-05-22 15:52:52
【问题描述】:

我对 Haskell 中绑定函数(>>=) 的定义有一些疑问。

因为 Haskell 是纯语言,所以我们可以使用 Monad 来处理有副作用的操作。我认为这个策略有点像把所有可能对另一个世界造成副作用的行为,我们可以通过do>>=,从我们的“纯”haskell 世界控制它们。

所以当我查看>>=函数的定义时

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

它需要一个(a -> m b) 函数,因此前一个操作的结果m a 可以“解包”到>>= 中的非单子a。然后函数(a -> m b)a 作为其输入并返回另一个单子m b 作为其结果。通过绑定函数,我可以在不给纯 Haskell 代码带来任何副作用的情况下对 monadic 进行操作。

我的问题是为什么我们使用(a -> m b) 函数?在我看来,m a -> m b 函数也可以做到这一点。是有什么原因,还是只是因为它是这样设计的?

编辑

从 cmets 我了解到很难从 m a 中提取 a。但是,我认为我可以将一元 m a 视为具有副作用的 a

是否可以假设函数m a -> m ba -> b 行为相似,所以我们可以像定义a -> b 一样定义m a -> m b

【问题讨论】:

  • m a -> m b 函数无法做到这一点,因为没有通用方法可以从 m a 中提取 aIO 是最常见的示例。
  • 如果是m a -> m b,那么我们总是可以让x >>= f = f x 为任何m。这不会很有趣:它不会实现>>= 的“临时拆箱”,因为f 不会传递“拆箱值”。
  • OK 尝试在不使用>>= 的情况下定义IO Int -> IO String 类型的函数。
  • @calvin - MonadApplicative 的额外能力是能够检查内部值a 以选择结果值m b。如果(>>=) 采用函数m a -> m b,那么您需要一个函数m a -> a 才能检查a 来决定返回哪个值m b。但是 monad 不支持这样的功能。由于 monad 是函子,它们已经支持将 a -> b 提升到 m a -> m bfmap
  • @Lee 如果(>>=) 采用函数m a -> m b,那么它将被称为(.)

标签: haskell monads


【解决方案1】:

edit2:好的,这就是我从一开始就应该说的:

Monad 是 EDSL,

E嵌入式 领域特定语言一样。 嵌入意味着该语言的语句是 我们的语言 Haskell 中的普通值。

让我们尝试使用一种 IO 语言。假设我们有print1 :: IO () 原语,描述了在提示符处打印整数1 的动作。想象一下我们还有print2 :: IO ()。两者都是普通的 Haskell 值。在 Haskell 中,我们谈到了这些动作。这种IO 语言仍然需要在“运行”时由运行时系统的某些部分稍后 解释/执行。拥有两种语言,我们就有了两个世界,两条时间线。

我们可以写do { print1 ; print2 } 来描述复合动作。但是我们不能创建一个新的原语来在提示符下打印3,因为它在我们的 Haskell 世界之外。我们这里有一个 EDSL,但显然不是一个非常强大的。我们这里必须有无限的原语供应; 不是一个成功的提议。它甚至不是 Functor,因为我们无法修改这些值。

现在,如果我们可以呢?然后我们就可以告诉do { print1 ; print2 ; fmap (1+) print2 },也可以打印出3。现在它是一个函子。更强大,但仍然不够灵活。

我们在构造这些动作描述符(如print1)的原语中获得了灵活性。例如print :: Show a => a -> IO a。我们现在可以讨论更多功能的操作,例如do { print 42; getLine ; putStrLn ("Hello, " ++ "... you!") }

但现在我们看到需要参考之前操作的“结果”。我们希望能够写do { print 42; s <- getLine ; putStrLn ("Hello, " ++ s ++ "!") }。我们希望(在 Haskell 世界中)基于先前 IO 动作的结果(在 Haskell 世界中)创建新的动作描述(在 IO 世界中描述动作的 Haskell 值)这些IO-动作产生,它们运行,IO - 语言被解释(它描述的动作在IO-world 中执行)。

这意味着能够从 Haskell 值创建那些 IO 语言语句,例如 print :: a -> IO a。这正是您要询问的类型,并且 正是此 EDSL 成为 Monad 的原因。


假设我们有一个 IO 原语 (a_primitive :: IO Int -> IO ()),它按原样打印任何正整数,并在打印任何非正整数之前在单独的行上打印 "---"。然后我们可以按照您的建议写a_primitive (return 1)

但是IO是关闭的;它是不纯的;我们不能在 Haskell 中编写新的 IO 原语,而且不可能为每个可能出现在我们脑海中的新想法定义一个原语。所以我们改写(\x -> if x > 0 then print x else do { putStrln "---"; print x }),那个lambda表达式的类型是Int -> IO ()(或多或少)。

如果上述 lambda 表达式中的参数 xIO Int 类型,则表达式 x > 0 将输入错误。如果不使用标准的>>= 运算符(或等效运算符),就无法从IO a 中取出a

另见:

还有,这个quote

“有人在某个时候注意到了,”哦,为了得到不纯的效果 从纯代码我需要做元编程,这意味着我的一个 types 需要是 '计算 X 的程序'。我想拿一个 '计算 X 的程序' 和一个接受 X 和 产生下一个程序,一个'计算Y'的程序',并且不知何故 将它们粘合到一个'程序中,该程序计算一个 Y'"(这是 bind 操作)。 IO monad 诞生了。”


edit:这些是泛化函数应用的四种类型:

( $ ) ::                     (a ->   b) ->   a ->   b     -- plain
(<$>) :: Functor f     =>    (a ->   b) -> f a -> f b     -- functorial
(<*>) :: Applicative f =>  f (a ->   b) -> f a -> f b     -- applicative
(=<<) :: Monad f       =>    (a -> f b) -> f a -> f b     -- monadic

这里是对应的类型推导规则,为了清楚起见,颠倒了参数顺序,

 a                f a                f  a                f a
 a -> b             a -> b           f (a -> b)            a -> f b
 ------           --------           ----------          ----------
      b           f      b           f       b           f        b

 no `f`s          one `f`            two `f`s,           two `f`s: 
                                     both known          one known,
                                                         one constructed

为什么?他们只是。你的问题是真的,为什么我们需要 Monads?为什么 Functor 或 Applicative Functor 还不够? 这肯定已经被问过很多次了(例如,上面列表中的第二个链接)。一方面,正如我在上面试图展示的那样,monad 让我们在 Haskell 中编写新的计算。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-05
    • 2013-06-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多