有几个合理的优势可以解决这个问题。我在这里的策略,虽然可能有点粗糙,但工作得很好,同时说明了关键思想,没有太多的技术复杂性。
这个答案有两个部分。第一部分,如果读者时间不够,可以独立阅读,介绍了所选择的观点和主要结论。第二部分通过提供详细的理由对此进行了扩展。在它的最后,有一份Traversable
法律允许和禁止的事情的简明清单。
答案有点长,所以这里有一个使用 Ctrl+F 跳过的部分标题列表:
第一部分
第二部分
- 可填充和可遍历,近距离
- 复制效果:再一次,有感觉
- foldMapDefault 和其他自然法则
- 执行摘要:Traversable 的注意事项
事实上,有人可能会反对这个答案对于这种格式来说太长了。在我的辩护中,我注意到父问题在有关复制效果的部分中得到解决,其他所有内容要么证明直接答案是合理的,要么与上下文相关。
形状和内容
最终,这一切都归结为我喜欢称之为形状和内容的分解。用最简单的术语来说,这意味着Traversable
可以通过这样的类进行编码:
class (Functor t, Foldable t) => Fillable t where
fill :: t () -> [a] -> t a
fill
采用t
函数形状,我们在这里使用一个t ()
值表示,并用从[a]
列表中提取的内容填充它。我们可以依靠Functor
并Foldable
给我们一个相反方向的转换:
detach :: (Functor t, Foldable t) => t a -> (t (), [a])
detach = (() <$) &&& toList
使用fill
and detach
,我们可以sequenceA
根据具体sequenceA
的列表来实现:分离,对列表进行排序,然后填充:
sequenceFill :: (Fillable t, Applicative f) => t (f a) -> f (t a)
sequenceFill = uncurry (fmap . fill) . second (sequenceList) . detach
-- The usual sequenceA for lists.
sequenceList :: Applicative f => [f a] -> f [a]
sequenceList = foldr (liftA2 (:)) (pure [])
如果有点尴尬,也可以用以下方式fill
定义Traversable
:
-- Partial, handle with care.
fillTr :: Traversable t => t () -> [a] -> t a
fillTr = evalState . traverse (const pop)
where
pop = state (\(a : as) -> (a, as))
(对于这种方法的现有技术,例如,参见这个答案。)
就 而言Fillable
,Traversable
定律说fill
并且detach
几乎见证了同构的两个方向:
fill
必须是 的左逆detach
:
uncurry fill . detach = id
这相当于 的恒等律Traversable
。
detach
必须表现为fill
只要fill
只提供具有兼容大小的列表和形状的左逆(否则情况是无望的):
-- Precondition: length (toList sh) = length as
detach . uncurry fill $ (sh, as) = (sh, as)
该属性对应于合成法则。事实上,就其本身而言,它比组合法则更强大。然而,如果我们假设恒等律,它在物质上就等同于合成律。既然如此,可以将这些属性作为Traversable
定律的替代表示,除非您想孤立地研究合成定律。(在我们更仔细地研究合成定律之后,答案的第二部分将对这种近似等效进行更详细的解释。)
复制效果
这一切与你的问题有什么关系?假设我们想要定义一个在不改变可遍历形状的情况下复制效果的遍历(因为改变它会公然违反恒等律)。现在,假设 our sequenceA
is 实际上sequenceFill
,如上定义,我们有哪些选项?由于sequenceFill
搭载sequenceList
,已知每个元素只访问一次,我们唯一的希望是依赖一个伴随Foldable
实例toList
,因此detach
,生成一个包含重复元素的列表。我们可以让Fillable
法律在这种情况下成立吗?
既然如此,traverseFill
那复制效果只能来自非法Fillable
。由于Fillable
法律与法律等价,因此Traversable
我们得出结论,法律Traversable
不能复制效果。
(顺便说一下,上面讨论的效果重复场景适用于您的Bar
类型:它不符合第二Fillable
定律,因此它也符合Traversable
合成定律,正如您的反例所示。)
我非常喜欢的一篇论文涵盖了这个问题和与之相关的问题,是 Bird 等人的《理解惯用语向后和向前遍历》 (2013 年)。虽然一开始可能看起来不像,但它的方法与我在这里展示的内容密切相关。detach
特别是,它的“表示定理”与此处探讨的/关系基本相同,主要区别在于论文中的定义更严格,无需在给出错误列表时fill
大惊小怪fill
长度。
免费的应用演示
虽然我不会尝试提出 Bird 等人的完整论点。论文,在这个答案的上下文中,值得注意的是它对上述表示定理的证明如何依赖于自由应用函子的公式。Traversable
我们可以稍微扭曲一下这个想法,以获得关于Ap
from free的Control.Applicative.Free
额外表示:
-- Adapted from Control.Applicative.Free.
data Ap f a where
Pure :: a -> Ap f a
Ap :: f a -> Ap f (a -> b) -> Ap f b
instance Applicative (Ap f) where
pure = Pure
Pure f <*> y = fmap f y
Ap x y <*> z = Ap x (flip <$> y <*> z)
liftAp :: f a -> Ap f a
liftAp x = Ap x (Pure id)
retractAp :: Applicative f => Ap f a -> f a
retractAp (Pure a) = pure a
retractAp (Ap x y) = x <**> retractAp y
class (Functor t, Foldable t) => Batchable t where
toAp :: t (f a) -> Ap f (t a)
sequenceBatch :: (Batchable t, Applicative f) => t (f a) -> f (t a)
sequenceBatch = retractAp . toAp
toApTr :: Traversable t => t (f a) -> Ap f (t a)
toApTr = traverse liftAp
我非常确定以下是适当的法律,尽管仔细检查可能是值得的:
retractAp . toAp . fmap Identity . runIdentity = id
toAp . fmap Identity . runIdentity . retractAp = id
detach
虽然这看起来与我们一开始的谦逊和组合相去甚远fill
,但它最终只是对同一想法的更精确编码。一个Ap f (t a)
值可以是t a
包装在 中的单个结构Pure
,也可以是零个或多个f a
值的序列(Ap
构造函数),由适当的元数函数封顶,该函数接受与a
s 一样多的f a
s 并产生 a t a
。就我们对形状和内容分解的初步尝试而言,我们有:
(第一部分结束。)
可填充和可遍历,近距离
正如所承诺的那样,第二部分将从证明这Fillable
确实是Traversable
. 在下文中,我将使用更容易用笔和纸操作的定义的调整版本:
-- Making the tuple shuffling implicit. It would have been fine to use
-- the derived Foldable and Traversable. I will refrain from that here
-- only for the sake of explicitness.
newtype Decomp t a = Decomp { getDecomp :: (t (), [a]) }
deriving Functor
deriving instance (Show a, Show (t ())) => Show (Decomp t a)
detach' :: (Functor t, Foldable t) => t a -> Decomp t a
detach' = Decomp . detach
fill' :: Fillable t => Decomp t a -> t a
fill' = uncurry fill . getDecomp
-- Sequence the list, then shift the shape into the applicative layer.
-- Also a lawful sequenceA (amounts to Compose ((,) (t ())) []).
sequenceList' :: Applicative f => Decomp t (f a) -> f (Decomp t a)
sequenceList'
= fmap Decomp . uncurry (map . (,)) . second sequenceList . getDecomp
instance Traversable Decomp where
sequenceA = sequenceList'
instance Foldable Decomp where
foldMap = foldMapDefault
sequenceFill' :: (Fillable t, Applicative f) => t (f a) -> f (t a)
sequenceFill' = fmap fill' . sequenceList' . detach'
(顺便说一句,上面更简洁的定义提供了一个很好的机会来说明,如果我们要离开编写实际 Haskell 的范围,将一直携带的形状移动sequenceFill'
到类型级别并不需要太多,在根据可能的形状对可遍历函子进行分区的效果。据我了解,这将使我们朝着容器的标准依赖类型处理迈进。我不会在这里深入研究;如果您想探索,我衷心推荐Conor McBride(养猪工)关于该主题的答案。)
身份
我们可以从处理恒等律开始,这是一个更直接的问题:
-- Abbreviations:
I = Identity
uI = runIdentity
C = Compose
uC = getCompose
-- Goal: Given the identity law...
sequenceFill' @_ @I . fmap I = I
-- ... obtain detach-then-fill:
fill' . detach' = id
sequenceFill' @_ @I . fmap I = I
uI . fmap fill' . sequenceList' @I . detach' . fmap I = id
-- sequenceList is lawful (identity law):
uI . fmap fill' . I . fmap uI . detach' . fmap I = id
uI . fmap fill' . I . detach' . fmap uI . fmap I = id
uI . fmap fill' . I . detach' = id
uI . I . fill' . detach' = id
fill' . detach' = id -- Goal.
由于上述推导中的所有步骤都是可逆的,我们可以得出结论,同构的先分离后填充方向等价于恒等律。
作品
至于合成法则,我们可以从使用相同的策略开始:
-- Goal: Given the composition law...
sequenceFill' @_ @(C _ _) . fmap C = C . fmap sequenceFill' . sequenceFill'
-- ... obtain fill-then-detach...
detach' . fill' = id
-- ... within the domain specified by its precondition.
sequenceFill' @_ @(C _ _) . fmap C = C . fmap sequenceFill' . sequenceFill'
fmap fill' . sequenceList' @(C _ _) . detach' . fmap C
= C . fmap (fmap fill' . sequenceList' . detach')
. fmap fill' . sequenceList' . detach'
-- LHS
fmap fill' . sequenceList' @(C _ _) . detach' . fmap C
fmap fill' . sequenceList' @(C _ _) . fmap C . detach'
-- sequenceList' is lawful (composition law)
fmap fill' . C . fmap sequenceList' . sequenceList' . detach'
C . fmap (fmap fill') . fmap sequenceList' . sequenceList' . detach'
C . fmap (fmap fill' . sequenceList') . sequenceList' . toList'
-- RHS
C . fmap (fmap fill' . sequenceList' . detach')
. fmap fill' . sequenceList' . detach'
C . fmap (fmap fill' . sequenceList') . fmap (detach' . fill')
. sequenceList' . detach'
-- LHS = RHS
C . fmap (fmap fill' . sequenceList') . sequenceList' . detach'
= C . fmap (fmap fill' . sequenceList') . fmap (detach' . fill')
. sequenceList' . detach'
-- C is injective:
fmap (fmap fill' . sequenceList') . sequenceList' . detach'
= fmap (fmap fill' . sequenceList') . fmap (detach' . fill')
. sequenceList' . detach' -- On hold.
在这一点上,我们似乎被困在一个比detach' . fill' = id
我们预期发现的更弱的属性上。从好的方面来说,它有一些好处:
上述推导中的所有步骤都是可逆的,因此性质等价于合成定律。
填充等式两边的sequenceList' . detach'
和fmap (fmap fill' . sequenceList')
额外的项使得每个fill'
前面都有 a detach'
,并且每个detach'
后面都有 a fill'
。这意味着填充-然后-分离法则的前提条件自动成立。
填充然后分离法则比这个属性更严格。既然如此,如果detach' . fill' = id
(在前提条件等的范围内)那么这个性质,因此合成法则,也成立。
稍后我将回到这些观察结果,以证明我之前detach' . fill' = id
可以被视为Traversable
法律的主张的合理性。
幂等性
短暂的休息,然后我们继续我们的常规日程。我们可以通过将组合律中的两个应用函子专门化来发现一些琐事Identity
。从我们停止的地方继续:
fmap (fmap fill' . sequenceList') . sequenceList' . detach'
= fmap (fmap fill' . sequenceList') . fmap (detach' . fill')
. sequenceList' . detach'
-- In particular:
fmap (fmap fill' . sequenceList' @I) . sequenceList' @I . detach'
= fmap (fmap fill' . sequenceList' @I) . fmap (detach' . fill')
. sequenceList' @I . detach'
-- sequenceList' is lawful (identity):
fmap (fmap fill' . I . fmap uI) . I . fmap uI . detach'
= fmap (fmap fill' . I . fmap uI) . fmap (detach' . fill') . I
. fmap uI . detach'
-- shift the I leftwards, and the uI rightwards, on both sides:
I . I . fill' . detach' . fmap uI . fmap uI
= I . I . fill' . detach' . fill' . detach' . fmap uI . fmap uI
-- I is injective, and fmap uI is surjective:
fill' . detach' = fill' . detach' . fill' . detach'
我们最终得到 的幂等性属性fill' . detach'
,并且间接地也为sequenceA
。尽管这样的属性就其Traversable
本身而言并不令人惊讶,因为它直接来自同一律,但相当有趣的是,它本身也来自组合律。(在相关的说明中,我有时想知道我们是否可以从Semitraversable
仅具有合成定律的一类中获得任何里程。)
复制效果:再一次,有感觉
现在是重新审视您最初的问题的好时机:究竟为什么重复效应会导致法律出现问题?Fillable
演示文稿有助于澄清联系。让我们再看看组合律的两面,以我们刚刚给出的形式:
fmap (fmap fill' . sequenceList')
. sequenceList' . detach' -- LHS
fmap (fmap fill' . sequenceList')
. fmap (detach' . fill')
. sequenceList' . detach' -- RHS
让我们假设恒等律成立。在这种情况下,复制效果的唯一可能来源sequenceFill'
是被复制的元素detach'
(因为sequenceList'
不复制,并且fill'
由于恒等律而不能复制)。
现在,如果detach'
在某些位置引入重复项,则fill'
必须删除它们以使同一律成立。然而,由于参数化,这些位置的元素将始终被删除,即使相关元素实际上并没有被复制,因为列表不是由detach'
. 换句话说,无害地删除重复项有一个先决条件fill'
,即必须给出可能由detach'
. 在组合法则中,根据应用效果的不同,第一个sequenceList'
产生的列表可能不在此前提条件范围内。在这种情况下,fmap fill'
紧随其后的右侧的 将消除内部影响(请记住第一个sequenceList'
只处理实际上没有复制的外部应用层),差异将被第二个适当地检测到sequenceList' . detach'
,它作用于内部效果层,我们最终会违反法律。
事实上,我们可以肯定一些更强有力的东西:如果sequenceFill'
重复效果,总是有可能以上述方式违反法律。对于这样的主张,我们只需要一个足够好的反例:
advance :: State (Const (Sum Natural) x) (Const (Sum Natural) x)
advance = get <* modify (+1)
诀窍是,如果您对仅包含 副本的advance
列表进行排序,则您将得到的列表保证不会有任何重复的Const (Sum Natural)
效果:
GHCi> flip evalState 0 $ sequenceA (replicate 3 advance)
[Const (Sum {getSum = 0}),Const (Sum {getSum = 1}),Const (Sum {getSum = 2})]
既然如此,如果这样的列表到达一个sequenceFill'
重复效果的实现,fmap fill'
它总是会丢弃非重复的:
data Bar a = Bar a
deriving (Show, Functor)
instance Foldable Bar where
foldMap f (Bar x) = f x <> f x
-- This corresponds to your Traversable instance.
instance Fillable Bar where
fill (Decomp (_, [x, y])) = Bar y
GHCi> flip evalState 0 <$> (advance <$ Bar ())
Bar (Const (Sum {getSum = 0}))
GHCi> flip evalState 0 <$> detach' (advance <$ Bar ())
Decomp {getDecomp = (Bar (),[Const (Sum {getSum = 0}),Const (Sum {getSum = 0})])}
GHCi> flip evalState 0 $ (sequenceList' . detach') (advance <$ Bar ())
Decomp {getDecomp = (Bar (),[Const (Sum {getSum = 0}),Const (Sum {getSum = 1})])}
GHCi> flip evalState 0 $ (fmap fill' . sequenceList' . detach') (advance <$ Bar ())
Bar (Const (Sum {getSum = 1}))
违规现在是不可避免的:
GHCi> lhs = fmap (fmap fill' . sequenceList') . sequenceList' . detach'
GHCi> rhs = fmap (fmap fill' . sequenceList') . fmap (detach' . fill') . sequenceList' . detach'
GHCi> flip evalState 0 $ lhs (advance <$ Bar ())
Const (Sum {getSum = 1})
GHCi> flip evalState 0 $ rhs (advance <$ Bar ())
Const (Sum {getSum = 2})
(正如您可能已经注意到的那样,它与您的答案advance
中的反例非常相似,只是进行了调整,以便它可以与任意可遍历的结构一起使用。)
这足以表明效果的重复与合成法则不相容。
简化合成法则
在这一点上,有一个方便的方法来证明为什么我们可以使用简化的填充然后分离属性......
-- Precondition: length (toList sh) = length as
detach' . fill' $ (sh, as) = (sh, as)
...代替我们在最后几节中处理的庞大的组合法。再次假设恒等律成立。在这种情况下,我们可以将可能的实现分为detach'
两种情况:
detach'
从不重复元素。因此,detach'
在填充后分离前提条件的范围内,满射(例如,如果可遍历函子是长度为 6 的向量,detach'
则可以生成所有可能的长度为 6 的列表,尽管它不会生成列表其他长度)。但是,如果具有左逆的函数是满射的,那么它的左逆也是右逆。因此,detach' . fill' = id
在前提条件的范围内,组合律成立。
(“在填充-然后-分离前提条件的范围内”位可能感觉像是在挥手,但我相信可以通过使用依赖类型根据形状划分可遍历函子类型来使其变得严格,就像我提到的那样第二部分的开始。)
detach'
重复元素。但是,在这种情况下,随后的效果重复意味着组合定律将不成立,正如我们刚刚展示的那样,更强的detach' . fill' = id
属性也不会成立。
这样一来,只要同一律成立,Traversable
组合律和填充-分离律总是一致的;Fillable
它们之间的区别只能出现在违反身份法的实现中。因此,如果综合起来,Fillable
答案第一部分所述的法律是等价Traversable
的。
foldMapDefault 和其他自然法则
演示文稿的一个漂亮特点Fillable
是它如何明确地表明,我们在定义合法时唯一的自由选择sequenceA
是效果的排序顺序。一旦通过选择一个Foldable
实现选择了某个顺序,该实现确定toList
和detach'
,sequenceList'
必须在对效果进行排序时遵循该顺序。此外,由于fill'
是(在填充后分离前提条件的范围内) 的完全逆detach'
,因此它是唯一确定的。
我们在基础库中的类层次结构与 的排列方式并不完全相同Fillable
:realsequenceA
是一种自给自足的方法Traversable
,与 不同的sequenceFill'
是,它的实现不依赖于Foldable
它。相反, 和 之间的联系Foldable
被Traversable
超类一致性法则理顺:
-- Given:
foldMapDefault :: (Traversable t, Monoid m) => (a -> m) -> t a -> m
foldMapDefault f = getConst . traverse (Const . f)
foldMapDefault = foldMap
( 和 有一个类似的性质Functor
,fmapDefault
但参数性意味着它遵循恒等律。)
就toList
和而言sequenceA
,该定律变为:
toList = getConst . sequenceA . fmap (Const . (:[]))
如果我们过去常常sequenceA = sequenceFill'
把我们带回Fillable
演示文稿......
getConst . fmap fill' . sequenceList' . detach' . fmap (Const . (:[]))
getConst . fmap fill' . sequenceList' . fmap (Const . (:[])) . detach'
-- fmap @(Const _) doesn't do anything:
getConst . sequenceList' . fmap (Const . (:[])) . detach'
-- sequenceList' is lawful (foldMapDefault law):
toList @(Detach _) . detach'
snd . getDecomp . detach'
toList
...我们得出结论,foldMapDefault
法律自动成立。
Bird 等人的“数据类型中的‘自然性’”
在恒等律和组合律之后,第三个最广为人知的定律Traversable
是应用函子中的自然性,通常简称为自然性定律:
-- Precondition: h is an applicative homomorphism, that is:
-- h (pure a) = pure a
-- h (u <*> v) = h u <*> h v
h . sequenceA = sequenceA . fmap h
虽然有用,并且在理论方面具有重要意义(它反映了sequenceA
作为应用函子和应用同态范畴中的自然变换的另一种观点,例如在 Jaskelioff 和 Rypacek, An Investigation of the Laws of Traversals中讨论过),自然性定律来自一个自由定理sequenceA
(在 Voigtländer 的脉络中,涉及构造函数类的自由定理),因此在这个答案的上下文中没有太多可说的。
鸟等人。第一部分提到的论文在第 6 节中讨论了一种不同的自然性属性,作者称之为“数据类型中的“自然性””。与更广为人知的自然法则不同,它是可遍历函子本身的自然属性:
-- Precondition: r preserves toList, that is
-- toList . r = toList
fmap r . sequenceA = sequenceA . r
(Bird 等人不要明确使用Foldable
,而是用 来说明属性contents = getConst . traverse (Const . (:[])
。假设foldMapDefault
相干定律成立,则没有区别。)
Fillable
透视图非常适合这种自然属性。我们可以首先注意到我们也可以在一些函子上进行自然转换t
以进行处理Decomp t
:
-- Decomp as a higher-order functor.
hmapDecomp :: (forall x. t x -> u x) -> Decomp t a -> Decomp u a
hmapDecomp r (Decomp (sh, as)) = Decomp (r sh, as)
如果r
保留toList
(或者,我们甚至可以说,如果它是可折叠的同态),那么它也保留detach'
,反之亦然:
-- Equivalent to toList . r = toList
hmapDecomp r . detach' = detach' . r'
(hmapDecomp
不影响内容列表,并且作为自然变换,与.)的一半r
通勤(() <$)
detach'
如果我们进一步假设这些定律,我们可以使用和是逆Fillable
的事实(在填充-然后-分离定律的前提条件范围内)从转变为:fill'
detach'
r
detach'
fill'
hmapDecomp r . detach' = detach' . r
hmapDecomp r . detach' . fill' = detach' . r . fill'
hmapDecomp r = detach' . r . fill'
fill' . hmapDecomp r = fill' . detach' . r . fill'
fill' . hmapDecomp r = r . fill'
也就是说,应用r
到形状然后填充它与填充然后应用r
到填充的形状相同。
在这一点上,我们可以回到sequenceFill'
:
fill' . hmapDecomp r = r . fill'
fmap (fill' . hmapDecomp r) = fmap (r . fill')
fmap (fill' . hmapDecomp r) . sequenceList' . detach'
= fmap (r . fill') . sequenceList' . detach'
-- LHS
fmap (fill' . hmapDecomp r) . sequenceList' . detach'
-- sequenceList' only changes the list, and `hmapDecomp` r only the shape.
fmap fill' . sequenceList' . hmapDecomp r . detach'
-- r is a foldable homomorphism.
fmap fill' . sequenceList' . detach' . r
sequenceFill' . r
-- RHS
fmap (r . fill') . sequenceList' . detach'
fmap r . sequenceFill'
-- LHS = RHS
fmap r . sequenceFill' = sequenceFill' . r
因此,我们已经获得了可遍历函子属性的自然性,正如在Fillable
和Traversable
定律之间的等价情况下所预料的那样。尽管如此,我们确实在这个过程中学到了一些东西。伯德等人。在谈论此属性时,对“自然性”一词保持谨慎是有道理的,因为toList
在标准类层次结构的上下文中,对保留自然转换的限制似乎是无关紧要的。然而,从这个Fillable
角度来看,fill'
是由我们对Foldable
实例的选择决定的,因此该属性与构造函数类的任何其他自然属性一样清晰。既然如此,我相信我们可以放弃围绕“自然”的吓人语录。
执行摘要:Traversable 的注意事项
Traversable
我们现在可以对法律的后果做出一个相当完整的清单。虽然没有真正的区别,但我将在这里用它来说话traverse
,因为使用它而不是sequenceA
使它更清楚“元素”的含义,而不是“效果”。
合法的traverse
不得:
由于恒等律,以任何方式更改可遍历形状。
- 如果改变是幂等的,恒等律仍然会被违反,但组合律可能成立。
由于恒等律,删除或重复元素。
- 特别是,即使通过用其他元素覆盖某些元素来保持形状不变,也不允许这样做。
由于恒等律,对可遍历结构中的元素重新排序。
重复的效果,即使没有重复的元素,由于组合法。
合法的traverse
可以:
- Reorder effects,即与原始可遍历结构中元素的顺序不同的序列效果。
合法traverse
必须:
- 序列效应按照
toList
从Foldable
实例给定的顺序为类型,由于foldMapDefault
规律。
合法traverse
遗嘱: