私はHaskellにはあまり慣れていないので、これは非常に簡単な質問かもしれません。
Rank2Typesはどの言語制限を解決しますか?Haskellの関数はすでに多相引数をサポートしていませんか?
私はHaskellにはあまり慣れていないので、これは非常に簡単な質問かもしれません。
Rank2Typesはどの言語制限を解決しますか?Haskellの関数はすでに多相引数をサポートしていませんか?
回答:
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
。
Haskellは単純化のためにその詳細を非表示にするように設計されているため、システムFを直接研究しない限り、上位の多態性を理解するのは困難です。
しかし、基本的に、大まかな考え方は、多態型は実際にはa -> b
Haskellで行うような形式を持たないということです。実際には、これらは常に明示的な量指定子を使用して次のようになります。
id :: ∀a.a → a
id = Λt.λx:t.x
「∀」記号がわからない場合は、「for all」と読みます。∀x.dog(x)
「すべてのxにとって、xは犬です」を意味します。「Λ」は大文字のラムダであり、型パラメーターの抽象化に使用されます。2行目では、idは型をt
受け取る関数であり、その型でパラメーター化された関数を返します。
システムFでは、このような関数をすぐにid
値に適用することはできません。まず、値に適用するλ関数を取得するために、Λ関数を型に適用する必要があります。だから例えば:
(Λt.λx: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
は、呼び出し元は最初にr
and に使用するタイプを選択し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
適切なタイプのa関数に適用する必要があります。つまり、の実装は、の呼び出し元ではなく、f'
に使用するタイプを選択するようになりますa
f'
。逆に、上位の型がないと、呼び出し元は常に型を選択します。
これは何に役立ちますか?まあ、実際には多くのことですが、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 → String
unknownのメソッド)を呼び出しています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: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
ルイス・カシージャスの回答は、ランク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
モナドを安全にするための鍵となります。
より高いランクのタイプは、他の回答が明らかにしたほどエキゾチックではありません。信じられないかもしれませんが、多くのオブジェクト指向言語(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> { /* ... */ }
ポイントは、戻り値のタイプが異なる多数の訪問者がすべて同じデータを操作できることです。これは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では、そのようなクラスを使用して、ランク2の制限された多態性を回避できます。
forall
私の例では、フロートすることはできません。手元に参照はありませんが、「型クラスのスクラップ」で何かを見つけられるかもしれません。より高いランクの多態性は確かに型チェックの問題を引き起こす可能性がありますが、クラスシステムで暗黙的に制限されたソートは問題ありません。