Rank2Types的目的是什么?


Answers:


116

Haskell中的函数是否已经支持多态参数?

它们确实可以,但是仅排名1。这意味着,尽管您可以编写不带此扩展名但接受不同类型参数的函数,但不能编写在同一调用中将其参数用作不同类型的函数。

例如,如果没有此扩展名,g则无法键入以下函数,因为在的定义中使用了不同的参数类型f

f g = g 1 + g "lala"

注意,完全有可能将多态函数作为参数传递给另一个函数。所以类似的事情map id ["a","b","c"]是完全合法的。但是该函数只能将其用作单态。在示例中,就好像map使用idtype一样String -> String。当然,您也可以传递给定类型的简单单态函数来代替id。没有rank2types,函数将无法要求其参数必须是多态函数,因此也就无法将其用作多态函数。


5
要添加一些与我的答案相关的词:考虑Haskell函数f' g x y = g x + g y。其推断的等级1类型为forall a r. Num r => (a -> r) -> a -> a -> r。由于forall a在功能箭头之外,因此调用者必须首先为a; 选择一种类型。如果他们选择了Int,我们得到了f' :: forall r. Num r => (Int -> r) -> Int -> Int -> r,现在我们已经固定了g论点,因此它可以接受Int但不能String。如果启用,RankNTypes则可以f'使用type 注释forall b c r. Num r => (forall a. a -> r) -> b -> c -> r。但是,不能使用它-会g是什么?
路易斯·卡西利亚斯

166

除非您直接学习System F,否则很难理解高级多态性,因为Haskell的设计目的是为了简单起见向您隐藏细节。

但基本上,粗略的想法是,多态类型实际上并a -> b没有在Haskell中使用的形式。实际上,它们看起来像这样,总是带有显式量词:

id :: a.a  a
id = Λtx:t.x

如果您不知道“∀”符号,则将其理解为“所有人”。∀x.dog(x)表示“对于所有x,x都是狗。” “Λ”是大写lambda,用于抽象类型参数;第二行说的是id是一个接受类型t的函数,然后返回由该类型参数化的函数。

您会看到,在系统F中,您不能立即将类似的功能应用于id值。首先,您需要将Λ函数应用于类型,以获取要应用于值的λ函数。因此,例如:

tx:t.x) Int 5 = x:Int.x) 5
                  = 5

标准Haskell(即Haskell 98和2010)为您简化了这一过程,因为它们没有任何类型量词,大写lambda和类型应用程序,但是GHC在分析程序进行编译时将它们放在了后台。(我相信,这些都是编译时的东西,没有运行时开销。)

但是Haskell对此的自动处理意味着它假定“∀”从不出现在函数(“→”)类型的左侧分支上。 Rank2TypesRankNTypes关闭这些限制,并允许您覆盖Haskell的默认插入位置规则forall

你为什么想做这个?因为完整的,不受限制的System F具有强大的功能,并且可以完成很多很酷的事情。例如,可以使用更高级别的类型来实现类型隐藏和模块化。以下面的rank-1类型的普通旧功能(设置场景)为例:

f :: r.∀a.((a  r)  a  r)  r

使用 f,调用者必须首先选择的类型ra,然后提供结果类型的参数。所以,你可以挑选r = Inta = String

f Int String :: ((String  Int)  String  Int)  Int

但现在将其与以下较高级别的类型进行比较:

f' :: r.(∀a.(a  r)  a  r)  r

这种功能如何工作?好吧,要使用它,首先您要指定要使用的类型r。说我们选择Int

f' Int :: (∀a.(a  Int)  a  Int)  Int

但现在∀a里面的功能箭头,所以你不能选择什么类型的使用a; 你必须申请f' Int适当类型的Λ函数。这意味着get 的实现f'选择要使用的类型a,而不是的调用者f'。相反,如果没有较高级别的类型,则调用者始终会选择这些类型。

这有什么用?是的,实际上,对于很多事情,但是一个想法是,您可以使用它来建模诸如面向对象的编程之类的东西,其中“对象”将一些隐藏的数据与一些对隐藏数据起作用的方法捆绑在一起。因此,例如,具有两种方法的对象-一种返回一个IntString可以使用以下类型实现具有,另一个返回a 。

myObject :: r.(∀a.(a  Int, a -> String)  a  r)  r

这是如何运作的?该对象被实现为具有一些隐藏类型内部数据的函数a。为了实际使用该对象,其客户端传递了“回调”函数,该对象将使用这两种方法进行调用。例如:

myObject String a. λ(length, name):(a  Int, a  String). λobjData:a. name objData)

基本上,这里是调用对象的第二种方法,该方法的类型是a → String针对unknown的a。好吧,对myObject客户来说是未知的;但是这些客户确实从签名中知道他们将能够将这两个功能之一应用于它,并获得an Int或a String

对于实际的Haskell示例,以下是我自学时编写的代码RankNTypes。这实现了一个称为的类型ShowBox,该类型将某些隐藏类型的值与其Show类实例捆绑在一起。请注意,在底部的示例中,我列出了ShowBox其第一个元素由数字组成,第二个元素由字符串组成的列表。由于类型是通过使用较高级别的类型隐藏的,因此这不会违反类型检查。

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ImpredicativeTypes #-}

type ShowBox = forall b. (forall a. Show a => a -> b) -> b

mkShowBox :: Show a => a -> ShowBox
mkShowBox x = \k -> k x

-- | This is the key function for using a 'ShowBox'.  You pass in
-- a function @k@ that will be applied to the contents of the 
-- ShowBox.  But you don't pick the type of @k@'s argument--the 
-- ShowBox does.  However, it's restricted to picking a type that
-- implements @Show@, so you know that whatever type it picks, you
-- can use the 'show' function.
runShowBox :: forall b. (forall a. Show a => a -> b) -> ShowBox -> b
-- Expanded type:
--
--     runShowBox 
--         :: forall b. (forall a. Show a => a -> b) 
--                   -> (forall b. (forall a. Show a => a -> b) -> b)
--                   -> b
--
runShowBox k box = box k


example :: [ShowBox] 
-- example :: [ShowBox] expands to this:
--
--     example :: [forall b. (forall a. Show a => a -> b) -> b]
--
-- Without the annotation the compiler infers the following, which
-- breaks in the definition of 'result' below:
--
--     example :: forall b. [(forall a. Show a => a -> b) -> b]
--
example = [mkShowBox 5, mkShowBox "foo"]

result :: [String]
result = map (runShowBox show) example

PS:对于任何想知道ExistentialTypesGHC用途的人forall,我相信原因是因为它在幕后使用了这种技术。


2
感谢您的详尽回答!(顺便说一句,这最终也激励了我学习正确的类型理论和系统F。)
Aleksandar Dimitrov

5
如果您有exists关键字,则可以将存在性类型定义为(例如)data Any = Any (exists a. a),其中Any :: (exists a. a) -> Any。使用∀xP(x)→Q≡(∃xP(x))→Q,我们可以得出结论,Any它也可能具有类型,forall a. a -> Any而这正是forall关键字的来源。我相信,由GHC实现的存在性类型只是普通数据类型,它也包含所有必需的类型类字典(抱歉,我找不到引用来支持此操作)。
Vitus 2012年

2
@Vitus:GHC存在性与类型类词典无关。你可以有data ApplyBox r = forall a. ApplyBox (a -> r) a; 当您将模式匹配到时ApplyBox f x,您将获得f :: h -> rx :: h为“隐藏的”受限类型h。如果我的理解对不对,该类型类字典的情况下被翻译成这样的事情:data ShowBox = forall a. Show a => ShowBox a被翻译成类似data ShowBox' = forall a. ShowBox' (ShowDict' a) a; instance Show ShowBox' where show (ShowBox' dict val) = show' dict val; show' :: ShowDict a -> a -> String
路易斯·卡西利亚斯

这是一个很好的答案,我将不得不花一些时间。我想我也已经习惯了C#泛型提供的抽象,所以我认为很多都是理所当然的,而不是真正地理解理论。
Andrey Shchekin,2012年

@sacundim:好吧,“所有必需的typeclass字典”也可以表示根本不需要任何字典。:)我的观点是,GHC很可能不会通过排名更高的类型来编码存在类型(即,您建议的转换-∃xP(x)〜∀r。(∀xP(x)→r)→r)。
Vitus 2012年

47

路易斯·卡西利亚斯(Luis Casillas)的答案就等级2的含义给出了很多很好的信息,但我只谈谈他未涵盖的一点。要求参数是多态的,不仅允许它与多种类型一起使用;它还限制了该函数可以对其参数进行处理以及如何产生结果。也就是说,它使呼叫者更少的灵活性。你为什么想这么做?我将从一个简单的示例开始:

假设我们有一个数据类型

data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly

我们想写一个函数

f g = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]

它具有的功能应该是从列表中选择一个元素,然后返回IO向该目标发射导弹的动作。我们可以给出f一个简单的类型:

f :: ([Country] -> Country) -> IO ()

问题是我们可能会意外运行

f (\_ -> BestAlly)

那就麻烦大了!提供f1级多态类型

f :: ([a] -> a) -> IO ()

根本没有帮助,因为我们a在调用时选择了类型f,而只是将其专门化Country\_ -> BestAlly再次使用我们的恶意软件。解决方案是使用等级2类型:

f :: (forall a . [a] -> a) -> IO ()

现在,我们传入的函数必须是多态的,因此\_ -> BestAlly不会进行类型检查!实际上,没有返回没有在列表中给出的元素的函数将进行类型检查(尽管有些函数陷入无限循环或产生错误,因此永不返回)。

当然,以上是人为设计的,但是对这种技术的变型是使STmonad安全的关键。


18

排名更高的类型并不像其他答案那样具有异国情调。信不信由你,许多面向对象的语言(包括Java和C#!)都具有它们的功能。(当然,这些社区中没有人以可怕的名字“高级类型”来认识他们。)

我想给这个例子是一个教科书实现Visitor模式,我用的所有的时间在我的日常工作。此答案无意作为访客模式的简介;知识在其他地方容易 获得

在这种虚构的人力资源应用程序中,我们希望对可能是全职长期员工或临时承包商的员工进行操作。我首选的Visitor模式变体(实际上是与相关的模式RankNTypes)参数化了访问者的返回类型。

interface IEmployeeVisitor<T>
{
    T Visit(PermanentEmployee e);
    T Visit(Contractor c);
}

class XmlVisitor : IEmployeeVisitor<string> { /* ... */ }
class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }

关键是许多具有不同返回类型的访问者都可以对同一数据进行操作。这表示IEmployee必须对T应该做的事情不发表任何意见。

interface IEmployee
{
    T Accept<T>(IEmployeeVisitor<T> v);
}
class PermanentEmployee : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}
class Contractor : IEmployee
{
    // ...
    public T Accept<T>(IEmployeeVisitor<T> v)
    {
        return v.Visit(this);
    }
}

我希望提请您注意这些类型。观察一下IEmployeeVisitor普遍地量化了它的返回类型,而IEmployee在其Accept方法内部对其进行了量化-也就是说,它的等级更高。从C#笨拙地转换为Haskell:

data IEmployeeVisitor r = IEmployeeVisitor {
    visitPermanent :: PermanentEmployee -> r,
    visitContractor :: Contractor -> r
}

newtype IEmployee = IEmployee {
    accept :: forall r. IEmployeeVisitor r -> r
}

所以你有它。编写包含泛型方法的类型时,C#中会显示更高级别的类型。


1
我很想知道是否有人撰写过有关C#/ Java / Blub对更高级别类型的支持的文章。亲爱的读者,如果您知道任何此类资源,请发送给我!
本杰明·霍奇森


-2

对于那些熟悉面向对象语言的人来说,更高级别的函数只是一个通用函数,它期望另一个通用函数作为其参数。

例如,您可以在TypeScript中编写:

type WithId<T> = T & { id: number }
type Identifier = <T>(obj: T) => WithId<T>
type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>

看到泛型函数类型如何Identify要求该类型的泛型函数Identifier吗?这使得Identify功能更高。


这对sepp2k的答案有什么作用?
dfeuer

还是本杰明·霍奇森的?
dfeuer

1
我想你错过了霍奇森的观点。Accept具有1级多态类型,但这是的方法IEmployee,其本身就是2级。如果有人给我一个IEmployee,我可以打开它并Accept以任何类型使用它的方法。
dfeuer

1
通过Visitee介绍的类,您的示例也是等级2 。f :: Visitee e => T e本质上是一个函数(一旦类的东西被删除)f :: (forall r. e -> Visitor e r -> r) -> T e。Haskell 2010使您可以使用类似的类摆脱有限的rank-2多态性。
dfeuer

1
forall在我的示例中,您无法浮出水面。我没有参考资料,但是您很可能会在“废除类型类”中找到一些东西。更高级别的多态确实可以引入类型检查问题,但是类系统中隐式的有限排序是可以的。
dfeuer
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.