web-dev-qa-db-ja.com

Haskell型システムをまさに尊敬しているのはなぜですか(たとえば、Java)。

私は学び始めています Haskell 。私はそれに非常に慣れていないので、オンラインブックをいくつか読んで、その基本的な構成を理解するだけです。

それに慣れ親しんでいる人がよく語っている「ミーム」の1つは、全体が「コンパイルできれば機能する」*ことです。これは、型システムの強さに関連していると思います。

この点で、正確にHaskellが他の静的型付け言語よりも優れている理由を理解しようとしています。

別の言い方をすれば、Javaでは、実際にArrayList<String>()にする必要があるものを含めるために、bury ArrayList<Animal>()のような凶悪なことを行うことができると思います。ここでの厄介なことは、あなたのstringに_elephant, giraffe_などが含まれていること、そして誰かがMercedesを入力した場合-コンパイラはあなたを助けません。

Ididdo ArrayList<Animal>()の場合、その後のある時点で、私のプログラムが本当に動物ではなく、車両に関するものであると判断した場合、たとえば、_ArrayList<Animal>_を生成する関数を_ArrayList<Vehicle>_およびmy IDEを生成するように変更できます。 =コンパイルが中断しているところはどこでも教えてくれます。

私の仮定は、これがstrong型システムで意味することですが、Haskellのほうが優れている理由は明らかではありません。別の言い方をすれば、良いJavaでも悪いJavaでも書くことができます。Haskellでも同じことができると思います(つまり、実際にはファーストクラスのデータ型であるはずの文字列/ intにものを詰め込みます)。

重要な/基本的な何かが欠けていると思います。
自分のやり方の誤りを見せてくれてとても嬉しいです!

209
phatmanace

Haskellで使用できる型システム機能の順序付けされていないリストを次に示しますJava(私の知る限り、これは確かに弱いw.r.t. Javaです)

  • 安全性。 Haskellの型には、「型の安全性」という優れた特性があります。これはかなり具体的ですが、本質的には、あるタイプの値が別のタイプに変換することしかできないことを意味します。これは、可変性と矛盾する場合があります(OCamlの value制限を参照)
  • 代数的データ型。 Haskellの型は、高校の数学と基本的に同じ構造を持っています。これは途方もなくシンプルで一貫性がありますが、結局のところ、あなたが望むほど強力です。型システムの優れた基盤にすぎません。
    • Datatype-generic programming。これはジェネリック型と同じではありません( generalization を参照)。代わりに、前述のように型構造が単純であるため、その構造に対して一般的に機能するコードを書くのは比較的簡単です。後で、Equalityのようなものが、Haskellコンパイラーによってユーザー定義型から自動派生される方法について説明します。基本的に、これを行う方法は、ユーザー定義型の基礎となる一般的で単純な構造を調べ、それを値の間で一致させることです。これは、構造的平等の非常に自然な形です。
  • 相互再帰型。これは、重要な型を記述するための重要なコンポーネントにすぎません。
    • ネストされたタイプ。これにより、異なる型で再帰する変数に対して再帰型を定義できます。たとえば、平衡型ツリーの1つのタイプはdata Bt a = Here a | There (Bt (a, a))です。 Bt aの有効な値について慎重に検討し、その型がどのように機能するかに注意してください。トリッキーです!
  • 汎化。これは、型システム(あへん、あなたを見ている、Go)にはないほど愚かです。タイプ変数の概念と、その変数の選択に依存しないコードについて話す能力を持つことが重要です。 Hindley Milnerは、システムFから派生した型システムです。Haskellの型システムはHMタイピングの詳細であり、システムFは基本的にハースの一般化です。私が言いたいのは、Haskellには very good 汎化ストーリーがあるということです。
  • 抽象的なタイプ。ここでのHaskellの話は素晴らしいものではありませんが、存在しません。パブリックインターフェイスを持つがプライベート実装を持つタイプを作成することは可能です。これにより、後で実装コードへの変更を認めることができます。重要なのは、Haskellのすべての操作の基礎であるため、IOなどの明確に定義されたインターフェースを持つ「マジック」タイプを記述することです。 Java正直に言うと、おそらく実際にはより良い抽象型のストーリーがありますが、インターフェイスがより一般的になるまでは、本当にそうだったとは思いません。
  • パラメトリック性。 Haskellの値には、 any ユニバーサル演算はありません。 Javaは、参照の等価性やハッシュのようなもので、さらに強制的に強制的にこれに違反しています。これが意味することは、許可する型について自由な定理を得ることです。操作または値の意​​味をそのタイプから完全にそのタイプから完全に知ることができます---特定のタイプでは、非常に少数の住民しか存在できません。
  • より種類の多いタイプは、トリッキーなものをエンコードするときにすべてのタイプを表示します。 Functor/Applicative/Monad、Foldable/Traversable、mtlエフェクトタイピングシステム全体、一般化されたFunctorフィックスポイント。リストはどんどん続きます。上位の種類で最もよく表現されるものがたくさんあり、ユーザーがこれらのことについて話すことを可能にするタイプのシステムは比較的少数です。
  • タイプクラス。型システムをロジックとして考えると(これは便利です)、物事の証明を要求されることがよくあります。多くの場合、これは本質的にラインノイズです。正しい答えは1つしかなく、プログラマがこれを述べるのは時間と労力の無駄です。型クラスは、Haskellが証明を生成する方法です。より具体的に言えば、これにより、「どのタイプで(+)を一緒に実行するつもりですか?ああ、Integer、OK!今すぐ正しいコードをインライン化しましょう!」のような単純な「タイプ方程式システム」を解くことができます。より複雑なシステムでは、より興味深い制約を確立している可能性があります。
    • 制約計算。タイプクラスプロローグシステムに到達するためのメカニズムであるHaskellの制約は、構造的に型付けされています。これにより、 very simple 形式のサブタイプ関係が得られ、単純な制約から複雑な制約を組み立てることができます。 mtlライブラリ全体は、この考えに基づいています。
    • 導出。型クラスシステムの canonicity を駆動するには、ユーザー定義型がインスタンス化する必要がある制約を説明するために、多くの場合自明なコードを書く必要があります。 Haskell型のごく通常の構造を使用して、コンパイラーにこのボイラープレートを実行するように依頼することがしばしば可能です。
    • Type class prolog。 Haskell型クラスソルバー(前述の「証明」を生成するシステム)は、本質的に、より優れたセマンティックプロパティを備えたPrologの機能が損なわれた形式です。つまり、毛むくじゃらのものをプロローグ型にエンコードして、コンパイル時にすべて処理されることを期待できます。良い例は、順序を忘れた場合に2つの異種リストが同等であるという証明を解決することです。これらは同等の異種「セット」です。
    • マルチパラメータタイプのクラスと機能の依存関係。これらは、ベースタイプクラスプロローグの非常に便利な改良です。 Prologを知っている場合は、複数の変数の述語を記述できるときに、表現力がどの程度増加するか想像できます。
  • かなり良い推論。 Hindley Milner型システムに基づく言語には、かなり良い推論があります。 HM自体には complete 推論があります。つまり、型変数を記述する必要はありません。 Haskellの最も単純な形式であるHaskell 98は、非常にまれな状況ですでにそれを破棄しています。一般的に、最近のHaskellは、完全な推論のスペースを徐々に減らしながら、HMにさらにパワーを追加し、ユーザーが不満を言う時期を確認する実験でした。人々が文句を言うことは非常にまれです—ハスケルの推論はかなり良いです。
  • 非常に、非常に、非常に弱いサブタイピングのみ。タイプクラスプロローグの制約システムには、構造サブタイプの概念があることを前述しました。 それがHaskell でのサブタイピングの唯一の形式です。サブタイピングは推論と推論のためにひどいです。それはそれらの問題のそれぞれを著しく困難にします(平等のシステムの代わりに不平等のシステム)。また、誤解しやすくなります(サブクラス化はサブタイピングと同じですか?もちろん違います!しかし、多くの人がそれを混乱させ、多くの言語がその混乱を助長しています!どのようにここに到達したのですか?LSPを調べた人はいないと思います。)
    • Note 最近(2017年初頭)Steven Dolanが thesis on MLsub を公開しました。これはMLの一種で、Hindley-Milnerの型推論です。 very Nice subtyping story( see also )があります。これは、私が上で書いたものを取り除くものではありません。ほとんどのサブタイピングシステムは壊れており、推論が不適切ですが、完全な推論とサブタイピングをうまく組み合わせるいくつかの有望な方法を今日発見した可能性があることを示唆しています。さて、完全に明確にするために、サブタイプのJavaの概念は、Dolanのアルゴリズムとシステムを利用することができません。サブタイピングの意味を再考する必要があります。
  • 上位ランクタイプ。先に一般化について話しましたが、単なる一般化よりも、一般化された変数 /の中にある型について話すことができると便利です。たとえば、これらの構造に「含まれている」型が(forall a. f a -> g a)のような型に気づかない( parametricity を参照)高次構造間のマッピング。ストレートHMでは、この型で関数を記述できますが、上位の型では、 argument のような関数を要求します:mapFree :: (forall a . f a -> g a) -> Free f -> Free ga変数は引数内でのみバインドされることに注意してください。つまり、関数mapFree definer は、aのユーザーではなく、使用時にmapFreeがインスタンス化されるものを決定します。
  • 既存のタイプ。上位の型では普遍的な数量化について説明できますが、存在型では存在数量化について説明できます。つまり、存在いくつかの方程式を満たす未知の型が存在するだけという考えです。これは有用であり、それについてもっと長く続けるには長い時間がかかります。
  • タイプファミリー。型クラスのメカニズムは、Prologで常に考えるとは限らないため、不便な場合があります。タイプファミリーを使用すると、タイプ間の functional 関係を簡単に記述できます。
    • クローズドタイプファミリ。タイプファミリーはデフォルトでオープンですが、いつでも拡張することはできますが、成功を期待してそれらを「反転」することはできないため、煩わしいです。これは、 injectiveness を証明できないためですが、クローズドタイプファミリーでは証明できます。
  • 種類のインデックス付きタイプとタイププロモーション。この時点で私は本当にエキゾチックになっていますが、これらは時々実用的です。開いているか閉じているハンドルのタイプを記述したい場合は、非常にうまく行うことができます。次のスニペットで、Stateは非常に単純な代数型であり、その値も型レベルにプロモートされていることに注意してください。次に、Handleのような特定の kinds の引数を取ることで、Stateのような typeコンストラクターについて話します。すべての詳細を理解するのは紛らわしいですが、非常に正しいです。

    data State = Open | Closed
    
    data Handle :: State -> * -> * where
      OpenHandle :: {- something -} -> Handle Open a
      ClosedHandle :: {- something -} -> Handle Closed a
    
  • 動作するランタイムタイプの表現。 Javaは、型の消去と、一部の人々のパレードでその機能の雨が降ることで悪名高いです。型の消去 is 正しい方法は、あたかも関数getRepr :: a -> TypeReprがある場合、少なくともパラメトリック性に違反します。さらに悪いのは、それが実行時に安全でない強制をトリガーするために使用されるユーザー生成関数である場合、大規模な安全性の懸念事項。HaskellのTypeableシステムでは、安全なcoerce :: (Typeable a, Typeable b) => a -> Maybe bを作成できます。このシステムは、Typeableがコンパイラーに実装されていることに依存しており(ユーザーランドではありません)、また、 Haskellの型クラスメカニズムとそれが従うことが保証されている法則。

これらだけでなく、Haskellの型システムの価値は、型が言語をどのように記述するかにも関係しています。型システムを通じて価値を生み出すHaskellのいくつかの機能を次に示します。

  • 純度。 Haskellは、「副作用」の非常に、非常に、非常に広い定義に対して副作用を認めていません。型は入力と出力を管理し、副作用なしで型に情報を追加する必要がありますすべてを入力と出力で考慮する必要があります。
    • [〜#〜] io [〜#〜]。その後、Haskellは副作用について話す方法を必要としていました-実際のプログラムにはいくつかを含める必要があるため-型クラス、より高い種類の型、および抽象型の組み合わせにより、IO aと呼ばれる特定の超特殊型を使用するという概念が生まれましたa型の値をもたらす副作用のある計算を表します。これは、純粋な言語の内部に埋め込まれた very Niceエフェクトシステムの基盤です。
  • nullがありません。 nullが現代のプログラミング言語の10億ドルの間違いであることは誰もが知っています。代数型、特にA型をMaybe A型に変換することにより、「存在しない」状態を型に追加するだけで、nullの問題を完全に緩和できます。
  • ポリモーフィック再帰。これにより、型変数を一般化する各再帰呼び出しで異なる型で使用しても、型変数を一般化する再帰関数を定義できます。これについて話すのは難しいですが、特にネストされた型について話すのに役立ちます。以前のBt a型を振り返り、サイズを計算する関数size :: Bt a -> Intを記述してみてください。 size (Here a) = 1size (There bt) = 2 * size btのようになります。運用上はそれほど複雑ではありませんが、最後の方程式のsizeへの再帰呼び出しは異なる型で発生しますが、全体的な定義にはナイス一般化型size :: Bt a -> Intがあります。これは完全な推論を打破する機能ですが、型シグネチャを提供すると、Haskellはそれを許可します。

私は続けることができますが、このリストはあなたを始めて、そしてそれからいくつかを得るはずです。

234
J. Abrahamson
  • 完全な型推論。実際に、複雑な型を使用することができます。
  • タイプは完全に 代数 であり、複雑なアイデアを非常に簡単に表現できます。
  • Haskellには型クラスがあります。これは一種のインターフェースに似ていますが、1つの型のすべての実装を同じ場所に配置する必要がない点が異なります。ソースにアクセスしなくても、既存のサードパーティタイプの独自のタイプクラスの実装を作成できます。
  • 高次関数と再帰関数は、型チェッカーの範囲に多くの機能を組み込む傾向があります。たとえば filter を考えます。命令型言語では、同じ機能を実装するためにforループを書くことができますが、forループには戻り値の型の概念がないため、同じ静的型保証はありません。
  • サブタイプの欠如は、パラメトリック多態性を大幅に簡素化します。
  • 種類の高いタイプ(タイプのタイプ)は、Haskellで比較的簡単に指定および使用できます。これにより、Javaで完全に計り知れないタイプの抽象化を作成できます。
80
Karl Bielefeldt
a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer

Haskellの場合:整数、nullの可能性がある整数、値が外部の世界からのものである整数、および代わりに文字列の可能性がある整数は、すべて異なる型です-およびコンパイラーはこれを強制します =。これらの違いを尊重しないHaskellプログラムをコンパイルすることはできません。

(ただし、型宣言を省略できます。ほとんどの場合、コンパイラーは、変数の最も一般的な型を判別して、コンパイルを成功させることができます。それではうまくいきませんか?)

63
WolfeFan

関連SO質問

私はあなたがhaskellで同じことをできると思います(つまり、本当にファーストクラスのデータ型であるべきものを文字列/ intに詰め込みます)

いいえ、実際にはできません。少なくともJavaと同じ方法ではありません。Javaでは、次のようなことが起こります。

String x = (String)someNonString;

そしてJavaは喜んであなたの非文字列を文字列としてキャストします。Haskellはこの種のものを許可せず、ランタイムエラーのクラス全体を排除します。

nullは(Nothingのように)型システムの一部であるため、明示的に要求して処理する必要があり、他のクラスの実行時エラー全体を排除します。

他にもたくさんの微妙な利点があります-特に再利用と型クラスに関して-通信するのに十分な知識を持っている私には専門知識がありません。

しかし、ほとんどの場合、Haskellの型システムは多くの表現力を許可しているためです。ほんの少しのルールでたくさんのことができます。常に存在するHaskellツリーについて考えてみましょう。

data Tree a = Leaf a | Branch (Tree a) (Tree a) 

かなり読みやすい1行のコードで、汎用バイナリツリー全体(および2つのデータコンストラクター)を定義しました。すべていくつかのルールを使用しているだけです( 合計タイプと製品タイプ を持っています)。これは、Javaの3〜4個のコードファイルとクラスです。

特にタイプシステムを崇拝する傾向があるものの中で、この種の簡潔さ/優雅さは高く評価されています。

17
Telastyn

それに慣れ親しんでいる人がよく語っている「ミーム」の1つは、全体が「コンパイルできれば機能する」*ことです。これは、型システムの強さに関連していると思います。

これは主に小さなプログラムに当てはまります。 Haskellは、他の言語で簡単にできる間違いを防ぐ(例:Int32Word32と何かが爆発します)が、すべてのミスを防ぐことはできません。

Haskellはlotのリファクタリングを実際に容易にします。あなたのプログラムが以前は正しく、それがタイプチェックした場合、マイナーな修正の後でもそれがまだ正しいであろうかなりの可能性があります。

この点で、Haskellが他の静的型付き言語よりも優れている理由を理解しようとしています。

Haskellの型はかなり軽量であり、新しい型を宣言するのは簡単です。これは、Rustのような言語とは対照的です。この言語では、すべてが少し面倒です。

私の仮定では、これは強い型システムによって人々が意味することですが、Haskellのほうが優れている理由は明らかではありません。

Haskellには、単純な和や積タイプ以外の多くの機能があります。普遍的に数量化されたタイプ(例:id :: a -> a) 同様に。 JavaやRustなどの言語とはかなり異なる関数を含むレコードタイプを作成することもできます。

GHCは、タイプのみに基づいていくつかのインスタンスを導出することもできます。ジェネリックの登場以来、タイプ間でジェネリックな関数を作成できます。これは非常に便利であり、Javaの場合よりも流暢です。

別の違いは、Haskellは(少なくとも執筆時点では)比較的良好なタイプエラーを持つ傾向があることです。 Haskellの型推論は洗練されており、何かをコンパイルするために型注釈を提供する必要があることは非常にまれです。これはRustとは対照的です。Rustは、コンパイラーが原則的に型を推定できる場合でも、型の推論でアノテーションが必要になる場合があります。

最後に、Haskellにはタイプクラスがあり、その中に有名なモナドがあります。モナドはたまたまエラーを処理する特に良い方法です。それらは基本的に、恐ろしいデバッグをせず、タイプセーフをあきらめることなく、nullのほぼすべての便利さを提供します。したがって、これらの型にfunctionsを書き込む機能は、実際にそれらを使用するように促すときにかなり重要です!

別の言い方をすれば、良いJavaでも悪いJavaでも書くことができます。Haskellでも同じことができると思います

それは本当かもしれませんが、重要なポイントが欠けています。Haskellで足で自分を撃ち始めるポイントは、Javaで足で自分を撃ち始めるポイントよりもはるかに進んでいます。

0
user275647