web-dev-qa-db-ja.com

Haskell / GHCの `forall`キーワードは何をしますか?

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全体...

アカデミックな環境で普通に使われる言語の種類よりも、明確で専門用語のない英語を好む傾向があります。私がこれについて読み込もうとする説明のほとんど(検索エンジンで見つけることができるもの)には、次の問題があります。

  1. それらは不完全です。彼らは、このキーワードの使用の一部(「実存型」など)を説明しており、まったく異なる方法で使用するコード(runSTfoo、およびbar上記)。
  2. 今週は、離散数学、カテゴリー理論、または抽象代数の分野で最新のものを読んだという仮定が密集しています。 (「実装の詳細について「論文whateverに相談する」」という言葉をもう一度読んだことがなければ、もうすぐです。)
  3. それらは、単純な概念でさえも曲がりくねったひねりのある文法とセマンティクスにしばしば変わるような方法で書かれています。

そう...

実際の質問に移ります。誰もが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拡張機能があります:ScopedTypeVariablesRankNTypes/Rank2TypesExistentialQuantification

上記のコードは、いずれかが有効になっている場合は構文エラーになりませんが、ScopedTypeVariablesが有効になっている場合の型チェックのみです。

スコープ付きタイプ変数:

スコープ付き型変数は、where句内のコードの型を指定するのに役立ちます。 val :: bbは、foob :: forall a b. (b -> b) -> b -> (a -> b) -> Maybe a -> bbと同じものになります。

紛らわしい点:タイプからforallを省略しても、実際には暗黙的にそこにあると聞くかもしれません。 ( ノーマンの答え:「通常、これらの言語は多相型からforallを省略します」 )。この主張は正しいです。butこれは、forallの使用ではなく、ScopedTypeVariablesの他の使用を指します。

ランクNタイプ:

まず、mayb :: b -> (a -> b) -> Maybe a -> bmayb :: 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およびliftFuncvを必要とするため、liftFuncxを適用できません。本当に可能性のある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は、式がすべてのasに適合する必要があることを意味します。例えば:

ghci> length ([] :: forall a. [a])
0

空のリストは、あらゆるタイプのリストとして機能します。

そのため、Existential-Quantificationでは、forall定義のdatasは、含まれる値がcan beであるanyであるということではなく、mustであることを意味しますall適切なタイプ。

247
yairchu

誰でも完全に、forallキーワードを明確でわかりやすい英語で説明できますか?

いいえ。(まあ、ドン・スチュワートができるかもしれません。)

以下は、単純で明確な説明またはforallの障壁です。

  • それは量指定子です。普遍的または実存的な量指定子を見るには、少なくとも少しのロジック(述語計算)が必要です。述語計算を一度も見たことがないか、または量指定子に慣れていない場合(そして、博士課程の資格試験中に快適でない学生を見たことがあります)、あなたにとって、forallの簡単な説明はありません。

  • これはtype量指定子です。 System F を見ておらず、ポリモーフィック型を書く練習をしている場合は、forallが紛らわしいでしょう。 HaskellまたはMLの経験は十分ではありません。通常、これらの言語は多相型からforallを省略します。 (私の考えでは、これは言語設計の間違いです。)

  • 特にHaskellでは、forallがわかりにくい方法で使用されています。 (私は型理論家ではありませんが、私の仕事は型理論のlotと接触するようになり、私はそれに非常に満足しています。)混乱は、forallを使用して、私自身がexistsで記述したいタイプをエンコードするために使用されることです。それは、量指定子と矢印を含む型同型のトリッキーなビットによって正当化され、それを理解したいたびに、私は自分で同型を調べて解決しなければなりません。

    型同型の概念に慣れていない場合、または型同型について考える練習がない場合は、forallを使用すると混乱を招くことになります。

  • forallの一般的な概念は常に同じですが(型変数を導入するためのバインディング)、さまざまな用途の詳細は大きく異なる可能性があります。非公式の英語は、バリエーションを説明するための非常に良いツールではありません。何が起こっているのかを本当に理解するには、数学が必要です。この場合、関連する数学は、Benjamin Pierceの入門テキストTypes and Programming Languagesにあります。これは非常に良い本です。

あなたの特定の例に関しては、

  • runSTshouldは頭を痛めます。上位のタイプ(矢印の左側)は、野生ではめったに見つかりません。 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が何であるかを選択できません。むしろ、fooimplementationです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
    

    括弧内の型は、「矢印の左側の全体」と呼ばれます。 (作業中のオプティマイザーでこのような型を使用しています。)

111
Norman Ramsey

私の元の答え:

誰でも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 です。

48
Don Stewart

今週は、離散数学、カテゴリー理論、または抽象代数の分野で最新のものを読んだという仮定が密集しています。 (「実装の詳細については何でも論文を参照してください」という言葉を二度と読まなければ、もうすぐです。)

えー、そして単純な一次論理はどうですか? forallniversal 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)

ここでbar2Char型の関数に関数を適用しているため、bar2Char以外の型パラメーターを指定すると、型エラーが発生します。

一方、次のような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-前者の場合、すでに関数定義のように見えます。

28
C. A. McCann

ここに、あなたがすでによく知っていると思われる素朴な言葉での迅速で汚い説明があります。

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です。

24
Apocalisp

このキーワードのさまざまな用途がある理由は、実際には少なくとも2つの異なるタイプシステム拡張機能で使用されるためです:上位のタイプと存在。

「forall」が同時に両方の適切な構文である理由を説明しようとするのではなく、これらの2つのことを個別に読んで理解することが最善です。

8
user370536

実存的実存はどうですか?

Existential-Quantificationを使用すると、forall定義のdatasは、含まれる値が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を意味します

このすべての私の単純な解釈は、「forallは本当に「すべて」を意味する」ということです。重要な違いは、foralldefinitionと関数applicationに与える影響です。

forallは、値または関数のdefinitionがポリモーフィックでなければならないことを意味します。

定義されているものがポリモーフィックvalueである場合、値はすべての適切なaに対して有効でなければならないことを意味し、これは非常に制限的です。

定義されているものが多相functionである場合、関数はすべての適切なaに対して有効でなければならないことを意味します。関数が多相であるという理由だけではパラメーターを意味しないため、それほど限定的ではありませんappliedであることは多態的でなければなりません。つまり、関数がすべてのaに対して有効な場合、逆にany適切なaappliedにすることができます。ただし、パラメーターのタイプは、関数定義で一度しか選択できません。

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の意味はExistentialQuantificationRankNTypeで一貫していますが、パターン一致でdataコンストラクターを使用できるため、実存には違いがあります。 ghcユーザーガイド に記載されているとおり:

パターンマッチングの場合、各パターンマッチは、各実在型変数に対して新しい、異なる型を導入します。これらのタイプを他のタイプと統合したり、パターンマッチの範囲から逃れたりすることはできません。

2
Louis Pan