Rank2Types的目的是什么?
我不是很精通Haskell,所以这可能是一个非常简单的问题。
Rank2Types解决什么语言限制? Haskell中的函数是否已经支持多态参数?
Haskell中的函数是否已经支持多态参数?
他们这样做,但只有等级1.这意味着,虽然你可以编写一个函数,采用不同types的参数没有这个扩展,你不能写一个函数,使用它的参数作为不同的types在同一个调用。
例如,如果没有这个扩展名,下面的函数就不能被input,因为g
在f
的定义中使用了不同的参数types:
fg = g 1 + g "lala"
请注意,将多态函数作为parameter passing给另一个函数是完全可能的。 所以像map id ["a","b","c"]
是完全合法的。 但是这个函数可能只能用作单态。 在示例中, map
使用id
,就好像它有String -> String
types一样。 当然,你也可以传递给定types的简单单形函数而不是id
。 没有rank2types,函数就无法要求它的参数必须是一个多态函数,因此也没有办法将它用作多态函数。
除非您直接研究System F ,否则很难理解更高级别的多态性,因为Haskell是为了简单起见而隐藏您的细节。
但基本上,大概的想法是,多态types实际上并没有在Haskell中做的a -> b
forms; 实际上,他们看起来像这样,总是用明确的量词:
id :: ∀aa → a id = Λt.λx:tx
如果您不知道“∀”符号,则表示为“for all”。 ∀x.dog(x)
表示“对于所有的x,x是一只狗”。 “Λ”是大写lambda,用于抽象types参数; 第二行说的是,id是一个接受typest
的函数,然后返回一个被该types参数化的函数。
你看,在系统F中,你不能只是把一个像这个id
这样的函数立即应用到一个值上; 首先,您需要将Λ函数应用于某个types,以获得适用于某个值的λ函数。 举个例子:
(Λt.λx:tx) Int 5 = (λx:Int.x) 5 = 5
标准的Haskell(即Haskell 98和2010)通过不使用任何这些types的量词,大写lambdas和types的应用程序来简化这个过程,但在分析程序进行编译时,在后台GHC将其放入。 (这是所有编译时的东西,我相信,没有运行时间的开销。)
但Haskell的自动处理意味着它假定“∀”从不出现在函数(“→”)types的左侧分支上。 Rank2Types
和RankNTypes
closures了这些限制,并允许您覆盖Haskell默认的规则来插入forall
。
你为什么想做这个? 因为完整的,无限制的系统F是hella强大的,它可以做很多很酷的东西。 例如,types隐藏和模块化可以使用更高级的types来实现。 举例来说,一个普通的老式function如下rank-1types(设置场景):
f :: ∀r.∀a.((a → r) → a → r) → r
要使用f
,调用者首先必须selectr
和a
使用的types,然后提供结果types的参数。 所以你可以selectr = Int
和a = String
:
f Int String :: ((String → Int) → String → Int) → Int
但是现在将它与下面的更高级别types进行比较:
f' :: ∀r.(∀a.(a → r) → a → r) → r
这种types的函数是如何工作的? 那么,要使用它,首先你要指定哪个types用于r
。 假设我们selectInt
:
f' Int :: (∀a.(a → Int) → a → Int) → Int
但现在∀a
在函数箭头里面 ,所以你不能select什么types来使用; 您必须将f' Int
应用于适当types的Λ函数。 这意味着f'
的实现可以select用于a
types,而不是f'
的调用者 。 相反,没有更高级的types,调用者总是selecttypes。
这有什么用? 实际上,对于很多事情来说,其中一个想法是,你可以用它来模拟像面向对象编程这样的事物,其中“对象”将一些隐藏的数据与一些处理隐藏数据的方法捆绑在一起。 因此,例如,具有两个方法的对象(一个返回一个Int
,另一个返回一个String
)可以用这种types实现:
myObject :: ∀r.(∀a.(a → Int, a -> String) → a → r) → r
这个怎么用? 该对象被实现为具有隐藏typesa
一些内部数据的函数。 为了实际使用这个对象,它的客户端传入一个“callback”函数,这个对象将用这两个方法调用。 例如:
myObject String (Λa. λ(length, name):(a → Int, a → String). λobjData:a. name objData)
在这里,我们基本上是调用对象的第二个方法,其types是a → String
为未知的a
。 那么, myObject
的客户是不知道的; 但是这些客户端从签名中知道他们可以将两个函数中的任何一个应用到它,并且获得一个Int
或者一个String
。
对于一个实际的Haskell例子,下面是我自学RankNTypes
时写的代码。 这实现了一个名为ShowBox
的types,它将一些隐藏types的值与其Show
类实例捆绑在一起。 请注意,在底部的示例中,我创build了一个ShowBox
的列表,其中第一个元素由一个数字组成,第二个元素来自一个string。 由于types是使用高级types隐藏的,因此这不会违反types检查。
{-# LANGUAGE RankNTypes #-} {-# LANGUAGE ImpredicativeTypes #-} type ShowBox = forall b. (forall a. Show a => a -> b) -> b mkShowBox :: Show a => a -> ShowBox mkShowBox x = \k -> kx -- | 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:对于任何人来说,谁都想知道GHC的ExistentialTypes
是如何使用的,我相信其原因是因为它在幕后使用了这种技术。
路易斯·卡西利亚斯(Luis Casillas)的回答给出了许多关于什么是第二types的很好的信息,但是我只是扩大了他没有提到的一点。 要求参数是多态的,不仅允许它被用于多种types; 它也限制了这个函数可以对它的参数做什么,以及它如何产生结果。 也就是说,这给调用者更less的灵活性。 你为什么要这么做? 我将从一个简单的例子开始:
假设我们有一个数据types
data Country = BigEnemy | MediumEnemy | PunyEnemy | TradePartner | Ally | BestAlly
我们想写一个函数
fg = launchMissilesAt $ g [BigEnemy, MediumEnemy, PunyEnemy]
它需要一个function,应该select列表中的一个元素,然后返回一个IO
动作,在这个目标上发射导弹。 我们可以给一个简单的types:
f :: ([Country] -> Country) -> IO ()
问题是我们可能会意外跑步
f (\_ -> BestAlly)
然后我们会遇到很大的麻烦 赋予1级多态types
f :: ([a] -> a) -> IO ()
根本没有任何帮助,因为我们在调用f
时select了a
types,我们只是将它专门化到Country
并再次使用我们的恶意\_ -> BestAlly
。 解决方法是使用排名2types:
f :: (forall a . [a] -> a) -> IO ()
现在我们传入的函数需要多态,所以\_ -> BestAlly
不会inputcheck! 事实上, 没有函数返回一个不在列表中的元素会被检查(尽pipe一些函数会进入无限循环或产生错误,因此永远不会返回)。
当然,以上是人为devise的,但是这种技术的一个变种是ST
monad安全的关键。
更高级的types并不像其他答案所做的那样奇特。 不pipe你信不信,许多面向对象的语言(包括Java和C#!)都具有这些特性。 (当然,这些社区里没有人用“高级别”这个吓人的名字来认识他们)
我将要给出的例子是Visitor模式的教科书实现,我在日常工作中一直使用它。 这个答案不是作为访问者模式的介绍; 知识在其他地方 很容易 得到 。
在这个拙劣的想象中的人力资源应用中,我们希望对可能是专职长期员工或临时承包商的员工进行操作。 我最喜欢的Visitor模式的变体(实际上是与RankNTypes
相关的RankNTypes
)参数化访问者的返回types。
interface IEmployeeVisitor<T> { T Visit(PermanentEmployee e); T Visit(Contractor c); } class XmlVisitor : IEmployeeVisitor<string> { /* ... */ } class PaymentCalculator : IEmployeeVisitor<int> { /* ... */ }
重点是有不同types的访问者可以使用相同的数据。 这意味着IEmployee
必须expression什么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); } }
我想提请你注意的types。 观察IEmployeeVisitor
普遍量化它的返回types,而IEmployee
在它的Accept
方法中量化它 – 也就是说在更高的等级。 笨重地从C#转换到Haskell:
data IEmployeeVisitor r = IEmployeeVisitor { visitPermanent :: PermanentEmployee -> r, visitContractor :: Contractor -> r } newtype IEmployee = IEmployee { accept :: forall r. IEmployeeVisitor r -> r }
所以你有它。 编写包含generics方法的types时,C#中会显示更高级别的types。
来自斯坦福大学Bryan O'Sullivan的Haskell课程的幻灯片帮助我理解了Rank2Types
。
对于那些熟悉面向对象的语言的人来说,一个更高级别的函数只不过是一个普通的函数,它可以作为另一个generics函数的参数。
例如在TypeScript中,你可以写:
type WithId<T> = T & { id: number } type Identifier = <T>(obj: T) => WithId<T> type Identify = <TObj>(obj: TObj, f: Identifier) => WithId<TObj>
看看generics函数typesIdentify
如何要求typesIdentifier
的generics函数? 这使Identify
更高级别的function。