通过单态化(仅单态化)在语言中实现多态性的一个限制是您失去了支持多态性递归的能力(例如,参见rust-lang #4287)。
在编程语言中支持多态递归的一些引人注目的用例是什么?我一直在尝试找到使用此功能的库/概念,到目前为止,我遇到了一个示例:
为防止问题过于广泛,我正在寻找其他将多态递归应用于传统计算机科学问题(例如编写编译器的问题)的程序/库/研究论文。
我不想要的东西的例子:
答案显示了如何使用多态递归从类别理论对X进行编码,除非他们演示了如何编码X对于解决上述条件下的Y有好处。
小玩具示例,表明您可以使用多态递归来执行X,但不能没有它。
答案 0 :(得分:3)
有时您需要对类型中的一些约束进行编码,以便在编译时强制执行。
例如,可以将完整的二叉树定义为
data CTree a = Tree a | Dup (CTree (a,a))
example :: CTree Int
example = Dup . Dup . Tree $ ((1,2),(3,4))
该类型将阻止将诸如((1,2),3)
之类的不完整的树存储在内部,从而强制执行不变式。
冈崎的书展示了许多这样的例子。
如果然后要对此类树进行操作,则需要多态递归。
编写函数来计算树的高度,将CTree Int
中的所有数字相加,或者通用映射或折叠都需要多态递归。
现在,不需要/想要这样的多态递归类型并不十分频繁。尽管如此,他们还是很高兴。
在我个人看来,单态化并不令人满意,这不仅是因为它阻止了多态性递归,而且还因为它需要针对使用的每种类型一次编译一次多态性代码。在Haskell或Java中,使用Maybe Int, Maybe String, Maybe Bool
不会导致与Maybe
相关的函数被编译三次并在最终目标代码中出现三次。在C ++中,会发生这种情况,使目标代码膨胀。不过,在C ++中,这确实允许使用更有效的专业化方法(例如,std::vector<bool>
可以用位向量实现)。这样可以进一步启用C ++的SFINAE等。不过,我认为在将多态代码编译一次并进行一次类型检查之后,我还是更喜欢它-之后保证所有类型的类型都是安全的。
答案 1 :(得分:2)
以下是我工作的一个例子,我认为它可以很好地概括:在连接语言中,即一种基于在共享程序状态(例如堆栈)上运行的组合函数构建的语言,所有函数相对于他们不接触的堆栈部分,所有递归都是多态递归,而且所有高阶函数也都具有较高的排名。例如,这种语言中的map
的类型可能是:
∀αβσ。 σ×列表α×(∀τ。τ×α→τ×β)→σ×列表β
其中×是左侧关联产品类型,左侧为堆栈类型,右侧为值类型,σ和τ为堆栈类型变量,而α和β为值类型变量。 map
可以在任何程序状态σ上调用,只要它具有一个αs列表并在顶部具有一个从αs到βs的函数,例如:
"ignored" [ 1 2 3 ] { succ show } map
=
"ignored" [ "2" "3" "4" ]
这里存在多态递归,因为map
在σ的不同实例(即不同类型的“堆栈其余部分”)上递归调用自身:
-- σ = Bottom × String
"ignored" [ 1 2 3 ] { succ show } map
"ignored" 1 succ show [ 2 3 ] { succ show } map cons
-- σ = Bottom × String × String
"ignored" "2" [ 2 3 ] { succ show } map cons
"ignored" "2" 2 succ show [ 3 ] { succ show } map cons cons
-- σ = Bottom × String × String × String
"ignored" "2" "3" [ 3 ] { succ show } map cons cons
"ignored" "2" "3" 3 succ show [ ] { succ show } map cons cons cons
-- σ = Bottom × String × String × String × String
"ignored" "2" "3" "4" [ ] { succ show } map cons cons cons
"ignored" "2" "3" "4" [ ] cons cons cons
"ignored" "2" "3" [ "4" ] cons cons
"ignored" "2" [ "3" "4" ] cons
"ignored" [ "2" "3" "4" ]
map
的函数参数必须具有更高的排名,因为它也在不同的堆栈类型(τ的不同实例化)上被调用。
为了做到这一点而无需多态递归,您将需要一个额外的堆栈或局部变量,在其中放置map
的中间结果以使它们“不受干扰”,以便进行所有递归调用在相同类型的堆栈上。这对如何将功能语言编译为例如类型的组合器计算机:通过多态递归,您可以在保持虚拟机简单性的同时保留安全性。
这种形式的一般形式是您具有一个递归函数,该函数在数据结构的 part 上是多态的,例如HList
的初始元素或多态的子集记录。
就像@chi已经提到的那样,在Haskell的函数级别需要多态递归的主要实例是在 type 级别具有多态递归,例如:
data Nest a = Nest a (Nest [a]) | Nil
example = Nest 1 $ Nest [1, 2] $ Nest [[1, 2], [3, 4]] Nil
这种类型的递归函数始终是多态递归的,因为类型参数随每次递归调用而变化。
Haskell要求此类函数具有类型签名,但是除了类型之外,在机械上,递归和多态递归之间没有区别。如果您有一个辅助newtype
隐藏了多态性,则可以编写一个多态定点运算符:
newtype Forall f = Abstract { instantiate :: forall a. f a }
fix' :: forall f. ((forall a. f a) -> (forall a. f a)) -> (forall a. f a)
fix' f = instantiate (fix (\x -> Abstract (f (instantiate x))))
没有进行所有的包装和展开仪式,这与fix' f = fix f
相同。
这也是为什么多态递归不需要导致函数实例化的原因-即使该函数专用于其值型类型参数,其在递归参数中也是“完全多态的”,因此它根本不会操纵它,因此只需要一个编译的表示即可。
答案 2 :(得分:1)
我可以分享我在项目中使用的真实示例。
长话短说,我有一个数据结构&
,其中将类型存储为键,并且此类型与相应值的类型匹配。
要对我的库进行基准测试,我需要列出1000种类型,以检查TypeRepMap
在此数据结构中的运行速度。接下来是多态递归。
为此,我引入了以下数据类型作为类型级自然数:
lookup
使用这些数据类型,我能够实现构建所需大小的data Z
data S a
的功能。
TypeRepMap
因此,当我以buildBigMap :: forall a . Typeable a
=> Int
-> Proxy a
-> TypeRepMap
-> TypeRepMap
buildBigMap 1 x = insert x
buildBigMap n x = insert x . buildBigMap (n - 1) (Proxy @(S a))
和buildBigMap
的大小运行n
时,它在每一步都以Proxy a
和n - 1
递归调用自身,因此类型是在每一步上成长。
答案 3 :(得分:0)
Haskell Sequence容器使用内部非常规递归数据类型,该数据类型只能通过多态递归处理