Rank2Typesの目的は何ですか?


110

私はHaskellにはあまり慣れていないので、これは非常に簡単な質問かもしれません。

Rank2Typesはどの言語制限を解決しますか?Haskellの関数はすでに多相引数をサポートしていませんか?


基本的には、HM型システムから多形ラムダ計算(別名)へのアップグレードです。λ2/システムF。λ2では型推論が決定できないことに注意してください。
Poscat

回答:


116

Haskellの関数はすでに多相引数をサポートしていませんか?

これらは機能しますが、ランク1のみです。つまり、この拡張機能なしで異なるタイプの引数を取る関数を作成することはできますが、同じ呼び出しで異なるタイプの引数を使用する関数を作成することはできません。

たとえば、次の関数はgの定義でさまざまな引数の型と一緒に使用されるため、この拡張なしでは型指定できませんf

f g = g 1 + g "lala"

ポリモーフィック関数を引数として別の関数に渡すことは完全に可能であることに注意してください。のようなものmap id ["a","b","c"]は完全に合法です。ただし、関数はそれを単相としてのみ使用できます。この例でmapid、typeのように使用し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
Luis Casillas

166

Haskellは単純化のためにその詳細を非表示にするように設計されているため、システムFを直接研究しない限り、上位の多態性を理解するのは困難です。

しかし、基本的に、大まかな考え方は、多態型は実際にはa -> bHaskellで行うような形式を持たないということです。実際には、これらは常に明示的な量指定子を使用して次のようになります。

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

「∀」記号がわからない場合は、「for all」と読みます。∀x.dog(x)「すべてのxにとって、xは犬です」を意味します。「Λ」は大文字のラムダであり、型パラメーターの抽象化に使用されます。2行目では、idは型をt受け取る関数であり、その型でパラメーター化された関数を返します。

システムFでは、このような関数をすぐにid値に適用することはできません。まず、値に適用するλ関数を取得するために、Λ関数を型に適用する必要があります。だから例えば:

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

標準のHaskell(Haskell 98および2010)は、これらの型数量詞、大文字のラムダ、および型アプリケーションを持たないことでこれを簡素化しますが、GHCがコンパイルのためにプログラムを分析するときに裏側に配置します。(これはすべて、コンパイル時のものであり、実行時のオーバーヘッドはありません。)

しかし、Haskellがこれを自動的に処理するということは、「∀」が関数(「→」)型の左側の分岐に現れないことを前提としています。 Rank2TypesそしてRankNTypes、これらの制限をオフにして、挿入する場所のためのHaskellのデフォルトのルールを上書きすることができますforall

なぜこれをしたいのですか?制限のない完全なシステムFは非常に強力であり、多くの優れた機能を実行できるからです。たとえば、型の非表示とモジュール性は、上位の型を使用して実装できます。たとえば、次のランク1タイプの単純な古い関数(シーンを設定する)を考えてみます。

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

を使用するにfは、呼び出し元は最初にrand に使用するタイプを選択し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あるの内側にあなたはどのようなタイプのために使用することに選ぶことができないので、関数矢印af' Int適切なタイプのa関数に適用する必要があります。つまり、の実装は、の呼び出し元ではなく、f'に使用するタイプを選択するようになりますaf'。逆に、上位の型がないと、呼び出し元は常に型を選択します。

これは何に役立ちますか?まあ、実際には多くのことですが、1つのアイデアは、これを使用して、オブジェクト指向プログラミングのようなものをモデル化できるというものです。したがって、たとえば、2つのメソッド(1つはを返すメソッドIntともう1つはを返すメソッド)を持つオブジェクトは、String次のタイプで実装できます。

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

これはどのように作動しますか?オブジェクトは、隠しタイプの内部データを持つ関数として実装されますa。オブジェクトを実際に使用するために、そのクライアントは、オブジェクトが2つのメソッドで呼び出す「コールバック」関数を渡します。例えば:

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

ここでは、基本的に、オブジェクトの2番目のメソッド(型がa → Stringunknownのメソッド)を呼び出していますa。さて、myObjectのクライアントには不明です。しかし、これらのクライアントは、署名から、2つの関数のいずれかを適用して、Intまたはのいずれかを取得できることを知っていますString

実際のHaskellの例として、以下は、私が独学で書いたときに記述したコードですRankNTypes。これShowBoxは、いくつかの非表示の型の値をそのShowクラスインスタンスと一緒にバンドルすると呼ばれる型を実装します。一番下の例ではShowBox、最初の要素が数値から、2番目の要素が文字列から作成されたリストを作成していることに注意してください。上位の型を使用して型が非表示になるため、これは型チェックに違反しません。

{-# 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 '20

5
あなたが持っていた場合はexists、キーワードを、あなたは、(例えば)として実存タイプを定義することができますdata Any = Any (exists a. a)場合は、Any :: (exists a. a) -> Any。∀xP(x)→Q≡(∃xP(x))→Qを使用するとAny、タイプも持つことができforall a. a -> Anyforallキーワードがどこから来るかを結論付けることができます。GHCによって実装された実存型は、通常のデータ型であり、必要なすべての型クラスディクショナリも保持していると思います(これをバックアップするための参照が見つかりませんでした。申し訳ありません)。
Vitus

2
@Vitus:GHCの存在はタイプクラスの辞書に関連付けられていません。あなたが持つことができますdata ApplyBox r = forall a. ApplyBox (a -> r) a。にパターンマッチすると、「非表示」の制限されたタイプApplyBox f xを取得f :: h -> rします。私は右理解していれば、型クラスの辞書場合は、このようなものに翻訳されていますのようなものに変換されます。; 。x :: hhdata ShowBox = forall a. Show a => ShowBox adata ShowBox' = forall a. ShowBox' (ShowDict' a) ainstance Show ShowBox' where show (ShowBox' dict val) = show' dict valshow' :: ShowDict a -> a -> String
Luis Casillas

それは私が時間を費やさなければならない素晴らしい答えです。C#のジェネリックが提供する抽象化にも慣れすぎていると思うので、実際に理論を理解するのではなく、当然のこととして多くのことを考えていました。
Andrey Shchekin 2012

@sacundim:まあ、「すべての必要な型クラス辞書」は、何も辞書が必要なければ、まったく辞書がないことも意味します。:)私のポイントは、GHCはおそらく上位の型(つまり、提案する変換-∃xP(x)〜∀r。(∀xP(x)→r)→r)を介して存在型をエンコードしないということでした。
Vitus 2012

47

ルイス・カシージャスの回答は、ランク2タイプが何を意味するかについて多くの素晴らしい情報を提供しますが、彼がカバーしなかった1点を拡張します。引数をポリモーフィックにする必要がある場合、複数の型で使用できるだけではありません。また、その関数が引数を使用して実行できること、および関数が結果を生成する方法を制限します。つまり、発信者の柔軟性が低下します。なぜそれをしたいのですか?簡単な例から始めましょう。

データ型があるとします

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

関数を書きたい

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

これは、与えられたリストの要素の1つを選択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モナドを安全にするための鍵となります。


18

より高いランクのタイプは、他の回答が明らかにしたほどエキゾチックではありません。信じられないかもしれませんが、多くのオブジェクト指向言語(JavaやC#を含む)がそれらを備えています。(もちろん、これらのコミュニティの誰も、恐ろしい音の名前「上位のタイプ」でそれらを知りません。)

ここで紹介する例は、Visitorパターンのテキストブックの実装です。これは、毎日の作業で常に使用しています。この回答は、訪問者パターンの紹介を目的としたものではありません。その知識は他の場所ですぐに 利用でき ます

この致命的な架空のHRアプリケーションでは、常勤の正社員または臨時の請負業者である可能性のある従業員を操作したいと考えています。Visitorパターン(そして実際にに関連するパターン)の私の好ましいバリアントはRankNTypes、ビジターの戻り型をパラメーター化します。

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

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

ポイントは、戻り値のタイプが異なる多数の訪問者がすべて同じデータを操作できることです。これはIEmployeeT本来あるべき姿について意見を表明してはならないことを意味します。

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戻り値の型を普遍的に数量化するのに対しIEmployeeAcceptメソッド内で、つまりより高いランクで数量化することに注意してください。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 2017

それともベンジャミン・ホジソンのものですか?
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では、そのようなクラスを使用して、ランク2の制限された多態性を回避できます。
dfeuer

1
forall私の例では、フロートすることはできません。手元に参照はありませんが、「型クラスのスクラップ」で何かを見つけられるかもしれません。より高いランクの多態性は確かに型チェックの問題を引き起こす可能性がありますが、クラスシステムで暗黙的に制限されたソートは問題ありません。
dfeuer 2017
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.