多くのプログラマーは、「関数型プログラミング(FP)は関数型言語でのみ実装されるべきだ」と暗黙的に暗示しています。 C#はオブジェクト指向言語であるため、機能的なコードを記述しようとするべきではありません。
もちろん、これは表面的な解釈です。 C#についてもう少し深い知識があり、その進化を想像すると、おそらくC#はマルチパラダイマティック(F#と同じ)であり、それがもともと命令型でオブジェクト指向であったとしても、その後はバージョンが追加され、多くの機能が追加され続けています。
だから、質問は請う:関数型プログラミングのための現在のC#言語はどれくらい良いですか? この質問に答える前に、「関数型プログラミング」の意味を説明します。 これは次のパラダイムです。
- 関数の操作に焦点を当てる
- 状態の変化を避けるのが習慣です
このスタイルでプログラミングを容易にする言語では、次のことを行う必要があります。
- 第1クラスの要素として機能をサポートします。 つまり、関数を他の値として解釈できます。たとえば、関数を引数として使用したり、他の関数の値を返したり、関数をコレクションに保存したりできます。
- すべての種類の部分的な「ローカル」置換を抑制します(または、さらに不可能にします)。変数、オブジェクト、およびデータ構造はデフォルトで不変である必要があり、オブジェクトの修正バージョンを簡単に作成できます。
- 自動的にメモリを管理します。結局のところ、適切なデータを更新するのではなく、そのような変更されたコピーを作成し、その結果、オブジェクトを増やしました。 これは、自動メモリ管理のない言語では実用的ではありません。
これらすべてを念頭に置いて、私たちは鋭い質問を投げかけます。
C#はどの程度機能しますか?
さて...見てみましょう。
1)C#の関数は実際にはファーストクラスの値です。 たとえば、
次のコード:
Func<int, int> triple = x => x * 3; var range = Enumerable.Range(1, 3); var triples = range.Select(triple); triples // => [3, 6, 9]
ここでは、関数が実際に最初のクラスの値であることがわかり、関数をトリプル変数に割り当てて、それを選択引数として設定できます。
実際、言語の最初のバージョンのC#には、ファーストクラス値としての関数のサポートがありました。これは、デリゲート型を使用して行われました。 その後、ラムダ式が導入され、この機能の構文レベルのサポートが改善されただけです。
この言語は、型推論に関していくつかの癖と制限を明らかにします(特に、引数として他の関数に複数の引数の関数を渡したい場合)。 これについては第8章で説明します。しかし、一般的に、ここではファーストクラスの値としての関数のサポートは非常に優れています。
2)理想的には、言語はローカル置換も抑制するべきです。 C#の最大の欠点は次のとおりです。 デフォルトではすべてが可変であり、プログラマーは不変性を確保するために一生懸命働く必要があります。 (デフォルトの変数が不変であるF#と比較してください。変数を変更するには、特別に可変としてマークする必要があります。)
型はどうですか? フレームワークには、文字列やDateTimeなど、いくつかの不変型がありますが、言語のユーザー定義の可変型のサポートは不十分です(ただし、以下に示すように、状況はC#で若干改善されており、将来のバージョンでも改善されるはずです)。 最後に、フレームワーク内のコレクションは変更可能ですが、不変コレクションの堅牢なライブラリが既に存在します。
3)一方、C#では、より重要な要件である自動メモリ管理が満たされています。 したがって、この言語はローカル置換を許可しないプログラミングスタイルを刺激しませんが、C#でのこのスタイルのプログラミングはガベージコレクションのため便利です。
そのため、C#では、一部(すべてではない)の関数型プログラミング手法が非常によくサポートされています。 この言語は進化しており、その中の機能的手法のサポートは改善されています。
次に、過去、現在、予見可能な将来のC#言語のいくつかの機能を検討します。関数型プログラミングのコンテキストで特に重要な機能について説明します。
LINQ機能エンティティ
C#3言語が.NET 3.5フレームワークと同時に登場したとき、本質的に関数型言語から借用された多くの可能性があることが判明しました。 それらの一部はライブラリ(
System.Linq
)に含まれ、その他の機能は拡張メソッドや式ツリーなどの特定のLINQ機能を提供または最適化しました。
LINQは、リスト(または、より一般的には、「シーケンス」、これはまさに
IEnumerable
を技術的な観点から呼び出す必要があります)で多くの一般的な操作の実装を提供します。 これらの操作の中で最も一般的なのは、表示、並べ替え、フィルタリングです。 3つすべてを表示する例を次に示します。
Enumerable.Range(1, 100). Where(i => i % 20 == 0). OrderBy(i => -i). Select(i => $”{i}%”) // => [“100%”, “80%”, “60%”, “40%”, “20%”]
Where、OrderBy、およびSelectが他の関数を引数として取り、結果のIEnumerableを変更せず、新しいIEnumerableを返すことに注意してください。上記のFPの両方の原理を示しています。
LINQを使用すると、メモリ内のオブジェクト(LINQ to Objects)だけでなく、他のさまざまなデータソース(SQLテーブルやXMLデータなど)を照会できます。 C#プログラマーは、LINQをリストとリレーショナルデータ(およびそのような情報がコードベースの大部分を占める)を操作するための標準ツールとして認識しています。 一方では、これは、機能的なライブラリAPIが何であるかについて既に少し理解していることを意味します。
一方、他のタイプで作業する場合、C#の専門家は通常、命令型のスタイルを順守し、プログラムの意図された動作を順次フロー制御命令の形式で表現します。 したがって、私が今まで見たC#コードベースのほとんどは、関数(
IEnumerable
と
IQueryable
)と命令型(他のすべて)のバカです。
したがって、C#プログラマーは、LINQなどの機能ライブラリを使用する利点を認識していますが、LINQデバイスの原理を十分に理解していないため、設計でそのような手法を個別に使用できません。
これは私の本が解決するために呼ばれている問題の一つです。
C#6およびC#7の機能
C#6とC#7はC#3ほど革新的ではありませんが、これらのバージョンは言語に多くの小さな変更をもたらします。これにより、コード作成時の使いやすさと慣用的な構文が大幅に向上します。
注 :C#6およびC#7の革新のほとんどは、機能を補完するのではなく、構文を最適化します。 したがって、C#の古いバージョンを使用している場合でも、この本で説明されているすべてのトリックを使用できます(ただし、手作業がもう少しあります)。 ただし、新機能によりコードの可読性が大幅に向上し、機能的なスタイルでプログラミングする方が快適になります。
以下のリストでこれらの機能がどのように実装されているかを検討し、FPでそれらが重要である理由を説明します。
リスト1.関数型プログラミングのコンテキストで重要なC#6およびC#7の機能
using static System.Math; <1> public class Circle { public Circle(double radius) => Radius = radius; <2> public double Radius { get; } <2> public double Circumference <3> => PI * 2 * Radius; <3> public double Area { get { double Square(double d) => Pow(d, 2); <4> return PI * Square(Radius); } } public (double Circumference, double Area) Stats <5> => (Circumference, Area); }
-
using static
をusing static
すると、以下のPI
やPow
などの静的System.Math
メンバーへのSystem.Math
なアクセスが提供されます。 -
getter-only
自動プロパティは、コンストラクターでgetter-only
設定できます - 式本体のプロパティ
- ローカル関数は、別のメソッド内で宣言されたメソッドです
- C#7タプル構文はメンバー名を許可します
「静的を使用する」を使用して静的メンバーをインポートする
C#6で
using static
を使用すると、クラス(この場合は
System.Math
クラス)の静的メンバーを「インポート」できます。 したがって、この場合、追加の資格なしで
Math
から
PI
および
Pow
メンバーを呼び出すことができます。
using static System.Math; //... public double Circumference => PI * 2 * Radius;
なぜこれが重要なのですか? FPでは、動作が入力引数のみに依存するような関数が優先されます。そのような関数はそれぞれ個別にテストし、コンテキスト外で議論できるためです(インスタンスメソッドと比較して、各実装はインスタンスメンバーに依存します)。 C#のこれらの関数は静的メソッドとして実装されるため、C#の関数ライブラリは主に静的メソッドで構成されます。
using static
を
using static
すると、そのようなライブラリを簡単に使用でき、それを悪用すると名前空間が汚染される可能性がありますが、適度に使用するとクリーンで読みやすいコードになります。
ゲッターのみの自動プロパティを備えたシンプルな不変型
Radius
などの
getter-only
自動プロパティを宣言する場合、コンパイラはフォールバックフィールドを読み取り専用で暗黙的に宣言します。 その結果、これらのプロパティの値は、コンストラクターまたはインラインでのみ割り当てることができます。
public Circle(double radius) => Radius = radius; public double Radius { get; }
C#6の
Getter-only
自動プロパティにより、不変の型を簡単に定義できます。 これはCircleクラスの例で見ることができます。1つのフィールド(
Radius
フォールバックフィールド)のみがあり、読み取り専用です。 ですから、
Circle
を作成した後、それを変更することはできません。
式の本体のメンバーを使用したより簡潔な関数
Circumference
プロパティ
Circumference
、
=>
で囲まれた通常の「命令の本体」ではなく、
=>
で始まる「式の本体」とともに宣言されます。 このコードがAreaプロパティと比較してどれだけ簡潔であるかに注目してください!
public double Circumference => PI * 2 * Radius;
FPでは、多くの単純な関数を記述するのが慣例であり、その中には単一行の関数がいっぱいです。 これらの関数は、より複雑なワークフローにコンパイルされます。 式の本体で宣言されたメソッドは、この場合、構文上のノイズを最小限に抑えます。 これは、関数を返す関数を記述しようとする場合に特に顕著です。これは、本で非常に頻繁に行われます。
式の本体に宣言を含む構文は、メソッドとプロパティのC#6で登場し、C#7ではより汎用的になり、コンストラクタ、デストラクタ、ゲッター、セッターでも使用されます。
ローカル機能
多くの単純な関数を記述する必要がある場合、これは多くの場合、1つの場所から関数が呼び出されることを意味します。 C#7では、メソッドのスコープ内でメソッドを宣言することにより、これを明示的にプログラムできます。 たとえば、Squareメソッドは
Area
ゲッターのスコープで宣言されます。
get { double Square(double d) => Pow(d, 2); return PI * Square(Radius); }
最適化されたタプル構文
おそらくこれはC#7の最も重要なプロパティです。 したがって、タプルを簡単に作成して使用でき、最も重要なことには、意味のある名前を要素に割り当てることができます。 たとえば、
Stats
プロパティは、タイプ
(double, double)
タプルを返しますが、さらにタプル要素に意味のある名前を設定し、これらの要素にアクセスできます。
public (double Circumference, double Area) Stats => (Circumference, Area);
AFでのタプルの重要性の理由は、タスクをできるだけコンパクトな機能に分割するという同じ傾向で説明されています。 1つの関数によって返され、別の関数によって入力として受け入れられる情報をキャプチャするために使用されるデータ型を取得できない場合があります。 そのような構造に対して、サブジェクト領域からの抽象化に対応しない識別型を決定することは非合理的です-そのような場合には、タプルが便利になります。
C#は将来より機能的になりますか?
2016年の初めにこの章の草案を書いたとき、開発チームに「強い関心」を呼び起こしたすべての機能は、伝統的に関数型言語に関連付けられていることに注目しました。 これらの機能には次のものがあります。
- 登録済みの型(ステンシルコードのない不変の型)
- 代数データ型(型システムへの強力な追加)
- パターンマッチング(値だけでなく、タイプなど、データの「形式」を切り替える「switch」演算子に似ています)
- 最適化されたタプル構文
しかし、私は最後のポイントだけで満足する必要がありました。 パターンマッチングは限られた範囲で実装されていますが、これまでのところ、これは関数型言語で利用可能なパターンマッチングの淡い影にすぎず、実際にはこのバージョンでは通常十分ではありません。
一方、このような機能は将来のバージョンで計画されており、関連する提案はすでに策定されています。 したがって、将来的には、C#で登録されたタイプとパターンマッチングが見られるでしょう。
そのため、C#は、機能コンポーネントがますます発音されるマルチパラダイム言語として開発を続けます。