私は、Haskell、Ocaml、Elm、Rustなどの言語で記述されたFPプログラムを取得でき、エラーなしでコンパイルできるようになると、彼らがプログラムは正しく動作します。
彼らは通常、それらがFP言語の型システムによるものであることがほとんどであると述べています。それで、FP Haskell、Elmなどの言語で型システムをどうやって行うのですか? Java、Delphi、C#などの言語のものとは異なりますか?
私は後者に精通しており、関数のシグネチャとパラメーターの型が一致しない場合にコンパイラーがエラーをキャッチする方法を知っていますが、FP言語の型システムは、この種のロジックに拡張されているようです、操作、またはプロシージャの呼び出しが機能するドメイン。
誰かが良い説明をすることはできますか?
更新: Haskell型システムをまさに尊敬するもの(Javaなど)とは何ですか? を参照すると、その質問に対する答えが理解できないと言わざるを得ません。答えは、関数型プログラミングに既に精通している人を対象にしているようです。
ほとんどの場合、関数型言語がはるかに優れているのは、型システムのプログラム権限のもとをより多く配置することです。たとえば、forループの戻り値は何ですか?命令型のスタイルでは、すべての場所でforループを使用し、型チェッカーはそれらを使用してまったく支援できません。対照的に、関数型スタイルでは、forループの代わりにmap
、filter
、fold
、または再帰を使用します。これらはすべて、非常に強い型のセマンティクスを持っています。
「命令型」言語で不純な関数と命令型ループを回避した場合、タイプチェックされたプログラムが機能する傾向がある限り、「関数型」言語でプログラミングしている人と同じような経験になりますが、プログラムは、関数型のスタイルで書かれるように設計された言語。
まず第一に、Haskellは私のNo.1言語から遠く離れていますが、コンパイルするすべてが正しいと言っても過言ではないことを明確にしましょう。コンパイルしたとしても、新しいコードはしばしば奇妙なことをします。しかし:
(慣用的に書かれた)Haskellがをコンパイルし、すべての単一シナリオについて正常にテストした場合、通常はすべて可能な入力でも動作します† 正しいタイプの。そして パラメトリックポリモーフィズム のおかげで、最終的にはかなりトリッキーなタスクを実行するコードを使用することがよくありますが、分析が容易な「ダムダウン」形式でテストすることができます。型システムは、プロダクションバリアントがテストと同じように機能することを保証します。
これは、パワーまたはHindley-Milner型システムの1つの側面です。それらは、不要なときに複雑さを隠し、=必要な場合にのみ複雑さを「注入」できるようにします。タイプここでは推論が重要です。実際には、動的またはその他のダック型言語(Python、C++テンプレート)と少し似ていますが、実際のインスタンス化ディスパッチは、何らかのtype...is
によって最終的に処理されないという違いがあります。 、テンプレートの特殊化、dynamic_cast
など(これは、インスタンス化ごとに驚くほど異なる動作を引き起こす可能性があり、動的な場合は、同じ型である必要がある単一の構造体で異なる型の値を渡すと失敗します)。型付けされた強制、これは安全ではないおよび遅い(JavaScript、PHP)または恐ろしく安全でない(C)のいずれかです。代わりに
完全なパラメトリック性とは、すべてのタイプがと同じように処理される必要があることを意味します。関数forall a . [a] -> [a]
は要素の順序を変更できるだけで、要素自体を台無しにすることはできません。このような関数は、[Int] -> [Int]
として整数のみでテストした場合、したがって、保証済みどの要素が含まれていても同じように動作することが保証されています。
現在、これは原則的に、不透明な参照を単に格納することによっても可能です。 C関数
void reorder(void** ptrs, int arrLength)
また、これらの値を実際に使用することはできず、(破壊的に)並べ替えるだけです。ただし、命令型言語では、これは非常に単純な例に限定されます。あなたがすぐにさまざまな形の構造にデータを格納したい場合、1つの配列を配置するだけでは十分ではありません。しかし、これらの値をたとえばツリーに再ラップした場合、最初に具体的な型にキャストし直さないと、それらを使用してdo何かを実行することはできません–うまく追跡できているはずですが、コンパイラはそれであなたを助けません。
Haskellで、実際にdoパラメトリックコンテナーに含まれる一般的に型付けされた値を使用して何かを行う必要がある場合、これには適切に制御されたメカニズムがあります: type classes 。型クラスC
は、いくつかの単純な基本操作をオーバーロードし、メソッドがどのように連携する必要があるかが明確になるようにグループ化されます。これらの「法則」は、一般的なコード(関数など)を記述するときに使用できます。 f :: C a => [a] -> [a]
)、それが常に実際に正しく動作することを証明します。法律は非常に抽象的であるため、new型のサポートを追加するときに証明する必要があることは明らかです(常に容易ではない場合)。これらすべての基本メソッドに加えて、f
も正しく機能し、型システムから取得されます。
簡単な例、すべての比較可能な型のOrd
クラスa
:
class Ord a where
(<) :: a -> a -> Bool
...
関数sort :: Ord a => [a] -> [a]
を生成します。この場合のlawは、厳密に弱い順序付けのプロパティにすぎません。他の言語では、一般的にcommentとして並べ替え関数に追加されます。Haskellでは、これらの種類の法律は型クラスで一度言及しましたが、これにより、実行するテストの量が減少します。
これは少し手に波状になります。問題は、型は基本的に何かが適切に指定されたインターフェイスを通過するときは常に重要ですです。今では、関数型プログラミングスタイルは常にそれを行います。データを引数として明示的に渡し、結果を戻り値として取得します。何らかの形で渡され、変化し、タイプに反映されない方法でその構造を変更した非表示の状態。
そして、これは主に1つの方法で私たちの利点になります:一度正しく機能したコードは、少し変更しても突然失敗することはありません。理由は、変更(実際の状態を維持するときに実際に必要になる種類の変更)です。プロジェクト-別のライブラリへの移行など;私は愚かなプログラムの論理的誤りについて話しているのではありません、それらは主にnewcode)を書くと通常いくつかのローカル値が異なる型になる原因になります。今、それは伝統的に悪夢:
手続き型の強く型付けされた言語では、何百もの型宣言を変更する必要があります。 うまくいけば、コンパイラーは実際に正しいスポットを表示します(私の経験では、完全に無関係なスポットで間違いを表示することが多くなります!)しかし、スポットを見つけたとしても、どのような変更を行うのかを判断するのは簡単ではありません。実際に行う必要があります。多くの場合、プロセスの一部の制御フローも変更する必要があります。多くの作業と、プロセスで発生する可能性のある多くのミスです。実際に不要な作業を行うこともよくあります。エラーは正常に機能するはずの場所に表示されましたが、の後にのみ、他の場所で型を変更しました。
動的言語では、最初は問題はありません。ただ、テストスイートはおかしくなります。しかし、小さな単体テストでも実際には何が悪いのかを示すことができず、that何かが間違っているだけです。
さて、OOカプセル化はこれらの問題を回避するのにまともな仕事をしますが、かなりいくつかの定型文としばしば効率も重要です。
Hindley-Milner言語ではスマートな型推論があるため、これは必要ありません。ローカルバインディングは通常、型注釈をまったく必要とせず、コンパイラが決定します。一部のライブラリタイプが変更された場合、コンパイラはそれを考慮に入れ、all関連するローカルバインディングを変更し、新しいタイプが古いコードと実際に調整できない場合にのみエラーを表示します。同時に、明示的なトップレベルシグネチャは、エラーが実際の原因から遠く離れることを防ぎます。
参考文献:
https://github.com/Gabriel439/post-rfc/blob/master/sotu.md#maintenance