为什么 haskell 的 bind 函数将一个函数从非 monadic 转换为 monadic
Why does haskell's bind function take a function from non-monadic to monadic
我对Haskell中绑定函数(>>=)
的定义有一些疑问。
因为Haskell是一门纯语言,所以我们可以使用Monad来处理有副作用的操作。我认为这个策略有点像把所有的动作都可能对另一个世界产生副作用,我们可以从我们的 "pure" haskell 世界通过 do
或 >>=
控制它们。
所以当我查看 >>=
函数的定义时
(>>=) :: Monad m => m a -> (a -> m b) -> m b
它需要一个 (a -> m b)
函数,因此前一个动作的结果 m a
可以 "unpack" 到 >>=
中的非单子 a
。然后函数 (a -> m b)
将 a
作为输入,将 return 另一个单子 m b
作为结果。通过绑定函数,我可以对 monadic 进行操作,而不会给纯 haskell 代码带来任何副作用。
我的问题是为什么我们使用 (a -> m b)
函数?在我看来,一个 m a -> m b
函数也可以做到这一点。是不是有什么原因,还是因为它是这样设计的?
编辑
从评论中我了解到很难从 m a
中提取 a
。但是,我认为我可以将 monadic m a
视为具有副作用的 a
。
是否可以假设函数 m a -> m b
的行为与 a -> b
相似,因此我们可以像定义 a -> b
一样定义 m a -> m b
?
edit2: 好的,这是我从一开始就应该说的:
Monad 是 EDSL,
E 与 嵌入式 领域特定语言一样。 Embedded 表示该语言的语句在我们的语言中是普通值,Haskell.
让我们尝试使用 IO 语言。假设我们有 print1 :: IO ()
原语,描述了在提示符下打印整数 1
的操作。想象一下我们也有 print2 :: IO ()
。两者都是普通的 Haskell 值。在 Haskell 中,我们谈到了这些操作。 IO-语言仍然需要由运行-时间系统稍后的某些部分解释/执行,在"run"-时间。我们有两种种语言,有两个世界,两条时间线。
我们可以写do { print1 ; print2 }
来描述复合动作。但是我们不能在提示符下创建用于打印 3
的新基元,因为它在 outside 我们的 pure Haskell 世界。我们这里有一个 EDSL,但显然不是一个非常强大的。我们这里必须有无限供应的原语; 不是 一个成功的命题。而且它甚至不是 Functor,因为我们无法修改这些值。
现在,如果我们可以呢?然后我们就可以告诉 do { print1 ; print2 ; fmap (1+) print2 }
,也打印出 3
。现在它是一个函子。更强大,还是不够灵活。
我们可以灵活地使用原语来构造这些动作描述符(比如print1
)。它是例如print :: Show a => a -> IO a
。我们现在可以讨论更通用的操作,例如 do { print 42; getLine ; putStrLn ("Hello, " ++ "... you!") }
。
但是现在我们看到需要参考之前的"results"。我们希望能够写成 do { print 42; s <- getLine ; putStrLn ("Hello, " ++ s ++ "!") }
。我们想创建(在 Haskell 世界中)基于结果(在 Haskell 中)的新动作描述(Haskell 值描述在 IO-世界中的动作)世界)之前的IO-动作,这些IO-动作将产生,当它们是运行,当 IO-语言被解释(它描述的动作是在IO-世界).
这意味着能够从 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 表达式中的参数 x
是 IO Int
类型,则表达式 x > 0
将被错误键入。如果不使用标准的 >>=
运算符(或其等价物),就无法从 IO a
中得到 a
。
另见:
- Difference between Monad and Applicative in Haskell
- Monads with Join() instead of Bind()
- How to handle side effect with Applicative?
而且,这个 quote:
"Someone at some point noticed, "oh, in order to get impure effects
from pure code I need to do metaprogramming, which means one of my
types needs to be 'programs which compute an X'. I want to take a
'program that computes an X' and a function which takes an X and
produces the next program, a 'program that computes a Y', and somehow
glue them together into a 'program which computes a Y' " (which is the
bind
operation). The IO monad was born."
编辑:这些是广义函数应用的四种类型:
( $ ) :: (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
为什么?他们只是。您的问题确实是,为什么我们需要 Monad?为什么 Functor 或 Applicative Functor 还不够? 这肯定已经被问过并回答过很多次了(例如,上面列表中的第二个 link)。首先,正如我在上面试图展示的那样,monad 让我们在 Haskell.
中编写新的计算代码
我对Haskell中绑定函数(>>=)
的定义有一些疑问。
因为Haskell是一门纯语言,所以我们可以使用Monad来处理有副作用的操作。我认为这个策略有点像把所有的动作都可能对另一个世界产生副作用,我们可以从我们的 "pure" haskell 世界通过 do
或 >>=
控制它们。
所以当我查看 >>=
函数的定义时
(>>=) :: Monad m => m a -> (a -> m b) -> m b
它需要一个 (a -> m b)
函数,因此前一个动作的结果 m a
可以 "unpack" 到 >>=
中的非单子 a
。然后函数 (a -> m b)
将 a
作为输入,将 return 另一个单子 m b
作为结果。通过绑定函数,我可以对 monadic 进行操作,而不会给纯 haskell 代码带来任何副作用。
我的问题是为什么我们使用 (a -> m b)
函数?在我看来,一个 m a -> m b
函数也可以做到这一点。是不是有什么原因,还是因为它是这样设计的?
编辑
从评论中我了解到很难从 m a
中提取 a
。但是,我认为我可以将 monadic m a
视为具有副作用的 a
。
是否可以假设函数 m a -> m b
的行为与 a -> b
相似,因此我们可以像定义 a -> b
一样定义 m a -> m b
?
edit2: 好的,这是我从一开始就应该说的:
Monad 是 EDSL,
E 与 嵌入式 领域特定语言一样。 Embedded 表示该语言的语句在我们的语言中是普通值,Haskell.
让我们尝试使用 IO 语言。假设我们有 print1 :: IO ()
原语,描述了在提示符下打印整数 1
的操作。想象一下我们也有 print2 :: IO ()
。两者都是普通的 Haskell 值。在 Haskell 中,我们谈到了这些操作。 IO-语言仍然需要由运行-时间系统稍后的某些部分解释/执行,在"run"-时间。我们有两种种语言,有两个世界,两条时间线。
我们可以写do { print1 ; print2 }
来描述复合动作。但是我们不能在提示符下创建用于打印 3
的新基元,因为它在 outside 我们的 pure Haskell 世界。我们这里有一个 EDSL,但显然不是一个非常强大的。我们这里必须有无限供应的原语; 不是 一个成功的命题。而且它甚至不是 Functor,因为我们无法修改这些值。
现在,如果我们可以呢?然后我们就可以告诉 do { print1 ; print2 ; fmap (1+) print2 }
,也打印出 3
。现在它是一个函子。更强大,还是不够灵活。
我们可以灵活地使用原语来构造这些动作描述符(比如print1
)。它是例如print :: Show a => a -> IO a
。我们现在可以讨论更通用的操作,例如 do { print 42; getLine ; putStrLn ("Hello, " ++ "... you!") }
。
但是现在我们看到需要参考之前的"results"。我们希望能够写成 do { print 42; s <- getLine ; putStrLn ("Hello, " ++ s ++ "!") }
。我们想创建(在 Haskell 世界中)基于结果(在 Haskell 中)的新动作描述(Haskell 值描述在 IO-世界中的动作)世界)之前的IO-动作,这些IO-动作将产生,当它们是运行,当 IO-语言被解释(它描述的动作是在IO-世界).
这意味着能够从 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 表达式中的参数 x
是 IO Int
类型,则表达式 x > 0
将被错误键入。如果不使用标准的 >>=
运算符(或其等价物),就无法从 IO a
中得到 a
。
另见:
- Difference between Monad and Applicative in Haskell
- Monads with Join() instead of Bind()
- How to handle side effect with Applicative?
而且,这个 quote:
"Someone at some point noticed, "oh, in order to get impure effects from pure code I need to do metaprogramming, which means one of my types needs to be 'programs which compute an X'. I want to take a 'program that computes an X' and a function which takes an X and produces the next program, a 'program that computes a Y', and somehow glue them together into a 'program which computes a Y' " (which is the
bind
operation). The IO monad was born."
编辑:这些是广义函数应用的四种类型:
( $ ) :: (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
为什么?他们只是。您的问题确实是,为什么我们需要 Monad?为什么 Functor 或 Applicative Functor 还不够? 这肯定已经被问过并回答过很多次了(例如,上面列表中的第二个 link)。首先,正如我在上面试图展示的那样,monad 让我们在 Haskell.
中编写新的计算代码