多态递归的应用

时间:2018-06-29 01:40:00

标签: haskell recursion polymorphism

通过单态化(仅单态化)在语言中实现多态性的一个限制是您失去了支持多态性递归的能力(例如,参见rust-lang #4287)。

在编程语言中支持多态递归的一些引人注目的用例是什么?我一直在尝试找到使用此功能的库/概念,到目前为止,我遇到了一个示例:

  1. 在“命名问题”中,我们希望同时进行(a)避免替换的快速捕获和(b)快速的alpha等效性检查,其中有bound库(更详细的说明{{3} }。为功能编程语言编写编译器时,这两个属性都是理想的。

为防止问题过于广泛,我正在寻找其他将多态递归应用于传统计算机科学问题(例如编写编译器的问题)的程序/库/研究论文。

我不想要的东西的例子:

  1. 答案显示了如何使用多态递归从类别理论对X进行编码,除非他们演示了如何编码X对于解决上述条件下的Y有好处。

  2. 小玩具示例,表明您可以使用多态递归来执行X,但不能没有它。

4 个答案:

答案 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)

我可以分享我在项目中使用的真实示例。

长话短说,我有一个数据结构&amp;,其中将类型存储为键,并且此类型与相应值的类型匹配。

要对我的库进行基准测试,我需要列出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 an - 1递归调用自身,因此类型是在每一步上成长。

答案 3 :(得分:0)

Haskell Sequence容器使用内部非常规递归数据类型,该数据类型只能通过多态递归处理