我不太精通Haskell,所以这可能是一个非常简单的问题。
Rank2Types解决什么语言限制?Haskell中的函数是否已经支持多态参数?
我不太精通Haskell,所以这可能是一个非常简单的问题。
Rank2Types解决什么语言限制?Haskell中的函数是否已经支持多态参数?
Answers:
Haskell中的函数是否已经支持多态参数?
它们确实可以,但是仅排名1。这意味着,尽管您可以编写不带此扩展名但接受不同类型参数的函数,但不能编写在同一调用中将其参数用作不同类型的函数。
例如,如果没有此扩展名,g
则无法键入以下函数,因为在的定义中使用了不同的参数类型f
:
f g = g 1 + g "lala"
注意,完全有可能将多态函数作为参数传递给另一个函数。所以类似的事情map id ["a","b","c"]
是完全合法的。但是该函数只能将其用作单态。在示例中,就好像map
使用id
type一样String -> String
。当然,您也可以传递给定类型的简单单态函数来代替id
。没有rank2types,函数将无法要求其参数必须是多态函数,因此也就无法将其用作多态函数。
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
是什么?
除非您直接学习System F,否则很难理解高级多态性,因为Haskell的设计目的是为了简单起见向您隐藏细节。
但基本上,粗略的想法是,多态类型实际上并a -> b
没有在Haskell中使用的形式。实际上,它们看起来像这样,总是带有显式量词:
id :: ∀a.a → a
id = Λt.λx:t.x
如果您不知道“∀”符号,则将其理解为“所有人”。∀x.dog(x)
表示“对于所有x,x都是狗。” “Λ”是大写lambda,用于抽象类型参数;第二行说的是id是一个接受类型t
的函数,然后返回由该类型参数化的函数。
您会看到,在系统F中,您不能立即将类似的功能应用于id
值。首先,您需要将Λ函数应用于类型,以获取要应用于值的λ函数。因此,例如:
(Λt.λx:t.x) Int 5 = (λx:Int.x) 5
= 5
标准Haskell(即Haskell 98和2010)为您简化了这一过程,因为它们没有任何类型量词,大写lambda和类型应用程序,但是GHC在分析程序进行编译时将它们放在了后台。(我相信,这些都是编译时的东西,没有运行时开销。)
但是Haskell对此的自动处理意味着它假定“∀”从不出现在函数(“→”)类型的左侧分支上。 Rank2Types
并RankNTypes
关闭这些限制,并允许您覆盖Haskell的默认插入位置规则forall
。
你为什么想做这个?因为完整的,不受限制的System F具有强大的功能,并且可以完成很多很酷的事情。例如,可以使用更高级别的类型来实现类型隐藏和模块化。以下面的rank-1类型的普通旧功能(设置场景)为例:
f :: ∀r.∀a.((a → r) → a → r) → r
使用 f
,调用者必须首先选择的类型r
和a
,然后提供结果类型的参数。所以,你可以挑选r = Int
和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
适当类型的Λ函数。这意味着get 的实现f'
选择要使用的类型a
,而不是的调用者f'
。相反,如果没有较高级别的类型,则调用者始终会选择这些类型。
这有什么用?是的,实际上,对于很多事情,但是一个想法是,您可以使用它来建模诸如面向对象的编程之类的东西,其中“对象”将一些隐藏的数据与一些对隐藏数据起作用的方法捆绑在一起。因此,例如,具有两种方法的对象-一种返回一个Int
String
可以使用以下类型实现具有,另一个返回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:对于任何想知道ExistentialTypes
GHC用途的人forall
,我相信原因是因为它在幕后使用了这种技术。
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实现的存在性类型只是普通数据类型,它也包含所有必需的类型类字典(抱歉,我找不到引用来支持此操作)。
data ApplyBox r = forall a. ApplyBox (a -> r) a
; 当您将模式匹配到时ApplyBox f x
,您将获得f :: h -> r
和x :: 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
。
路易斯·卡西利亚斯(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)
那就麻烦大了!提供f
1级多态类型
f :: ([a] -> a) -> IO ()
根本没有帮助,因为我们a
在调用时选择了类型f
,而只是将其专门化Country
并\_ -> BestAlly
再次使用我们的恶意软件。解决方案是使用等级2类型:
f :: (forall a . [a] -> a) -> IO ()
现在,我们传入的函数必须是多态的,因此\_ -> BestAlly
不会进行类型检查!实际上,没有返回没有在列表中给出的元素的函数将进行类型检查(尽管有些函数陷入无限循环或产生错误,因此永不返回)。
当然,以上是人为设计的,但是对这种技术的变型是使ST
monad安全的关键。
排名更高的类型并不像其他答案那样具有异国情调。信不信由你,许多面向对象的语言(包括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#中会显示更高级别的类型。
对于那些熟悉面向对象语言的人来说,更高级别的函数只是一个通用函数,它期望另一个通用函数作为其参数。
例如,您可以在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
功能更高。
Accept
具有1级多态类型,但这是的方法IEmployee
,其本身就是2级。如果有人给我一个IEmployee
,我可以打开它并Accept
以任何类型使用它的方法。
Visitee
介绍的类,您的示例也是等级2 。f :: Visitee e => T e
本质上是一个函数(一旦类的东西被删除)f :: (forall r. e -> Visitor e r -> r) -> T e
。Haskell 2010使您可以使用类似的类摆脱有限的rank-2多态性。