我正在阅读“了解你是一个很好的Haskell!”并且我对某个代码块的评估的解释感到磕磕绊绊。我已多次阅读这些解释,并开始怀疑即使是作者也能理解这段代码的作用。
ghci> (+) <$> (+3) <*> (*100) $ 5
508
应用程序函数在某些上下文中将函数应用于某个上下文中的值,以在某些上下文中获得某些结果。我花了几个小时来研究这个代码块,并对这个表达式的评估方式提出了一些解释,但没有一个是令人满意的。我知道(5 + 3)+(5 * 100)是508,但问题是这个表达式。有没有人对这段代码有明确的解释?
它正在使用应用实例来实现功能。你的代码
(+) <$> (+3) <*> (*100) $ 5
被评估为
( (\a->b->a+b) <$> (\c->c+3) <*> (\d->d*100) ) 5 ( (\x -> (\a->b->a+b) ((\c->c+3) x)) <*> (\d->d*100) ) 5 ( (\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 (\y -> ((\x->b->(x+3)+b) y) ((\d->d*100) y)) 5 (\y -> (b->(y+3)+b) (y*100)) 5 (\y -> (y+3)+(y*100)) 5 (5+3)+(5*100)
其中<$>
是fmap
或只是函数组合.
,<*>
是ap
,如果你知道它在monad上的行为。
另外两个答案给出了计算方法的细节 - 但我想我可能会用更“直观”的答案来解释如何在不经过详细计算的情况下“看到”结果必须为508 。
正如你暗示的那样,每个Applicative
(实际上,甚至每个Functor
)都可以被视为一种特定的“上下文”,它包含给定类型的值。举个简单的例子:
Maybe a
是一个上下文,其中可能存在a
类型的值,但可能不存在(通常是由于某种原因可能会失败的计算结果)[a]
是一个上下文,它可以保存零个或多个a
类型的值,没有数字上限 - 表示特定计算的所有可能结果IO a
是一种上下文,其中a
类型的值可以通过某种方式与“外部世界”进行交互。 (好吧,那不是那么简单......)并且,与此示例相关:
r -> a
是一个上下文,其中a
类型的值可用,但其特定值尚不清楚,因为它取决于r
类型的某些(尚未知)值。基于这种背景下的价值观,可以很好地理解Applicative
方法。 pure
在“默认上下文”中嵌入一个“普通值”,在该上下文中,它与“无上下文”的行为尽可能接近。我不会对上面的4个例子中的每个例子(大多数都非常明显)进行讨论,但是我会注意到,对于函数,pure = const
- 也就是说,“纯值”a
由始终产生的函数表示a
无论什么来源价值。
我不想详述使用“上下文”比喻如何最好地描述<*>
,而是想详述一下:
f <$> a <*> b
其中f
是2“纯值”之间的函数,a
和b
是“上下文中的值”。事实上,这个表达式有一个同义词作为函数:liftA2。虽然使用liftA2
函数通常被认为不如使用<$>
和<*>
的“适用风格”惯用,但该名称强调该想法是将“普通值”上的函数“提升”为“上下文中的值”。当我想到这样的时候,我认为这通常非常直观,给定一个特定的“背景”(即特定的Applicative
实例)。
所以表达式:
(+) <$> a <*> b
对于值a
和b
类型,如f Int
为适用的f
,对于不同的实例f
表现如下:
f = Maybe
,那么结果,如果a
和b
都是Just
值,是将基础值加起来并将它们包装在Just
中。如果a
或b
是Nothing
,那么整个表达式就是Nothing
。f = []
(列表实例)那么上面的表达式是一个包含a' + b'
形式的所有总和的列表,其中a'
在a
而b'
在b
。f = IO
,那么上面的表达式是一个IO动作,执行a
的所有I / O效果,然后是b
的那些,并导致这两个动作产生的Int
s的总和。那么,最后,如果f
是函数实例呢?由于a
和b
都是描述如何在给定任意(Int
)输入的情况下获得给定Int
的函数,因此将(+)
函数提升到它们上应该是这样的函数,给定输入,得到a
和b
的结果。 f <*> g = \x -> f x (g x)
函数,然后添加结果。
当然,这就是它的作用 - 以及它所做的明确路线,其他答案已经非常巧妙地绘制出来。但是之所以这样做的原因 - 实际上,我们之所以有fmap
这个实例,其原因可能看似相当随意(尽管实际上它是极少数事情之一,如果不是唯一的事情,将会输入-check),使得实例匹配“依赖于某些尚未知的其他值的值,根据给定的函数”的语义。总的来说,我认为通常更好地考虑这样的“高水平”,而不是被迫深入到准确计算如何执行的低级细节。 (虽然我当然不想淡化能够做后者的重要性。)
[实际上,从哲学的角度来看,可能更准确地说定义是因为它只是因为它是类型检查的“自然”定义,而且实例然后采用这种定义只是快乐的巧合。一个很好的“意义”。数学当然充满了如此快乐的“巧合”,结果却背后有很深的理由。
我们先来看看如何为函数定义(<*>)
和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
这是预期的:
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)
从而
(<*>)
(这个答案的长版本如下。)
纯数学没有时间。 Pure Haskell没有时间。用动词(“applicative functor apply”等)说话可能会令人困惑(“适用......何时?......”)。
相反, ( $ ) :: (a -> b) -> a -> b
(<$>) :: (a -> b) -> f a -> f b
(<*>) :: f (a -> b) -> f a -> f b
(=<<) :: (a -> f b) -> f a -> f b
是一个组合器,它结合了一个“计算”(由一个应用函子表示),它带有一个函数(在某些上下文中)和一个相同类型的“计算”,携带一个值(在类似的上下文中),组合成一个“计算“执行该函数对该值的应用(在这种情况下)。
“计算”用于将其与纯Haskell“计算”进行对比。 “计算”可能是也可能不是纯粹的,这是一个正交的问题。但主要是它的意思是“计算”体现了一种通用的函数调用协议。除了/执行函数应用程序的一部分之外,它们可能会“做”某些事情。或者在类型中,
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
。因此,“函数”应用函子“计算”类型由定义
> :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。 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
(正如它定义的那样h <$> g = pure h <*> g
),
每个申请编织者必须遵循的法律是liftA2 h g x == pure h <*> g <*> x
-- h g x == (h g) x
。
最后,
<*>
因为infixl 4 <*>
与左边相关:它是qazxswpoi。