(作为借口:标题模仿Why do we need monads?)的标题
有containers(和indexed个)(以及hasochistic个)和descriptions。但是容器是problematic而且根据我非常小的经验,从容器的角度来看,它比在描述方面更难思考。非索引容器的类型与Σ
同构 - 这非常不明确。形状和位置描述有帮助,但在
⟦_⟧ᶜ : ∀ {α β γ} -> Container α β -> Set γ -> Set (α ⊔ β ⊔ γ)
⟦ Sh ◃ Pos ⟧ᶜ A = ∃ λ sh -> Pos sh -> A
Kᶜ : ∀ {α β} -> Set α -> Container α β
Kᶜ A = A ◃ const (Lift ⊥)
我们实际上使用Σ
而不是形状和位置。
容器上严格正面的免费monad的类型有一个相当简单的定义,但Freer
monad的类型看起来更简单(在某种意义上Freer
monad甚至比平常更好Free
monad {1}} monad,如paper中所述。)
那么我们可以用更好的方式处理容器而不是描述或其他东西呢?
答案 0 :(得分:22)
在我看来,容器的价值(如容器理论中)是一致性。这种一致性为使用容器表示作为可执行规范的基础提供了相当大的余地,甚至可能是机器辅助程序的推导。
我不推荐(规范化)容器的固定点作为实现递归数据结构的一种通用方法。也就是说,知道给定的函子具有(最多iso)一个表示作为容器是有帮助的,因为它告诉你可以实例化通用容器功能(易于实现,一劳永逸,由于均匀性)对你的特殊仿函数,以及你应该期待的行为。但这并不是说容器实现在任何实际方面都是有效的。实际上,我通常更喜欢一阶数据的一阶编码(标签和元组,而不是函数)。
要修复术语,我们假设容器的Cont
类型(在Set
上,但其他类别可用)由构造函数<|
提供,包含两个字段,形状和位置
S : Set
P : S -> Set
(这是用于确定Sigma类型,Pi类型或W类型的数据的相同签名,但这并不意味着容器与这些东西中的任何一个相同,或者这些东西是彼此相同的。)
给出了对仿函数这样的解释
[_]C : Cont -> Set -> Set
[ S <| P ]C X = Sg S \ s -> P s -> X -- I'd prefer (s : S) * (P s -> X)
mapC : (C : Cont){X Y : Set} -> (X -> Y) -> [ C ]C X -> [ C ]C Y
mapC (S <| P) f (s , k) = (s , f o k) -- o is composition
我们已经赢了。该map
为所有人实施了一次。更重要的是,算子法仅通过计算来实现。不需要对类型结构进行递归来构造操作,也不需要证明法则。
没有人试图声称,在操作上,Nat <| Fin
提供了高效列表的实现,只是通过进行识别我们学到了有关列表结构的有用信息。
让我谈谈 description 。为了懒惰读者的利益,让我重新构建它们。
data Desc : Set1 where
var : Desc
sg pi : (A : Set)(D : A -> Desc) -> Desc
one : Desc -- could be Pi with A = Zero
_*_ : Desc -> Desc -> Desc -- could be Pi with A = Bool
con : Set -> Desc -- constant descriptions as singleton tuples
con A = sg A \ _ -> one
_+_ : Desc -> Desc -> Desc -- disjoint sums by pairing with a tag
S + T = sg Two \ { true -> S ; false -> T }
Desc
中的值描述了其修复点提供数据类型的仿函数。他们描述了哪些仿函数?
[_]D : Desc -> Set -> Set
[ var ]D X = X
[ sg A D ]D X = Sg A \ a -> [ D a ]D X
[ pi A D ]D X = (a : A) -> [ D a ]D X
[ one ]D X = One
[ D * D' ]D X = Sg ([ D ]D X) \ _ -> [ D' ]D X
mapD : (D : Desc){X Y : Set} -> (X -> Y) -> [ D ]D X -> [ D ]D Y
mapD var f x = f x
mapD (sg A D) f (a , d) = (a , mapD (D a) f d)
mapD (pi A D) f g = \ a -> mapD (D a) f (g a)
mapD one f <> = <>
mapD (D * D') f (d , d') = (mapD D f d , mapD D' f d')
我们不可避免地要通过递归而不是描述来工作,所以这是更难的工作。仿函法也不是免费的。在操作上,我们可以更好地表示数据,因为在具体元组执行时我们不需要求助于功能编码。但我们必须更加努力地编写程序。
请注意,每个容器都有一个描述:
sg S \ s -> pi (P s) \ _ -> var
但是,每个描述都有表示作为同构容器也是如此。
ShD : Desc -> Set
ShD D = [ D ]D One
PosD : (D : Desc) -> ShD D -> Set
PosD var <> = One
PosD (sg A D) (a , d) = PosD (D a) d
PosD (pi A D) f = Sg A \ a -> PosD (D a) (f a)
PosD one <> = Zero
PosD (D * D') (d , d') = PosD D d + PosD D' d'
ContD : Desc -> Cont
ContD D = ShD D <| PosD D
这就是说,容器是描述的正常形式。这是一项练习,表明[ D ]D X
与[ ContD D ]C X
自然是同构的。这样可以让生活变得更轻松,因为要说明如何处理描述,原则上它足以说明如何处理正常形式的容器。原则上,上述mapD
操作可以通过将同构与mapC
的统一定义融合来获得。
同样,如果我们有一个相等的概念,我们可以说容器的单孔上下文统一
_-[_] : (X : Set) -> X -> Set
X -[ x ] = Sg X \ x' -> (x == x') -> Zero
dC : Cont -> Cont
dC (S <| P) = (Sg S P) <| (\ { (s , p) -> P s -[ p ] })
即,容器中的单孔上下文的形状是原始容器的形状和孔的位置的对;这些位置是除了洞之外的原始位置。这是&#34;与证据相关的版本乘以索引,减少索引&#34;区分电力系列时。
这种统一的处理为我们提供了一个规范,我们可以从中得出数百年来计算多项式导数的程序。
dD : Desc -> Desc
dD var = one
dD (sg A D) = sg A \ a -> dD (D a)
dD (pi A D) = sg A \ a -> (pi (A -[ a ]) \ { (a' , _) -> D a' }) * dD (D a)
dD one = con Zero
dD (D * D') = (dD D * D') + (D * dD D')
如何检查我的派生运算符的描述是否正确?通过检查它与容器的衍生物!
不要陷入这样的陷阱:只是因为某个想法的呈现在操作上没有帮助,因为它在概念上无助于它。
最后一件事。 Freer
技巧相当于以特定方式重新排列任意仿函数(切换到Haskell)
data Obfuncscate f x where
(:<) :: forall p. f p -> (p -> x) -> Obfuncscate f x
但这不是容器的替代。这是容器展示的轻微描述。如果我们有强存在和依赖类型,我们可以写
data Obfuncscate f x where
(:<) :: pi (s :: exists p. f p) -> (fst s -> x) -> Obfuncscate f x
这样(exists p. f p)
代表形状(你可以选择你的位置表示,然后用它的位置标记每个地方),fst
从形状中挑选出存在的见证(位置表示你选择)。它的优点是显然是正面完全,因为它是一个容器表示。
在Haskell中,当然,你必须讨论存在主义,幸运的是,它只依赖于类型投射。它是存在主义的弱点,它证明了Obfuncscate f
和f
的等价性是正当的。如果你在具有强烈存在性的依赖类型理论中尝试相同的技巧,编码会失去其独特性,因为你可以投射并分辨出不同的位置表示选择。也就是说,我可以通过
Just 3
Just () :< const 3
或
Just True :< \ b -> if b then 3 else 5
而且,在Coq中,这些可证明是独特的。
容器类型之间的每个多态函数都以特定方式给出。这种统一性正在努力再次澄清我们的理解。
如果你有一些
f : {X : Set} -> [ S <| T ]C X -> [ S' <| T' ]C X
它是(扩展性地)由以下数据给出的,它们没有提到任何元素:
toS : S -> S'
fromP : (s : S) -> P' (toS s) -> P s
f (s , k) = (toS s , k o fromP s)
也就是说,在容器之间定义多态函数的唯一方法是说如何将输入形状转换为输出形状,然后说明如何从输入位置填充输出位置。
对于您严格肯定的仿函数的首选表示,对多态函数进行类似的严格表征,从而消除对元素类型的抽象。 (为了描述,我会将它们的可还原性用于容器。)
考虑到两个仿函数f
和g
,很容易说出它们的构成f o g
是什么:(f o g) x
包装了f (g x)
中的内容,给出了我们&#34; f
- g
- 结构&#34;的结构。但是,您是否可以轻易地强加额外的条件,即g
结构中存储的所有f
结构具有相同的形状?
让我们说f >< g
抓住f o g
的转置片段,其中所有g
形状都相同,这样我们才能同样将事物变成g
结构的f
- 结构。例如,当[] o []
给出参差不齐的列表时,[] >< []
会给出矩形矩阵; [] >< Maybe
会提供全部Nothing
或全部Just
的列表。
给><
代表严格正面仿函数的首选代表。对于容器来说,这很容易。
(S <| P) >< (S' <| P') = (S * S') <| \ { (s , s') -> P s * P' s' }
容器,采用标准化的Sigma-then-Pi形式,并非旨在成为数据的高效机器表示。但是,然而,实现的给定仿函数具有作为容器的表示的知识有助于我们理解其结构并为其提供有用的设备。对于容器,可以抽象地给出许多有用的结构,一旦适用于所有情况,必须根据具体情况给出其他演示。所以,是的,了解容器是个好主意,只要掌握实际实现的更具体结构背后的基本原理。