Rank2类型的目的是什么?

我对哈斯克尔并不精通,所以这可能是一个非常简单的问题。

在哈斯克尔,函数不是已经支持多态参数了吗?

15435 次浏览

哈斯克尔的函数不是已经支持多态参数了吗?

是的,但只有一级。这意味着,虽然您可以编写一个不使用此扩展就接受不同类型参数的函数,但是您不能编写一个在同一调用中将其参数作为不同类型使用的函数。

For example the following function can't be typed without this extension because g is used with different argument types in the definition of f:

f g = g 1 + g "lala"

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

除非直接研究 系统 F,否则很难理解更高级别的多态性,因为 Haskell 的设计目的是为了简单起见而隐藏它的细节。

但基本上,大致的想法是,多态类型并不真正具有哈斯克尔的 a -> b形式,实际上,它们看起来像这样,总是带有明确的量词:

id :: ∀a.a → a
id = Λt.λx:t.x

如果您不知道“ something”符号,则将其读作“ for all”; ∀x.dog(x)的意思是“ for all x,x 是一条狗。”“ Λ”是大写的 lambda,用于抽象类型参数; 第二行说的是,id 是一个接受类型 t的函数,然后返回一个由该类型参数化的函数。

你看,在系统 F 中,你不能马上把一个像 id这样的函数应用到一个值上; ,首先,你需要把 Λ-函数应用到一个类型上,为了得到一个 λ- 函数,你应用到一个值上。例如:

(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5

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

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

你为什么要这么做?因为完整的,不受限制的系统 F 是非常强大的,它可以做很多很酷的东西。例如,类型隐藏和模块化可以使用更高级别的类型来实现。举个例子,一个普通的老式函数,类型如下: 1级(设置场景) :

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

To use f, the caller first must choose what types to use for r and a, then supply an argument of the resulting type. So you could pick r = Int and a = 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应用到适当类型的 Λ-函数。这意味着 f'的实现可以为 ABC1选择使用什么类型,而不是为 f'的调用者选择使用什么类型。如果没有更高级别的类型,相反,调用者总是选择类型。

这有什么用?嗯,实际上对于很多事情来说,但是一个想法是你可以用它来建模像面向对象程序设计这样的东西,“对象”把一些隐藏的数据和一些处理隐藏数据的方法捆绑在一起。例如,具有两个方法(一个返回 Int,另一个返回 String)的对象可以用这种类型实现:

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

How does this work? The object is implemented as a function that has some internal data of hidden type a. To actually use the object, its clients pass in a "callback" function that the object will call with the two methods. For example:

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

这里我们基本上调用了对象的第二个方法,它的类型是 a → String,表示未知的 a。嗯,对于 myObject的客户端来说是未知的; 但是这些客户端确实知道,从签名来看,他们将能够将这两个函数中的任何一个应用到它,并得到一个 Int或者一个 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: for anybody reading this who's wondered how come ExistentialTypes in GHC uses forall, I believe the reason is because it's using this sort of technique behind the scenes.

路易斯 · 卡西利亚斯的回答给出了很多关于2级类型意味着什么的重要信息,但是我只是扩展了他没有涉及到的一点。要求参数具有多态性不仅允许它与多种类型一起使用; 它还限制了该函数可以对其参数做什么,以及如何生成其结果。也就是说,它为调用方 更少提供了灵活性。你为什么要这么做?我将从一个简单的例子开始:

假设我们有一个数据类型

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

我们要写一个函数

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

它需要一个函数来选择它给出的列表中的一个元素然后返回一个 IO动作发射导弹击中目标。我们可以给 f一个简单的类型:

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

The problem is that we could accidentally run

f (\_ -> BestAlly)

然后我们就有大麻烦了! 给 f一个一级多态类型

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

没有任何帮助,因为我们选择类型 a时,我们调用 f,我们只是专门为 Country和使用我们的恶意 \_ -> BestAlly再次。解决方案是使用2级类型:

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

现在我们传入的函数需要是多态的,所以 \_ -> BestAlly不会键入 check!事实上,返回一个不在给定列表中的元素的 没有功能将进行类型检查(尽管一些函数进入无限循环或产生错误,因此永远不会返回)。

The above is contrived, of course, but a variation on this technique is key to making the ST monad safe.

高级别的类型并不像其他答案所显示的那样奇特。信不信由你,许多面向对象的语言(包括 Java 和 C # !)展示它们。(当然,这些社区中没有人知道他们的名字——听起来有点吓人的“高级类型”。)

The example I'm going to give is a textbook implementation of the Visitor pattern, which I use 一直都是 in my daily work. This answer is not intended as an introduction to the visitor pattern; that knowledge is 真的 有空 其他地方.

In this fatuous imaginary HR application, we wish to operate on employees who may be full-time permanent staff or temporary contractors. My preferred variant of the Visitor pattern (and indeed the one which is relevant to RankNTypes) parameterises the visitor's return type.

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 # 中。

对于那些熟悉面向对象语言的人来说,高阶函数只是一个泛型函数,它的参数是另一个泛型函数。

例如,在 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成为一个更高级的函数。