Applicative Functor 评估我不清楚
Applicative functor evaluation is not clear to me
我目前正在阅读 Haskell 学习你的好书!并且在对某个代码块的评估进行解释时绊倒了。解释看了好几遍,开始怀疑连作者都看不懂这段代码是干什么的了
ghci> (+) <$> (+3) <*> (*100) $ 5
508
应用函子将某些上下文中的函数应用于某些上下文中的值,以获得某些上下文中的某些结果。我花了几个小时研究这个代码块,并对如何计算这个表达式提出了一些解释,其中 none 个是令人满意的。我知道 (5+3)+(5*100) 是 508,但问题在于这个表达式。有没有人对这段代码有明确的解释?
正在使用函数的应用实例。你的代码
(+) <$> (+3) <*> (*100) $ 5
被评估为
( (\a->\b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5 -- f <$> g
( (\x -> (\a->\b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5 -- \x -> f (g x)
( (\x -> (\a->\b->a+b) (x+3)) <*> (\d->d*100) ) 5
( (\x -> \b -> (x+3)+b) <*> (\d->d*100) ) 5
( (\x->\b->(x+3)+b) <*> (\d->d*100) ) 5 -- f <*> g
(\y -> ((\x->\b->(x+3)+b) y) ((\d->d*100) y)) 5 -- \y -> (f y) (g y)
(\y -> (\b->(y+3)+b) (y*100)) 5
(\y -> (y+3)+(y*100)) 5
(5+3)+(5*100)
其中 <$>
是 fmap
或者只是函数组合 .
,而 <*>
是 ap
如果你知道它在 monads 上的行为。
我们先来看看fmap
and (<*>)
是如何定义一个函数的:
instance Functor ((->) r) where
fmap = (.)
instance Applicative ((->) a) where
pure = const
(<*>) f g x = f x (g x)
liftA2 q f g x = q (f x) (g x)
我们要评估的表达式是:
(+) <$> (+3) <*> (*100) $ 5
或更详细:
((+) <$> (+3)) <*> (*100) $ 5
如果我们这样计算 (<$>)
,它是 fmap
的中缀同义词,我们就会看到它等于:
(+) . (+3)
所以这意味着我们的表达式等同于:
((+) . (+3)) <*> (*100) $ 5
接下来我们可以应用顺序应用。因此,这里 f
等于 (+) . (+3)
而 g
等于 (*100)
。因此,这意味着我们构建了一个如下所示的函数:
\x -> ((+) . (+3)) x ((*100) x)
我们现在可以将其简化并重写为:
\x -> ((+) (x+3)) ((*100) x)
然后重写为:
\x -> (+) (x+3) ((*100) x)
因此,我们构建了一个如下所示的函数:
\x -> (x+3) + 100 * x
或更简单:
\x -> 101 * x + 3
如果我们再计算:
(\x -> 101*x + 3) 5
那我们当然得到:
101 * 5 + 3
因此:
505 + 3
这是预期的:
508
其他两个答案已经详细说明了这是如何计算的——但我想我可能会补充更多 "intuitive" 答案来解释如何在不进行详细计算的情况下,可以 "see" 结果必须是 508.
正如您所暗示的,每个 Applicative
(事实上,甚至每个 Functor
)都可以被视为一种特定类型的 "context",它包含给定类型的值。作为简单的例子:
Maybe a
是一个上下文,其中 a
类型的值可能存在,但也可能不存在(通常是由于某种原因可能失败的计算结果)
[a]
是一个上下文,可以包含零个或多个 a
类型的值,数量没有上限 - 表示特定计算的所有可能结果
IO a
是一个上下文,其中类型 a
的值作为与 "the outside world" 以某种方式交互的结果可用。 (好吧,那不是那么简单...)
并且,与此示例相关:
r -> a
是一个上下文,其中 a
类型的值可用,但其特定值尚不清楚,因为它取决于某些(尚未知的)类型 r
.
Applicative
方法在这种上下文中基于值可以很好地理解。 pure
将 "ordinary value" 嵌入到 "default context" 中,在该上下文中它的行为与 "context-free" 尽可能接近。我不会针对上面的 4 个示例中的每一个(其中大部分都非常明显),但我会注意到对于函数,pure = const
- 即 "pure value" a
由始终生成 a
的函数表示,无论源值是什么。
我不想详述如何使用 "context" 隐喻来最好地描述 <*>
,我想详述特定的表达方式:
f <$> a <*> b
其中f
是2"pure values"和a
之间的函数,b
是"values in a context"。这个表达式实际上有一个同义词作为函数:liftA2。尽管使用 liftA2
函数通常被认为不如使用 <$>
和 <*>
的 "applicative style" 惯用,但该名称强调的是 "lift" 上的一个函数"ordinary values" 到 "values in a context" 上的一个。当这样想时,我认为给定一个特定的 "context"(即一个特定的 Applicative
实例),这通常是非常直观的。
所以表达式:
(+) <$> a <*> b
对于值 a
和 b
的类型说 f Int
对于应用程序 f
,对于不同的实例表现如下 f
:
- if
f = Maybe
,那么如果 a
和 b
都是 Just
值,则结果是将基础值相加并将它们包装在 Just
。如果 a
或 b
是 Nothing
,则整个表达式是 Nothing
.
- if
f = []
(列表实例)则上面的表达式是一个列表,其中包含 a' + b'
形式的所有和,其中 a'
在 a
和 b'
在 b
.
- if
f = IO
,则上面的表达式是一个 IO 动作,它执行 a
的所有 I/O 效果,然后是 b
的效果,并导致这两个动作产生的 Int
的总和。
那么,如果 f
是函数实例,最后它会做什么呢?由于 a
和 b
都是描述如何在给定任意 (Int
) 输入的情况下获得给定 Int
的函数,因此提升 (+)
函数是很自然的在它们之上的应该是函数,在给定输入的情况下,获取 a
和 b
函数的结果,然后将结果相加。
当然,这就是它的作用 - 以及它所依据的明确路径已被其他答案非常巧妙地映射出来。但它为什么会这样工作的原因——事实上,这正是我们拥有 f <*> g = \x -> f x (g x)
实例的原因,否则它可能看起来相当随意(尽管实际上它是极少数事情之一,如果不是唯一的话) ,将进行类型检查),以便实例匹配 "values which depend on some as-yet-unknown other value, according to the given function" 的语义。总的来说,我会说这样想 "at a high level" 通常比被迫深入了解计算执行方式的低级细节要好。 (虽然我当然不想淡化也能够做到后者的重要性。)
[其实从哲学的角度来说,可能更准确的说法是定义如此,只是因为"natural"的定义进行了类型检查,纯属巧合然后该实例呈现出如此漂亮的 "meaning"。数学当然充满了这样的快乐 "coincidences" 这背后有很深的原因。]
对于任何应用程序,
a <$> b <*> c = liftA2 a b c
对于函数,
liftA2 a b c x
= a (b x) (c x) -- by definition;
= (a . b) x (c x)
= ((a <$> b) <*> c) x
因此
(+) <$> (+3) <*> (*100) $ 5
=
liftA2 (+) (+3) (*100) 5
=
(+) ((+3) 5) ((*100) 5)
=
(5+3) + (5*100)
(此答案的长版如下。)
纯数学没时间。纯Haskell没时间。用动词说话(“applicative functor applies”等)可能会造成混淆(“applies...when?...”)。
相反,(<*>)
是一个组合子,它组合了一个带有函数的“计算”(由应用函子表示)(在那种计算类型的上下文中 ) 和相同类型的“计算”,携带一个值( 在类似上下文 中),到一个组合的“计算”中,该计算执行该函数对该值的应用(在这种情况下).
“计算”是用来对比纯粹的Haskell“计算”(在 Philip Wadler 的“Calculating is better than Scheming”论文之后,它本身指的是 David Turner 的 Kent Recursive Calculator 语言,一个Haskell).
的(主要)前任 Miranda 的前任
“计算”本身可能是纯粹的,也可能不是纯粹的,这是一个正交问题。但它的主要意思是,“计算”体现了 通用函数调用协议 。除了/作为/执行函数对其参数的应用的一部分,他们可能会“做”一些事情。或者类型,
( $ ) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
(=<<) :: (a -> f b) -> f a -> f b
对于函数,上下文是应用程序(另一个一个),并且要恢复值——无论是函数还是参数——应用程序到一个普通的参数要执行。
(耐心等待,我们快到了)。
模式 a <$> b <*> c
也可以表示为 liftA2 a b c
。因此,“函数”应用函子“计算”类型由
定义
liftA2 h x y s = let x' = x s -- embellished application of h to x and y
y' = y s in -- in context of functions, or Reader
h x' y'
-- liftA2 h x y = let x' = x -- non-embellished application, or Identity
-- y' = y in
-- h x' y'
-- liftA2 h x y s = let (x',s') = x s -- embellished application of h to x and y
-- (y',s'') = y s' in -- in context of
-- (h x' y', s'') -- state-passing computations, or State
-- liftA2 h x y = let (x',w) = x -- embellished application of h to x and y
-- (y',w') = y in -- in context of
-- (h x' y', w++w') -- logging computations, or Writer
-- liftA2 h x y = [h x' y' | -- embellished application of h to x and y
-- x' <- x, -- in context of
-- y' <- y ] -- nondeterministic computations, or List
-- ( and for Monads we define `liftBind h x k =` and replace `y` with `k x'`
-- in the bodies of the above combinators; then liftA2 becomes liftBind: )
-- liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- liftBind :: (a -> b -> c) -> f a -> (a -> f b) -> f c
-- (>>=) = liftBind (\a b -> b) :: f a -> (a -> f b) -> f b
事实上,上面所有的片段都可以用 ApplicativeDo 写成 liftA2 h x y = do { x' <- x ; y' <- y ; pure (h x' y') }
或者更直观地写成
liftA2 h x y = [h x' y' | x' <- x, y' <- y]
,使用 Monad Comprehensions,因为上述所有计算类型都是 monad 以及应用函子。顺便说一句,这表明 (<*>) = liftA2 ($)
,这可能也很有启发性。
的确如此,
> :t let liftA2 h x y r = h (x r) (y r) in liftA2
:: (a -> b -> c) -> (t -> a) -> (t -> b) -> (t -> c)
> :t liftA2 -- the built-in one
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
即当我们取 f a ~ (t -> a) ~ (->) t a
时类型匹配,即 f ~ (->) t
.
因此,我们已经完成:
(+) <$> (+3) <*> (*100) $ 5
=
liftA2 (+) (+3) (*100) 5
=
(+) ((+3) 5) ((*100) 5)
=
(+) (5+3) (5*100)
=
(5+3) + (5*100)
这就是liftA2
为这种类型定义的方式,Applicative ((->) t) => ...
:
instance Applicative ((->) t) where
pure x t = x
liftA2 h x y t = h (x t) (y t)
不需要定义(<*>)
。 The source code says:
Minimal complete definition
pure, ((<*>) | liftA2)
所以现在你想问很久了,为什么是不是a <$> b <*> c
就等于liftA2 a b c
?
简短的回答是,就是这样。一个可以根据另一个来定义——即 (<*>)
可以通过 liftA2
、
来定义
g <*> x = liftA2 id g x -- i.e. (<*>) = liftA2 id = liftA2 ($)
-- (g <*> x) t = liftA2 id g x t
-- = id (g t) (x t)
-- = (id . g) t (x t) -- = (id <$> g <*> x) t
-- = g t (x t)
(与in the source定义的完全一样),
也是每个Applicative Functor都必须遵守的定律,那就是h <$> g = pure h <*> g
。
最后,
liftA2 h g x == pure h <*> g <*> x
-- h g x == (h g) x
因为<*>
联想到左边:是infixl 4 <*>
.
我目前正在阅读 Haskell 学习你的好书!并且在对某个代码块的评估进行解释时绊倒了。解释看了好几遍,开始怀疑连作者都看不懂这段代码是干什么的了
ghci> (+) <$> (+3) <*> (*100) $ 5
508
应用函子将某些上下文中的函数应用于某些上下文中的值,以获得某些上下文中的某些结果。我花了几个小时研究这个代码块,并对如何计算这个表达式提出了一些解释,其中 none 个是令人满意的。我知道 (5+3)+(5*100) 是 508,但问题在于这个表达式。有没有人对这段代码有明确的解释?
正在使用函数的应用实例。你的代码
(+) <$> (+3) <*> (*100) $ 5
被评估为
( (\a->\b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5 -- f <$> g ( (\x -> (\a->\b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5 -- \x -> f (g x) ( (\x -> (\a->\b->a+b) (x+3)) <*> (\d->d*100) ) 5 ( (\x -> \b -> (x+3)+b) <*> (\d->d*100) ) 5 ( (\x->\b->(x+3)+b) <*> (\d->d*100) ) 5 -- f <*> g (\y -> ((\x->\b->(x+3)+b) y) ((\d->d*100) y)) 5 -- \y -> (f y) (g y) (\y -> (\b->(y+3)+b) (y*100)) 5 (\y -> (y+3)+(y*100)) 5 (5+3)+(5*100)
其中 <$>
是 fmap
或者只是函数组合 .
,而 <*>
是 ap
如果你知道它在 monads 上的行为。
我们先来看看fmap
and (<*>)
是如何定义一个函数的:
instance Functor ((->) r) where fmap = (.) instance Applicative ((->) a) where pure = const (<*>) f g x = f x (g x) liftA2 q f g x = q (f x) (g x)
我们要评估的表达式是:
(+) <$> (+3) <*> (*100) $ 5
或更详细:
((+) <$> (+3)) <*> (*100) $ 5
如果我们这样计算 (<$>)
,它是 fmap
的中缀同义词,我们就会看到它等于:
(+) . (+3)
所以这意味着我们的表达式等同于:
((+) . (+3)) <*> (*100) $ 5
接下来我们可以应用顺序应用。因此,这里 f
等于 (+) . (+3)
而 g
等于 (*100)
。因此,这意味着我们构建了一个如下所示的函数:
\x -> ((+) . (+3)) x ((*100) x)
我们现在可以将其简化并重写为:
\x -> ((+) (x+3)) ((*100) x)
然后重写为:
\x -> (+) (x+3) ((*100) x)
因此,我们构建了一个如下所示的函数:
\x -> (x+3) + 100 * x
或更简单:
\x -> 101 * x + 3
如果我们再计算:
(\x -> 101*x + 3) 5
那我们当然得到:
101 * 5 + 3
因此:
505 + 3
这是预期的:
508
其他两个答案已经详细说明了这是如何计算的——但我想我可能会补充更多 "intuitive" 答案来解释如何在不进行详细计算的情况下,可以 "see" 结果必须是 508.
正如您所暗示的,每个 Applicative
(事实上,甚至每个 Functor
)都可以被视为一种特定类型的 "context",它包含给定类型的值。作为简单的例子:
Maybe a
是一个上下文,其中a
类型的值可能存在,但也可能不存在(通常是由于某种原因可能失败的计算结果)[a]
是一个上下文,可以包含零个或多个a
类型的值,数量没有上限 - 表示特定计算的所有可能结果IO a
是一个上下文,其中类型a
的值作为与 "the outside world" 以某种方式交互的结果可用。 (好吧,那不是那么简单...)
并且,与此示例相关:
r -> a
是一个上下文,其中a
类型的值可用,但其特定值尚不清楚,因为它取决于某些(尚未知的)类型r
.
Applicative
方法在这种上下文中基于值可以很好地理解。 pure
将 "ordinary value" 嵌入到 "default context" 中,在该上下文中它的行为与 "context-free" 尽可能接近。我不会针对上面的 4 个示例中的每一个(其中大部分都非常明显),但我会注意到对于函数,pure = const
- 即 "pure value" a
由始终生成 a
的函数表示,无论源值是什么。
我不想详述如何使用 "context" 隐喻来最好地描述 <*>
,我想详述特定的表达方式:
f <$> a <*> b
其中f
是2"pure values"和a
之间的函数,b
是"values in a context"。这个表达式实际上有一个同义词作为函数:liftA2。尽管使用 liftA2
函数通常被认为不如使用 <$>
和 <*>
的 "applicative style" 惯用,但该名称强调的是 "lift" 上的一个函数"ordinary values" 到 "values in a context" 上的一个。当这样想时,我认为给定一个特定的 "context"(即一个特定的 Applicative
实例),这通常是非常直观的。
所以表达式:
(+) <$> a <*> b
对于值 a
和 b
的类型说 f Int
对于应用程序 f
,对于不同的实例表现如下 f
:
- if
f = Maybe
,那么如果a
和b
都是Just
值,则结果是将基础值相加并将它们包装在Just
。如果a
或b
是Nothing
,则整个表达式是Nothing
. - if
f = []
(列表实例)则上面的表达式是一个列表,其中包含a' + b'
形式的所有和,其中a'
在a
和b'
在b
. - if
f = IO
,则上面的表达式是一个 IO 动作,它执行a
的所有 I/O 效果,然后是b
的效果,并导致这两个动作产生的Int
的总和。
那么,如果 f
是函数实例,最后它会做什么呢?由于 a
和 b
都是描述如何在给定任意 (Int
) 输入的情况下获得给定 Int
的函数,因此提升 (+)
函数是很自然的在它们之上的应该是函数,在给定输入的情况下,获取 a
和 b
函数的结果,然后将结果相加。
当然,这就是它的作用 - 以及它所依据的明确路径已被其他答案非常巧妙地映射出来。但它为什么会这样工作的原因——事实上,这正是我们拥有 f <*> g = \x -> f x (g x)
实例的原因,否则它可能看起来相当随意(尽管实际上它是极少数事情之一,如果不是唯一的话) ,将进行类型检查),以便实例匹配 "values which depend on some as-yet-unknown other value, according to the given function" 的语义。总的来说,我会说这样想 "at a high level" 通常比被迫深入了解计算执行方式的低级细节要好。 (虽然我当然不想淡化也能够做到后者的重要性。)
[其实从哲学的角度来说,可能更准确的说法是定义如此,只是因为"natural"的定义进行了类型检查,纯属巧合然后该实例呈现出如此漂亮的 "meaning"。数学当然充满了这样的快乐 "coincidences" 这背后有很深的原因。]
对于任何应用程序,
a <$> b <*> c = liftA2 a b c
对于函数,
liftA2 a b c x
= a (b x) (c x) -- by definition;
= (a . b) x (c x)
= ((a <$> b) <*> c) x
因此
(+) <$> (+3) <*> (*100) $ 5
=
liftA2 (+) (+3) (*100) 5
=
(+) ((+3) 5) ((*100) 5)
=
(5+3) + (5*100)
(此答案的长版如下。)
纯数学没时间。纯Haskell没时间。用动词说话(“applicative functor applies”等)可能会造成混淆(“applies...when?...”)。
相反,(<*>)
是一个组合子,它组合了一个带有函数的“计算”(由应用函子表示)(在那种计算类型的上下文中 ) 和相同类型的“计算”,携带一个值( 在类似上下文 中),到一个组合的“计算”中,该计算执行该函数对该值的应用(在这种情况下).
“计算”是用来对比纯粹的Haskell“计算”(在 Philip Wadler 的“Calculating is better than Scheming”论文之后,它本身指的是 David Turner 的 Kent Recursive Calculator 语言,一个Haskell).
的(主要)前任 Miranda 的前任“计算”本身可能是纯粹的,也可能不是纯粹的,这是一个正交问题。但它的主要意思是,“计算”体现了 通用函数调用协议 。除了/作为/执行函数对其参数的应用的一部分,他们可能会“做”一些事情。或者类型,
( $ ) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
(=<<) :: (a -> f b) -> f a -> f b
对于函数,上下文是应用程序(另一个一个),并且要恢复值——无论是函数还是参数——应用程序到一个普通的参数要执行。
(耐心等待,我们快到了)。
模式 a <$> b <*> c
也可以表示为 liftA2 a b c
。因此,“函数”应用函子“计算”类型由
liftA2 h x y s = let x' = x s -- embellished application of h to x and y
y' = y s in -- in context of functions, or Reader
h x' y'
-- liftA2 h x y = let x' = x -- non-embellished application, or Identity
-- y' = y in
-- h x' y'
-- liftA2 h x y s = let (x',s') = x s -- embellished application of h to x and y
-- (y',s'') = y s' in -- in context of
-- (h x' y', s'') -- state-passing computations, or State
-- liftA2 h x y = let (x',w) = x -- embellished application of h to x and y
-- (y',w') = y in -- in context of
-- (h x' y', w++w') -- logging computations, or Writer
-- liftA2 h x y = [h x' y' | -- embellished application of h to x and y
-- x' <- x, -- in context of
-- y' <- y ] -- nondeterministic computations, or List
-- ( and for Monads we define `liftBind h x k =` and replace `y` with `k x'`
-- in the bodies of the above combinators; then liftA2 becomes liftBind: )
-- liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- liftBind :: (a -> b -> c) -> f a -> (a -> f b) -> f c
-- (>>=) = liftBind (\a b -> b) :: f a -> (a -> f b) -> f b
事实上,上面所有的片段都可以用 ApplicativeDo 写成 liftA2 h x y = do { x' <- x ; y' <- y ; pure (h x' y') }
或者更直观地写成
liftA2 h x y = [h x' y' | x' <- x, y' <- y]
,使用 Monad Comprehensions,因为上述所有计算类型都是 monad 以及应用函子。顺便说一句,这表明 (<*>) = liftA2 ($)
,这可能也很有启发性。
的确如此,
> :t let liftA2 h x y r = h (x r) (y r) in liftA2
:: (a -> b -> c) -> (t -> a) -> (t -> b) -> (t -> c)
> :t liftA2 -- the built-in one
liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c
即当我们取 f a ~ (t -> a) ~ (->) t a
时类型匹配,即 f ~ (->) t
.
因此,我们已经完成:
(+) <$> (+3) <*> (*100) $ 5
=
liftA2 (+) (+3) (*100) 5
=
(+) ((+3) 5) ((*100) 5)
=
(+) (5+3) (5*100)
=
(5+3) + (5*100)
这就是liftA2
为这种类型定义的方式,Applicative ((->) t) => ...
:
instance Applicative ((->) t) where
pure x t = x
liftA2 h x y t = h (x t) (y t)
不需要定义(<*>)
。 The source code says:
Minimal complete definition
pure, ((<*>) | liftA2)
所以现在你想问很久了,为什么是不是a <$> b <*> c
就等于liftA2 a b c
?
简短的回答是,就是这样。一个可以根据另一个来定义——即 (<*>)
可以通过 liftA2
、
g <*> x = liftA2 id g x -- i.e. (<*>) = liftA2 id = liftA2 ($)
-- (g <*> x) t = liftA2 id g x t
-- = id (g t) (x t)
-- = (id . g) t (x t) -- = (id <$> g <*> x) t
-- = g t (x t)
(与in the source定义的完全一样),
也是每个Applicative Functor都必须遵守的定律,那就是h <$> g = pure h <*> g
。
最后,
liftA2 h g x == pure h <*> g <*> x
-- h g x == (h g) x
因为<*>
联想到左边:是infixl 4 <*>
.