matarillo.com

The best days are ahead of us.

7 草創期のF# -- 2002~2003

2021-09-25 00:00:21

F#の初期の構想は、OCamlの利点を.NETに、.NETの利点をOCamlに、というシンプルなものでした。強く型付けされた関数型プログラミングと.NETの融合です。 ここで「OCaml」とは、言語そのもののコアと、強く型付けされた関数型プログラミングの実用的なアプローチの両方を意味していました。 当初のタスクは比較的明確に定義されていました。 OCaml言語のコアとそのベースライブラリーの一部を、.NET Common Language Runtimeをターゲットにして再実装するというものです。 法的な問題を解消するために、OCamlのコードベースを一切使用しないという新鮮な実装を行いました。

F# 実装の最初の行は、2001年12月に書かれました。 バックエンドとしてILXをターゲットにし、Camlのコア構文を再実装したフロントエンドですので、.NETにコンパイルされます。 最初のコンパイラーはOCamlを使って書かれました(その後、2006年にはF# によるブートストラップができるようになりました)。

最初の設計選択は細かいものでした。 最も広範囲に及ぶ設計上の決定ですが、振り返ってみると見落としがちです。 OCamlを出発点として選択した後、F# の最も重要な設計選択は、.NET言語であることでした。他のすべてはその目標に従属するものでした。 特に、.NETの型はF# の型であり、.NETの値はF# の値であり、.NETの例外(とそのセマンティクス)はF# の例外(とそのセマンティクス)であり、.NETのスレッドはF# のスレッドであることです。 逆の場合も同様で、「双方向の相互運用」が常に設計目標でした。 型変換もなく、ある表現から別の表現へのマーシャリングも不要です。 F# の文字列は.NETの文字列になるでしょうし、その逆もまた同じです。 F# で定義された型や関数は、他の.NET言語からも使用することができるでしょう。

この決定は、F# がイノベーションをもたらす余地を少なくしました – 多くの場合、F# は.NETのやり方にこだわります – が、それによって双方向の相互運用が保証されました。 それが、既存の言語を.NETにマッピングするのではなく、新言語の設計を始めた大きな理由でした。 型とデータを完全に識別することは、ランタイムは1つなのか2つなのかという問題とは全く別です。 例えば整数のリストが、内部的にはある種の.NETオブジェクトとして表現されるけれども.NETメソッドに渡されるときはマーシャリングされる、というように、 統一ランタイムであっても、言語によって異なる表現を使用することで、ランタイムは1つ、表現は2つ、とすることもできました。 F# ではそのようなことはありません。1つのランタイムを使用し、可能な限り同一の表現を使用しています。これは多くの小さな決定に影響を与えました。 例えば、当初から、F# で宣言された関数は、.NETのコードで保証された安定した表現を持っていました。 つまり、安定した名前を持つクラスの静的メンバーとして、.NET言語から直接使用することができました。 これは、F# のコードは常に.NETリフレクションを介してアクセスできるという意味です。 F# の最初のバージョンは、当初「Caml for .NET」として発表されましたが、実際には常に新しい言語であり、最初から.NETのために設計されていました。 F# はどのバージョンのOCamlとも完全な互換性はありませんでしたが、互換性のあるサブセットを共有しており、Caml LightとOCamlを設計のガイダンスとインスピレーションの主要な源としていました。

さらに、「何を実装しないか」という問題もありました。 特に、OCamlの関数モジュールシステムは、設計対象から外されました。 ファンクターはStandard MLの重要な部分であり、OCamlにはその機能を修正したものが搭載されていますが、理論家の間ではいまだに論争の的になっています。 私は、プログラミング言語におけるパラメーター化のあり方の「ゴールドスタンダード」として、ファンクターに肯定的な気持ちを持っていましたが、その理論的な複雑さを警戒していました。 さらに、当時、OCamlのプログラマーが実際にファンクターを使用している場所は比較的少なかったのです。 OCamlのモジュールシステムの1つである、ネストされたモジュール定義は、最終的にF# の設計に含まれました。 しかし、ファンクターは.NETで直接実装するのは難しいと考えられており、.NETのオブジェクトプログラミングと両立する形で言語設計に含めることを正当化するのは困難でした。 もう一つの決定は、OCaml 3.0の機能、特にオブジェクトシステムや最近追加された「名前付き引数」の機能を含まないことでした。 上に挙げたLeroyのメールでは、オブジェクトシステムに関する問題点が説明されています。 .NETのオブジェクトシステムとOCamlのオブジェクトシステムの間には大きな差と不一致があり、後者を前者に使用することができませんでした。 OCamlのプリプロセッサであるCamlP4もサポートされていませんでしたが、CamlLexやCamlYaccは使用できました。 オブジェクトシステムの問題は後で説明します。 何にせよ、これはOCaml 2.0のコア言語の時点でF# とOCamlが分岐したことを意味しました。

最初のリリース(v0.1でしたが、すぐに0.5で置き換えられました)はILXプロジェクトへの追加として2002年6月4日に限りなくひっそりと行われ、プロジェクトのWebサイトには次の主張が掲載されました[Syme 2002]。

関数型/命令型の混合プログラミングは、多くのプログラミングタスクにとって素晴らしいパラダイムです。 ……F# を使って何百もの.NETライブラリーにアクセスすることができます。 ……F# は、Camlプログラミング言語のコアを.NET Framework用に実装したもので、言語横断的な拡張機能も備えています。 ……目的は、C#、Visual Basic、SML.NET、その他の.NETプログラミング言語とシームレスに連携することです。 ……MLプログラムの型や値は,いくつかの重要な言語(C#など)から,予測可能かつわかりやすい方法でアクセスすることができます。 ……F# は,OCamlライブラリーのサブセットの実装と,.NETライブラリーへのアクセス機能を提供しています。 .NETライブラリーの使用はオプションです。 ……F# は、Unicode文字列や動的リンクなど、MLの実装では不足しがちな機能をサポートしています。 ……ツールは、シンプルなコマンドラインコンパイラーで構成されており、分割コンパイル、デバッグ情報、最適化をサポートしています。 ……F# は、私の知る限り、優れたバイナリー互換性とバージョニング特性を備えた最初のMLコンパイラーです。……

途中で、いくつかのハードルが取り除かれました。 MSRはILXでコンパイルされたプログラムの商業利用を認め、この許可がF# 実装にも再適用されました。 次に、ある会議で、私はLeroyに対し、言語設計の変更を含んだ、.NET用のCaml変種を発表することにおける暗黙の承認を求めました。 Leroyは承認しました – OCaml自体、MLのコアを適応させ修正する長い歴史の一部でした – そして私たちが実験しなかった場合、研究はどうなったでしょうか? 後のEメール返信で、Leroyは次のように述べました。

Don SymeとMicrosoft Cambridgeにいる彼の同僚は、.NET Frameworkにパラメトリック多相 – 当初.NETで見過ごされていたもの – を追加する素晴らしい仕事をしました。 そして彼らがこの拡張を実際に動かしてみせるためにCamlのコアを選んだことをとても嬉しく思います。 https://caml.inria.fr/pub/ml-archives/caml-list/2002/06/8d07fd5058aa26127d1b7e7892698386.en.html

それに対する私の返信はこちらです[Syme 2001c]。

そして私のほうこそ、Xavierとチームに感謝します。 OCamlを使ったこのような素晴らしい仕事を何年にもわたってしてくれたこと、 そしてしっかりしたコア言語や、優れたランタイムシステムや、コアに追加した非常に興味深い一連の言語機能を提供してくれたことに対して。 コアCamlはあらゆる種類の仕事のための素晴らしい出発点を提供してくれます: たとえば私自身は、博士論文における定理証明のための用語言語として使いました。

私が.NET用のコアCamlコンパイラーを実装することにしたのは、部分的にはジェネリックをテストするためです。 しかしその他にも、私が.NETライブラリーを利用してプログラミングする時に、 自分がプログラミングに使いたいと思う言語を使い、自分が開発したライブラリーとテクニックを再利用したかったからでもありました。 F# について、もしかしたらCamlコミュニティには少々の不満があるかもしれませんね。 私はMicrosoft Researchにいますので、遅かれ早かれかなりの.NETコードを書くことになると思いますが、 個人的には、C# よりもCaml/F# を使いたいと思っています。 ……F# を一般公開することで、そのような機会を他の人にも利用可能にしたとしても、Camlコミュニティが受け入れてくれることを願っています。

最初の実際的な設計作業は、ドット表記法を介して.NETオブジェクト型にアクセスする機能の追加から始まりました。

C# や他の.NET言語はF# から直接アクセスできます。 ……型は Namespace.Type 記法を使ってアクセスします。 open Namespace 宣言が与えられている場合は、単に Type を使用することができます。 インスタンスメンバーは、 obj.Method(arg1, ..., argN)obj.Property 、または obj.Field を使用してアクセスされます。 静的メンバーは、Namespace.Type.Method(arg1,...,argN) または Type.Method(arg1,...,argN) を使用してアクセスされます。プロパティやフィールドも同様です。

一見無害に思えますが、この設計判断はOCamlや長い伝統を持つMLの言語設計を破りました: MLの言語設計では名前解決において推論された型情報を使います。 しかし、obj.MMのような名前は、新しい推論制約を追加するのではなく、objの部分的に推論された型を使用して即座に解決されることになりました。 つまり、型注釈が必要になることがあり、MLの伝統的な「規則」の1つ(つまり、型注釈は完全にオプションである)が損なわれ、その推論は「アルゴリズム的」すなわち「左から右」になります。

型付けについて。プログラムに型チェックをさせるために余分な注釈が必要になることがあります。 たとえば、 (cast <expr> : <type>) を使ったキャストや、型注釈を使用したオーバーロード解決などです。

私は、推論アルゴリズムが明確に定義され安定していれば、相互運用性の目的にはこれで十分であると判断しました。 結果的には、名前解決における部分的に推測された型情報の使用は効果的で安定していることが示され、F# の進化の間中ずっと維持されていました。 最終的に、型推論は言語仕様でアルゴリズム的に定義されました。

もう1つの設計上の課題は、Nullについてでした。 問題は安全性に関するものではありませんでした。.NETランタイムはJVMと同様、値がアクセスされたときにNullチェックを実行するからです。 それよりも、プログラムの正確性の問題でした。 SML.NETシステムでは、関連するすべての箇所に、標準MLが備えるSOME/NONEタグ付きの option 型を挿入することによって、すべての相互呼び出しを「サニタイズ」していました。 私は、F# ではそうしないことに決めました:

Nullについて。.NETアセンブリによって返されたNullオブジェクトは、アセンブリのインポートプロセスやF# の型システムによってチェック されません 。 この問題は将来解決される可能性がありますが、 当面の間は、Pervasivesnonnull 関数を使用して値がNullかどうかをチェックしたり、Objnull 値を使用して新しいNull値を作成したりしてください。 https://web.archive.org/web/20020814185220/http://research.microsoft.com:80/projects/ilx/fsharp-manual-import-interop.htm

代わりに採用されたルールは、.NETで宣言された型では null の使用を許可し、F# で宣言した公称型では許可しないというものでした。 これにより、OCamlに合わせて、F# のみのコード内ではNullに対する厳密なアプローチが維持されましたが、.NETの型との相互運用シナリオではNullを使用できました。 これは、人間工学による判断でもあります。 プログラミングするにあたりオプション型の挿入は非常に煩わしく、.NETのライブラリーではJavaのライブラリーほどNullが広く使われていないため、 バランスを考慮すると、プログラミング体験を快適にする必要性が相互運用におけるNull安全性の必要性を上回りました。 さらに私は、.NETのジェネリクスで行ったように、Null安全性の問題はすべての.NET言語にまたがって体系的に扱われるべきであると感じました1

当初、初期のF# ではオブジェクトプログラミングの宣言を追加することを避けました。

現在のところ、F# の中で新しいクラスを宣言することもインターフェースを実装することもできません。 当面の回避策は、仮想メンバーまたはインターフェースメンバーを実装し、デリゲートパラメーターを受け取る新しいクラスをC# で宣言し、 そのC# クラスにF# から関数値を渡すことです。 このC# クラスは、一度だけ書く必要があります。

さらに、前に示したEメールスレッドの中にあったDave Berryらによる警告に反して、スレッド処理には設計作業は必要ありませんでした。 F# は単純に.NET自体と同じスレッドモデルを想定していました。これは本質的に.NETの複数スレッドをOSの複数スレッドにマッピングしたものです。


インデックスへ戻る


  1. ふりかえりの章で取り上げていますが、この立場は最終的に2018年に結実しました。C# 9.0がついに、参照型はデフォルトで非nullを想定するという移行を始めたからです。 ↩︎