21.2 間違いと課題
2021-09-25 00:00:21
間違いを認めるのは困難ですが、歴史的文脈では最もよく見られます。 初期の歴史から振り返ると、F# に関する最大の間違いは、.NETと当言語がオープンソースでもオープンエンジニアリングでもなかったことです。 当時の中心的な貢献者たちはこの間違いをよく理解しており、Microsoftの多くの人がオープンソースへの移行を主張していました。 簡単に言えば、ある革新的な言語がオープンソースをまだ十分に受け入れていない企業の研究室で生まれたということです: 関係者はソースドロップを通じてできる限りのことを行い、最終的に2011年から2014年にかけてオープンソースのエンジニアリングと言語設計に移行することで問題は最終的に解決されました。 この間違いの修正は、おそらくこの言語の歴史の中で最も重要な発展となるでしょう。 さらに、F# がクローズドエンジニアリングを使用しながらも2002年から2011年まで進むことができたという事実は、主にMicrosoftの意思決定者によるその品質の認識によるものです。
クローズドエンジニアリングの不幸な副作用の1つは、不連続性でした: F# への初期の貢献者のほとんどは、すぐに他の仕事に移りました。 F# はオープンソースではなかったため、過渡的であってもコードベースに貢献し続けることはできませんでした。 今日では、貢献者は自由に行き来し、比較的古いコードに関する質問にも頻繁に答えることができます。
技術的な観点からは、F# には多くの貢献があったにもかかわらず、F# 1.9以降、コア機能セットは安定しており、バイナリー互換性さえありました。 もちろん、アクティブなバグを含む、設計上の誤りはいくつかあります。 これらのうち、「静的に解決される型パラメーター(SRTP)」のメカニズムは、おそらく最もコーナーケースのいらだちを引き起こす機能です。 もともと演算子のオーバーロード専用に設計されたこのメカニズムは、F# の上級ユーザーによって、Haskellの型クラスに似た型制約メカニズムとしても使用されています。 さらに気になるのは、F# の初心者や、チームの中で「最大限の抽象化」のためにDRY(Don’t Repeat Yourself)テクニックを過剰に適用してコーディングを行おうとする人たちが、この機能を多用し、不適切に使用していることです。 しかし、複雑なSRTP制約とアルゴリズムベースの型推論との組み合わせは脆弱であり、 SRTP制約の解決におけるいくつかの誤りは、既存のコードをコーナーケースで壊すことなく修正するのは困難です。 後方互換性がそれほど問題にならないのであれば、これは問題にならないでしょう。 しかし、F# コミュニティとMicrosoft設計グループの両方から、後方互換性は非常に重視されています。
F# の設計にはOCamlのいくつかの機能が組み込まれていましたが、振り返ってみると省略しても良かったのではないかというものがあります。
一つの例は、ジェネリックな比較です:
OCamlは制約のないジェネリックな構造的演算子 =
、 <>
、 <
、 >
、 <=
、 >=
、 compare
、 min
、 max
をサポートしています。
F# 設計では、これらの演算子に “equality” および “comparable” の型制約が適用されますが、
特に、浮動小数点数の NaN のようなコーナーケースのため、ジェネリックな比較の実装は実行時に複雑になります。
これらの演算子を使用すると、パフォーマンスにも影響があります。
振り返ってみると、ジェネリックな比較機能全体がF# では省略されているか、大幅に制約されていても良かったように思えます。
F# 言語進化で繰り返されてきたテーマの1つは、対応するC# および.NET 設計要素との相互作用です。
たとえば、F# 1.9は2007年に Async<T>
を追加しました。
対照的に、.NETは2010年に Task<T>
を追加し、C# 5.0は2012年に Task<T>
の言語統合サポートを追加しました。
高い視点からは、これらはすべて「同じもの」、つまり軽量なユーザーレベルスレッドです。
しかし、2019年、すなわち執筆の時点でさえ、これらを一緒に使うのにはぎこちなさがあります。
それらを相互運用することはできます:
Async<T>
から Task<T>
を生成し、Async<T>
の中で Task<T>
を待つことができますが、それぞれに個別の利点があります
たとえば、Async<T>
を使用する場合、F# プログラマーは明示的に取り消しトークンを渡す手間が省けますし、
Task<T>
を使用すると、ヒープ割り当て量が少なく、パフォーマンスが良くなります。
F# がある機能の1つのバージョンを追加し、後にC# だけが同様の機能の修正版を追加するという緊張は、タプルでも繰り返されました:
F# は2002年に、ボックス化されるタプルを初めから持っていましたが、C# は2017年にボックス化されないタプルを追加しました。
2017年に、F# 設計チームは、ボックス化されるタプルとボックス化されないタプルの両方を許可するようにF# を調整する必要がありました。
2007年のC# 式クォートの導入も同様でした:
F# には Expr<T>
というクォートがありますが、C# に追加された式クォートはLINQの Expression<T>
であり、.NETライブラリーで広く使用されています。
C# 式のクォートは、厳密にはF# クォートよりも制限され(C# 式のみを対象とし、ステートメント形式は対象外)、より複雑ですが、事実上.NET標準です。
私の知る限りでは、他の言語が「より大きな」言語とこんなに近い距離でダンスすることはありません。
F# 設計の長期的な整合性にとって、これらの調整は細心の注意を払って行われることが重要です。
1つの小さいながらも偶然の間違いは、「逆パイプライン」演算子 f <| x
の優先順位でした。
これは、左結合の優先順位のため、f2 <| f1 <| x
と繰り返し使用することはできません。
これは意図的ではありませんでした – 優先順位は単にOCamlを踏襲しました – しかしF# で優先されるスタイルは x |> f1 |> f2
なので、修正はされませんでした。
この間違いには、読みやすいコードになりにくい逆パイプライン演算子の使用を制限するという利点があると言えます。
F# のライブラリーは、複数引数の逆パイプライン演算子 <||
と <|||
を含みますが、それらは単なるスタイル上の理由で含めるべきではありませんでした:
それらを使用したコードは非常にまれなだけでなく、不可解です。
言語設計として、F# は進化できる可能性を多く残していますし、 公式のFSSF言語設計プロセスの一部を形成する「F# 言語提案」サイトには、200以上の活発な言語提案が記録されています[FSSF Contributors 2019b]。 最も人気のある提案は、型クラスと、高カインド型パラメーターの2つです。 ただし、どちらのケースも、C# に一致する機能を追加せずに、F# にこの機能を追加することは避けたいと私は述べています。 それは部分的には、似ているけれども互換性が不十分な機能が複数生まれるというパターンの繰り返しを避けるためです。
.NET Coreに関する説明で示したように、重要な進化のステップは、C# と.NETランタイム自体の両方の設計と関連して発生する可能性があります。 一例として、F# 4.5とC# 7.2に “Span” と呼ばれる安全で高性能なメモリープリミティブが追加されました。 この機能には、F# 2.0の設計以降に存在するさまざまな小さな問題を解決するのに役立つという追加の利点がありました。