我对Haskell并不十分精通,所以这可能是一个非常简单的问题。
Rank2Types解决了哪些语言限制? Haskell中的函数是否已经支持多态参数?
答案 0 :(得分:151)
除非你直接研究System F,否则很难理解更高级别的多态性,因为为简单起见,Haskell旨在隐藏你的详细信息。
但基本上,粗略的想法是多态类型并不真正具有他们在Haskell中所做的a -> b
形式;实际上,他们看起来像这样,总是用明确的量词:
id :: ∀a.a → a
id = Λt.λx:t.x
如果您不知道“∀”符号,则将其读作“for all”; ∀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)通过不使用任何这些类型量词,资本lambdas和类型应用程序来简化这一点,但GHC在分析程序进行编译时将其置于幕后。 (我相信这是所有编译时的东西,没有运行时开销。)
但是Haskell对此的自动处理意味着它假定“∀”永远不会出现在函数(“→”)类型的左侧分支上。 Rank2Types
和RankNTypes
会关闭这些限制,并允许您覆盖Haskell的默认规则,以便插入forall
的位置。
你为什么要这样做?因为完整的,不受限制的系统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
应用于相应类型的Λ-函数。这意味着 f'
的实现可以选择用于a
的类型,而不是f'
的调用者。相反,如果没有更高级别的类型,呼叫者总是选择类型。
这对什么有用?嗯,实际上对于很多事情,但有一个想法是你可以使用它来模拟面向对象编程之类的东西,其中“对象”将一些隐藏数据与一些处理隐藏数据的方法捆绑在一起。因此,例如,具有两个方法的对象(一个返回Int
而另一个返回String
)可以使用以下类型实现:
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
,其类型为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:对于任何阅读此内容的人来说,谁想知道GHC中的ExistentialTypes
如何使用forall
,我相信原因是因为它在幕后使用了这种技术。
答案 1 :(得分:110)
Haskell中的函数是否已经支持多态参数?
他们这样做,但只有1级。这意味着虽然你可以编写一个函数,在没有这个扩展的情况下接受不同类型的参数,但你不能在同一个调用中编写一个将其参数用作不同类型的函数。
例如,如果没有此扩展名,则无法输入以下函数,因为g
在f
的定义中与不同的参数类型一起使用:
f g = g 1 + g "lala"
请注意,将多态函数作为参数传递给另一个函数是完全可能的。所以像map id ["a","b","c"]
这样的东西是完全合法的。但该函数可能只将其用作单形。在示例中map
使用id
,就好像它有String -> String
类型一样。当然,您也可以传递给定类型的简单单形函数,而不是id
。如果没有rank2types,函数就无法要求其参数必须是多态函数,因此也无法将其用作多态函数。
答案 2 :(得分:40)
Luis Casillas's answer提供了很多关于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
不会打字检查!实际上,没有函数返回一个元素而不是它给出的列表将是typecheck(虽然一些函数进入无限循环或产生错误,因此永远不会返回)。
当然,上述是设计的,但这种技术的变化是使ST
monad安全的关键。
答案 3 :(得分:12)
更高等级的类型并不像其他答案那样具有异国情调。信不信由你,许多面向对象的语言(包括Java和C#!)都以它们为特色。 (当然,这些社区中没有人通过可怕的名字和#34;更高级别的类型"来了解他们。)
我要给出的示例是访问者模式的教科书实现,我在日常工作中一直使用 。这个答案并非旨在介绍访客模式;该知识为readily available elsewhere。
在这个虚构的虚构人力资源应用程序中,我们希望对可能是全职长期员工或临时承包商的员工进行操作。我首选的访问者模式变体(实际上是与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#中。
答案 4 :(得分:4)
Slides from Bryan O'Sullivan's Haskell course at Stanford帮助我了解Rank2Types
。
答案 5 :(得分:-2)
对于熟悉面向对象语言的人来说,更高级别的函数只是一个泛型函数,它希望将其作为参数的另一个泛型函数。
E.g。在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
成为更高级别的职能。