forall
キーワードが次のようないわゆる「存在型」でどのように使用されるかを理解し始めています。
data ShowBox = forall s. Show s => SB s
ただし、これはforall
がどのように使用されるかのサブセットにすぎず、次のようなことでその使用を思い浮かべることはできません。
runST :: forall a. (forall s. ST s a) -> a
または、これらが異なる理由の説明:
foo :: (forall a. a -> a) -> (Char, Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))
またはRankNTypes
全体...
アカデミックな環境で普通に使われる言語の種類よりも、明確で専門用語のない英語を好む傾向があります。私がこれについて読み込もうとする説明のほとんど(検索エンジンで見つけることができるもの)には、次の問題があります。
runST
、foo
、およびbar
上記)。そう...
実際の質問に移ります。誰もがforall
キーワードを明確でわかりやすい英語で完全に説明できますか(または、どこかに存在する場合は、私が見逃したような明確な説明を指しています)。 ?
追加して編集:
以下の高品質のものから2つの傑出した答えがありましたが、残念ながら私は最高の1つしか選択できません。 ノーマンの答え は詳細かつ有用であり、forall
の理論的基盤の一部を示すと同時に物事を説明すると同時に、その実用的な意味合いをいくつか示しました。 yairchu's answer 他の誰も言及していない領域(型変数)をカバーし、コードとGHCiセッションですべての概念を説明しました。両方を最良のものとして選択することが可能であった場合、私はそうするでしょう。残念ながら私はできません。両方の答えを注意深く調べた後、コードと添付の説明のために、yairchuの方がNormanの方をわずかに上回ると判断しました。しかし、これは少し不公平です。なぜなら、forall
が型シグネチャで見たときにかすかな恐怖感を私に残さないように、これを理解するには両方の答えが本当に必要だったからです。
コード例から始めましょう:
foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b
foob postProcess onNothin onJust mval =
postProcess val
where
val :: b
val = maybe onNothin onJust mval
このコードは、プレーンなHaskell 98ではコンパイル(構文エラー)しません。forall
キーワードをサポートするには拡張機能が必要です。
基本的に、forall
キーワードには3つのdifferentの一般的な使用法(または少なくともそうseems)があり、それぞれに独自のHaskell拡張機能があります:ScopedTypeVariables
、RankNTypes
/Rank2Types
、ExistentialQuantification
。
上記のコードは、いずれかが有効になっている場合は構文エラーになりませんが、ScopedTypeVariables
が有効になっている場合の型チェックのみです。
スコープ付きタイプ変数:
スコープ付き型変数は、where
句内のコードの型を指定するのに役立ちます。 val :: b
のb
は、foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> b
のb
と同じものになります。
紛らわしい点:タイプからforall
を省略しても、実際には暗黙的にそこにあると聞くかもしれません。 ( ノーマンの答え:「通常、これらの言語は多相型からforallを省略します」 )。この主張は正しいです。butこれは、forall
の使用ではなく、ScopedTypeVariables
の他の使用を指します。
ランクNタイプ:
まず、mayb :: b -> (a -> b) -> Maybe a -> b
はmayb :: forall a b. b -> (a -> b) -> Maybe a -> b
と同等です。ScopedTypeVariables
が有効な場合はexceptです。
これは、すべてのa
およびb
に対して機能することを意味します。
このようなことをしたいとしましょう。
ghci> let putInList x = [x]
ghci> liftTup putInList (5, "Blah")
([5], ["Blah"])
このliftTup
の型は何ですか?それはliftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)
です。理由を確認するために、コーディングしてみましょう。
ghci> let liftTup liftFunc (a, b) = (liftFunc a, liftFunc b)
ghci> liftTup (\x -> [x]) (5, "Hello")
No instance for (Num [Char])
...
ghci> -- huh?
ghci> :t liftTup
liftTup :: (t -> t1) -> (t, t) -> (t1, t1)
「うーん。なぜGHCは、タプルに同じタイプの2つを含める必要があると推論するのでしょうか。そうする必要はありません」
-- test.hs
liftTup :: (x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)
ghci> :l test.hs
Couldnt match expected type 'x' against inferred type 'b'
...
うーん。したがって、GHCでは、v :: b
およびliftFunc
がv
を必要とするため、liftFunc
にx
を適用できません。本当に可能性のあるx
を受け入れる関数を関数に取得させたいのです!
{-# LANGUAGE RankNTypes #-}
liftTup :: (forall x. x -> f x) -> (a, b) -> (f a, f b)
liftTup liftFunc (t, v) = (liftFunc t, liftFunc v)
すべてのliftTup
で機能するのはx
ではなく、取得する関数です。
実在の数量化:
例を使用してみましょう:
-- test.hs
{-# LANGUAGE ExistentialQuantification #-}
data EQList = forall a. EQList [a]
eqListLen :: EQList -> Int
eqListLen (EQList x) = length x
ghci> :l test.hs
ghci> eqListLen $ EQList ["Hello", "World"]
2
Rank-N-Typesとはどう違うのですか?
ghci> :set -XRankNTypes
ghci> length (["Hello", "World"] :: forall a. [a])
Couldnt match expected type 'a' against inferred type '[Char]'
...
ランクNタイプでは、forall a
は、式がすべてのa
sに適合する必要があることを意味します。例えば:
ghci> length ([] :: forall a. [a])
0
空のリストは、あらゆるタイプのリストとして機能します。
そのため、Existential-Quantificationでは、forall
定義のdata
sは、含まれる値がcan beであるanyであるということではなく、mustであることを意味しますall適切なタイプ。
誰でも完全に、forallキーワードを明確でわかりやすい英語で説明できますか?
いいえ。(まあ、ドン・スチュワートができるかもしれません。)
以下は、単純で明確な説明またはforall
の障壁です。
それは量指定子です。普遍的または実存的な量指定子を見るには、少なくとも少しのロジック(述語計算)が必要です。述語計算を一度も見たことがないか、または量指定子に慣れていない場合(そして、博士課程の資格試験中に快適でない学生を見たことがあります)、あなたにとって、forall
の簡単な説明はありません。
これはtype量指定子です。 System F を見ておらず、ポリモーフィック型を書く練習をしている場合は、forall
が紛らわしいでしょう。 HaskellまたはMLの経験は十分ではありません。通常、これらの言語は多相型からforall
を省略します。 (私の考えでは、これは言語設計の間違いです。)
特にHaskellでは、forall
がわかりにくい方法で使用されています。 (私は型理論家ではありませんが、私の仕事は型理論のlotと接触するようになり、私はそれに非常に満足しています。)混乱は、forall
を使用して、私自身がexists
で記述したいタイプをエンコードするために使用されることです。それは、量指定子と矢印を含む型同型のトリッキーなビットによって正当化され、それを理解したいたびに、私は自分で同型を調べて解決しなければなりません。
型同型の概念に慣れていない場合、または型同型について考える練習がない場合は、forall
を使用すると混乱を招くことになります。
forall
の一般的な概念は常に同じですが(型変数を導入するためのバインディング)、さまざまな用途の詳細は大きく異なる可能性があります。非公式の英語は、バリエーションを説明するための非常に良いツールではありません。何が起こっているのかを本当に理解するには、数学が必要です。この場合、関連する数学は、Benjamin Pierceの入門テキストTypes and Programming Languagesにあります。これは非常に良い本です。
あなたの特定の例に関しては、
runST
shouldは頭を痛めます。上位のタイプ(矢印の左側)は、野生ではめったに見つかりません。 runST
を紹介した論文を読むことをお勧めします: "Lazy Functional State Threads" 。これは非常に優れた論文であり、特にrunST
の型、および一般的に上位の型について、より優れた直感を提供します。説明には数ページかかりますが、非常によくできているので、ここで要約するつもりはありません。
検討する
foo :: (forall a. a -> a) -> (Char,Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))
bar
を呼び出すと、好きなタイプa
を選択でき、タイプa
からタイプa
への関数を渡すことができます。たとえば、関数(+1)
または関数reverse
を渡すことができます。 forall
は「今すぐ型を選択できるようになった」と考えることができます。 (タイプを選択するための技術用語はinstantiatingです。)
foo
の呼び出しに関する制限はさらに厳しくなります。foo
への引数mustは多態性関数でなければなりません。そのタイプでは、foo
に渡すことができる関数は、id
またはundefined
のように常に分岐またはエラーになる関数です。理由は、foo
では、forall
が矢印の左側にあるため、foo
の呼び出し元としてa
が何であるかを選択できません。むしろ、foo
のimplementationですa
が何であるかを選択できます。 forall
は、bar
のように矢印の上ではなく、矢印の左側にあるため、インスタンス化は呼び出しサイトではなく関数の本体で行われます。
要約:forall
キーワードのcompleteの説明には数学が必要であり、学習した人だけが理解できる数学。部分的な説明であっても、数学なしでは理解するのは困難です。しかし、私の部分的な数学以外の説明が少し役立つかもしれません。 runST
のLaunchburyとPeyton Jonesを読んでください!
補遺:用語「上」、「下」、「左へ」。これらは、textual型の記述方法とは関係がなく、抽象構文ツリーと関係があります。抽象構文では、forall
が型変数の名前を取り、次にforallの下に完全な型があります。矢印は2つのタイプ(引数と結果タイプ)を取り、新しいタイプ(関数タイプ)を形成します。引数のタイプは矢印の「左側」です。これは、抽象構文ツリーの矢印の左の子です。
例:
forall a . [a] -> [a]
では、forallは矢印の上にあります。矢印の左側にあるのは[a]
です。
に
forall n f e x . (forall e x . n e x -> f -> Fact x f)
-> Block n e x -> f -> Fact x f
括弧内の型は、「矢印の左側の全体」と呼ばれます。 (作業中のオプティマイザーでこのような型を使用しています。)
私の元の答え:
誰でもforallキーワードを明確でわかりやすい英語で完全に説明できますか
ノーマンが示すように、型理論から専門用語の明確でわかりやすい英語の説明を与えることは非常に困難です。しかし、私たちは皆、試みています。
「forall」について覚えておくべきことが1つだけあります。いくつかのスコープに型をバインドします。それを理解すると、すべてがかなり簡単になります。型レベルでは「ラムダ」(または「let」の形式)と同等です-ノーマンラムジーは「左」/「上」の概念を使用して、この同じスコープの概念を 彼の優れた答え 。
'forall'のほとんどの使用法は非常にシンプルで、 GHCユーザーマニュアル、S7.8 。、特にネストされたフォームの the S7.8.5 で紹介されています。 「forall」の。
Haskellでは、次のように型が普遍的に限定されている場合、通常は型のバインダーを省略します。
length :: forall a. [a] -> Int
以下と同等です:
length :: [a] -> Int
それでおしまい。
タイプ変数をいくつかのスコープにバインドできるようになったため、最初の例のように、タイプ変数がデータ構造内でのみ表示されるトップレベル( " niversally quantified ")以外のスコープを持つことができます。これにより、隠しタイプ( " existential types ")が許可されます。または、 任意のネスト バインディング(「ランクNタイプ」)を使用できます。
型システムを深く理解するには、専門用語を学ぶ必要があります。それがコンピューターサイエンスの性質です。ただし、上記のような単純な使用は、値レベルの「let」との類推により、直感的に把握できるはずです。すばらしい紹介は LaunchburyとPeyton Jones です。
今週は、離散数学、カテゴリー理論、または抽象代数の分野で最新のものを読んだという仮定が密集しています。 (「実装の詳細については何でも論文を参照してください」という言葉を二度と読まなければ、もうすぐです。)
えー、そして単純な一次論理はどうですか? forall
は niversal quantification を参照するとかなり明確であり、その文脈では existential という用語もより意味がありますが、 exists
キーワード。定量化が事実上普遍的であるか実存的であるかは、変数が関数矢印のどちら側で使用されるかに対する量化子の配置に依存し、少し混乱します。
したがって、それが役に立たない場合、またはシンボリックロジックが気に入らない場合は、より機能的なプログラミングの観点から、型変数を(暗黙的)typeと考えることができます。関数へのパラメーター。この意味で型パラメーターをとる関数は、何らかの理由で大文字のラムダを使用して従来から記述されています。ここでは、/\
と記述します。
したがって、id
関数を検討してください。
id :: forall a. a -> a
id x = x
これをラムダとして書き換えて、「型パラメーター」を型シグネチャから移動し、インライン型注釈を追加できます。
id = /\a -> (\x -> x) :: a -> a
const
に対しても同じことが行われます。
const = /\a b -> (\x y -> x) :: a -> b -> a
したがって、bar
関数は次のようになります。
bar = /\a -> (\f -> ('t', True)) :: (a -> a) -> (Char, Bool)
引数としてbar
に与えられる関数の型は、bar
の型パラメーターに依存することに注意してください。代わりに次のようなものがあるかどうかを検討してください。
bar2 = /\a -> (\f -> (f 't', True)) :: (a -> a) -> (Char, Bool)
ここでbar2
はChar
型の関数に関数を適用しているため、bar2
にChar
以外の型パラメーターを指定すると、型エラーが発生します。
一方、次のようなfoo
は次のようになります。
foo = (\f -> (f Char 't', f Bool True))
bar
とは異なり、foo
は実際には型パラメーターをまったく取りません!itselfが型パラメーターを取る関数を受け取り、その関数を2つのdifferent型に適用します。
したがって、タイプシグネチャにforall
が表示されている場合、それをタイプシグネチャのラムダ式と考えてください。通常のラムダと同様に、forall
のスコープは可能な限り右、括弧を囲むまで拡張され、通常のラムダでバインドされた変数と同様に、forall
でバインドされた型変数は定量化された式の範囲内。
Post scriptum:多分あなたは不思議に思うかもしれません-型パラメータを取る関数について考えているのに、なぜそれらのパラメータを置くよりももっと面白いことをできないのですか?型署名に?答えは、できるということです!
型変数をラベルと組み合わせて新しい型を返す関数は、type constructorで、次のように記述できます。
Either = /\a b -> ...
しかし、Either a b
のようなそのような型の記述方法は、既に「これらのパラメーターにEither
関数を適用する」ことを示唆しているため、完全に新しい表記が必要になります。
一方、その型パラメーターで「パターン一致」の種類があり、異なる型に対して異なる値を返す関数は、型クラスのmethodです。上記の/\
構文を少し拡張すると、次のようになります。
fmap = /\ f a b -> case f of
Maybe -> (\g x -> case x of
Just y -> Just b g y
Nothing -> Nothing b) :: (a -> b) -> Maybe a -> Maybe b
[] -> (\g x -> case x of
(y:ys) -> g y : fmap [] a b g ys
[] -> [] b) :: (a -> b) -> [a] -> [b]
個人的には、Haskellの実際の構文を好むと思います...
型パラメーターを「パターン一致」し、任意の既存の型を返す関数は、type familyまたはfunctional dependent-前者の場合、すでに関数定義のように見えます。
ここに、あなたがすでによく知っていると思われる素朴な言葉での迅速で汚い説明があります。
forall
キーワードは実際にはHaskellで1つの方法でのみ使用されます。それはあなたがそれを見たときにいつも同じことを意味します。
ユニバーサル定量化
普遍的に量化されたタイプは、forall a. f a
という形式のタイプです。そのタイプの値は、引数としてtypea
を取り、valueを返す関数と考えることができます。 f a
と入力します。 Haskellでは、これらの型引数は型システムによって暗黙的に渡されることを除きます。この「関数」は、受け取る型に関係なく同じ値を提供する必要があるため、値はpolymorphicです。
たとえば、タイプforall a. [a]
を考えます。そのタイプの値は別のタイプa
を取り、その同じタイプa
の要素のリストを返します。もちろん、可能な実装は1つだけです。 a
は絶対に任意のタイプである可能性があるため、空のリストを提供する必要があります。空のリストは、その要素タイプでポリモーフィックな唯一のリスト値です(要素がないため)。
またはタイプforall a. a -> a
。このような関数の呼び出し元は、タイプa
とタイプa
の値の両方を提供します。その後、実装は同じタイプの値を返す必要がありますa
。実装は1つしかありません。与えられたのと同じ値を返さなければなりません。
実在の定量化
Haskellがその表記法をサポートしている場合、既存の数量化タイプは、exists a. f a
という形式になります。そのタイプの値は、タイプa
とタイプf a
の値で構成されるペア(または「製品」)と考えることができます。
たとえば、exists a. [a]
型の値がある場合、ある型の要素のリストがあります。どのタイプでもかまいませんが、それが何であるかわからなくても、そのようなリストに対してできることはたくさんあります。逆にしたり、要素の数をカウントしたり、要素のタイプに依存しない他のリスト操作を実行したりできます。
OK、ちょっと待ってください。 Haskellがforall
を使用して、次のような「存在」型を示すのはなぜですか?
data ShowBox = forall s. Show s => SB s
紛らわしいかもしれませんが、実際にはデータコンストラクターのタイプを説明していますSB
:
SB :: forall s. Show s => s -> ShowBox
構築されると、タイプShowBox
の値は2つのもので構成されると考えることができます。タイプs
と、タイプs
の値です。言い換えれば、それは実存的に定量化された型の値です。 Haskellがその表記法をサポートしていれば、ShowBox
は実際にexists s. Show s => s
と書くことができます。
runST
とその友達
それを考えると、これらはどう違うのですか?
foo :: (forall a. a -> a) -> (Char,Bool)
bar :: forall a. ((a -> a) -> (Char, Bool))
最初にbar
を見てみましょう。タイプa
とタイプa -> a
の関数を取り、タイプ(Char, Bool)
の値を生成します。 Int
としてa
を選択し、たとえばInt -> Int
型の関数を指定できます。ただし、foo
は異なります。 foo
の実装は、必要な型を指定した関数に渡すことができる必要があります。したがって、合理的に提供できる唯一の関数はid
です。
これで、runST
のタイプの意味に取り組むことができるはずです。
runST :: forall a. (forall s. ST s a) -> a
したがって、runST
は、a
としてどの型を指定しても、a
型の値を生成できる必要があります。そのためには、forall s. ST s a
型の引数が必要です。この引数は、内部ではforall s. s -> (a, s)
型の関数にすぎません。次に、その関数は、runST
の実装がs
として指定するタイプに関係なく、タイプ(a, s)
の値を生成できる必要があります。
OK利点は、タイプrunST
がタイプa
をまったく含むことができないという点で、s
の呼び出し元に制約を課すことです。たとえば、ST s [s]
型の値を渡すことはできません。それが実際に意味するのは、runST
の実装がs
型の値を使って自由に変更を実行できるということです。型システムは、この変異がrunST
の実装に対してローカルであることを保証します。
runST
の型は、その引数の型にforall
量指定子が含まれているため、rank-2多相型の例です。 。上記のfoo
の型もランク2です。bar
のような通常の多態型はランク1ですが、引数の型が多型である必要がある場合はランク2になります。 、独自のforall
量指定子付き。関数がランク2の引数を取る場合、そのタイプはランク3になります。一般に、ランクn
の多態性引数をとる型のランクはn + 1
です。
このキーワードのさまざまな用途がある理由は、実際には少なくとも2つの異なるタイプシステム拡張機能で使用されるためです:上位のタイプと存在。
「forall」が同時に両方の適切な構文である理由を説明しようとするのではなく、これらの2つのことを個別に読んで理解することが最善です。
Existential-Quantificationを使用すると、
forall
定義のdata
sは、含まれる値がcanであることを意味しますanyではなく、mustであることを意味します- all適切なタイプ。 - やちるの答え
forall
定義のdata
が(exists a. a)
(擬似Haskell)と同型である理由の説明は、 wikibooksの "Haskell/Existentially quantified types" にあります。
以下は、簡潔な要約です。
data T = forall a. MkT a -- an existential datatype
MkT :: forall a. a -> T -- the type of the existential constructor
MkT x
のパターンマッチング/分解の場合、x
のタイプは何ですか?
foo (MkT x) = ... -- -- what is the type of x?
x
は(forall
に記載されているように)任意のタイプにすることができるため、そのタイプは次のとおりです。
x :: exists a. a -- (pseudo-Haskell)
したがって、次は同型です。
data T = forall a. MkT a -- an existential datatype
data T = MkT (exists a. a) -- (pseudo-Haskell)
このすべての私の単純な解釈は、「forall
は本当に「すべて」を意味する」ということです。重要な違いは、forall
がdefinitionと関数applicationに与える影響です。
forall
は、値または関数のdefinitionがポリモーフィックでなければならないことを意味します。
定義されているものがポリモーフィックvalueである場合、値はすべての適切なa
に対して有効でなければならないことを意味し、これは非常に制限的です。
定義されているものが多相functionである場合、関数はすべての適切なa
に対して有効でなければならないことを意味します。関数が多相であるという理由だけではパラメーターを意味しないため、それほど限定的ではありませんappliedであることは多態的でなければなりません。つまり、関数がすべてのa
に対して有効な場合、逆にany適切なa
をappliedにすることができます。ただし、パラメーターのタイプは、関数定義で一度しか選択できません。
forall
が関数パラメーターの型(つまり、Rank2Type
)内にある場合、appliedパラメーターはtrulyポリモーフィックでなければならず、アイデアと一致する必要がありますof forall
は、definitionがポリモーフィックであることを意味します。この場合、パラメーターのタイプは、関数定義で複数回選択できます( 「および関数の実装により選択されます」、ノーマンが指摘したように )
したがって、存在するdata
定義でanya
が許可される理由は、データコンストラクターが多態性であるためですfunction:
MkT :: forall a. a -> T
mkTの種類:: a -> *
つまり、a
を関数に適用できます。たとえば、多態的なvalueとは対照的に:
valueT :: forall a. [a]
値の種類T :: a
つまり、valueTのdefinitionはポリモーフィックでなければなりません。この場合、valueT
は、すべてのタイプの空のリスト[]
として定義できます。
[] :: [t]
forall
の意味はExistentialQuantification
とRankNType
で一貫していますが、パターン一致でdata
コンストラクターを使用できるため、実存には違いがあります。 ghcユーザーガイド に記載されているとおり:
パターンマッチングの場合、各パターンマッチは、各実在型変数に対して新しい、異なる型を導入します。これらのタイプを他のタイプと統合したり、パターンマッチの範囲から逃れたりすることはできません。