web-dev-qa-db-ja.com

Rank2Typesの目的は何ですか?

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

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

104
Andrey Shchekin

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

ただし、ランク1のみです。つまり、この拡張子なしでさまざまなタイプの引数を取る関数を作成できますが、同じ呼び出しで引数をさまざまなタイプとして使用する関数を作成することはできません。

たとえば、gの定義ではfが異なる引数タイプで使用されるため、次の関数はこの拡張子なしでは入力できません。

f g = g 1 + g "lala"

多相関数を別の関数の引数として渡すことは完全に可能であることに注意してください。したがって、map id ["a","b","c"]は完全に合法です。ただし、関数は単相としてのみ使用できます。例では、mapidを使用して、タイプがString -> String。そしてもちろん、idの代わりに、与えられた型の単純な単相関数を渡すこともできます。 rank2typesがなければ、関数が引数が多相関数でなければならないことを要求する方法がないため、多相関数として使用する方法もありません。

111
sepp2k

System F を直接研究しない限り、上位のポリモーフィズムを理解するのは困難です。なぜなら、Haskellは単純化のためにその詳細をあなたから隠すように設計されているからです。

しかし、基本的に、大まかな考えは、多相型には実際にはHaskellの_a -> b_形式がないということです。実際には、常に明示的な量指定子を使用して、次のように表示されます。

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

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

System Fでは、idのような関数をすぐに値に適用することはできません。まず、値に適用するλ関数を取得するために、Λ関数を型に適用する必要があります。たとえば、次のとおりです。

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

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

しかし、Haskellのこれの自動処理は、関数( "→")タイプの左側のブランチに「∀」が表示されないことを前提としていることを意味します。 _Rank2Types_およびRankNTypesはこれらの制限を無効にし、forallを挿入する場所に関するHaskellのデフォルトルールを上書きできるようにします。

なぜこれをしたいのですか?完全で無制限のSystem Fは非常に強力であり、多くのすばらしい機能を実行できるためです。たとえば、型の非表示とモジュール性は、上位の型を使用して実装できます。たとえば、次のランク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_はinside関数矢印なので、aに使用するタイプを選択できません。 _f' Int_を適切なタイプのΛ関数に適用する必要があります。つまり、_f'_の実装は、_f'_の呼び出し元ではなく、aに使用する型を選択できます。逆に、上位の型がない場合、呼び出し元は常に型を選択します。

これは何に役立ちますか?実際、多くのことについてですが、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に対してタイプが_a → String_です。まあ、myObjectのクライアントには不明です。しかし、これらのクライアントは署名から、2つの関数のいずれかを適用でき、IntまたはStringを取得できることを知っています。

実際のHaskellの例については、RankNTypesを教えたときに書いたコードを以下に示します。これはShowBoxと呼ばれる型を実装します。これは、Showクラスインスタンスと一緒に、いくつかの隠し型の値をまとめます。一番下の例では、最初の要素が数字から作成され、2番目の要素が文字列から作成された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を使用するのか疑問に思っている人は、このようなテクニックを舞台裏で使用しているためだと思います。

155
Luis Casillas

Luis Casillasの答え は、ランク2のタイプが何を意味するかについて多くの素晴らしい情報を提供しますが、私は彼がカバーしなかった1つの点だけを拡張します。引数をポリモーフィックにすることは、複数のタイプで使用できるようにするだけではありません。また、その関数がその引数で実行できることと、その結果を生成する方法を制限します。つまり、呼び出し元にlessの柔軟性を与えます。なぜあなたはそれをしたいのですか?簡単な例から始めましょう。

データ型があるとします

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チェックを入力しません!実際、no function与えられたリストにない要素を返すと、タイプチェックが行われます(ただし、無限ループに入るかエラーを生成する関数はありませんが、リターンはそうします)。

もちろん上記は不自然ですが、この手法のバリエーションはSTモナドを安全にするための鍵です。

40
dfeuer

上位の型は、他の答えが出したほどエキゾチックではありません。信じられないかもしれませんが、多くのオブジェクト指向言語(JavaおよびC#!を含む)がそれらを備えています。タイプ"。)

これから説明する例は、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は戻り値の型を普遍的に定量化するのに対し、IEmployeeAcceptメソッド内で、つまりより高いランクで定量化することに注意してください。 C#からHaskellへの不格好な翻訳:

data IEmployeeVisitor r = IEmployeeVisitor {
    visitPermanent :: PermanentEmployee -> r,
    visitContractor :: Contractor -> r
}

newtype IEmployee = IEmployee {
    accept :: forall r. IEmployeeVisitor r -> r
}

だからあなたはそれを持っています。ジェネリックメソッドを含む型を記述すると、C#で上位の型が表示されます。

14
5
moiseev