C言語の関数プログラミング


関数型プログラミングは、副作用がなく、常に与えられた入力に対して同じ出力を返す純粋な関数に依存します.このパラダイムは多くの利点を持っていますが、特にC言語では達成できません.たとえば、あなたが自分自身を書くか、あるいはそれを返す方法を使用しているのを見つけたらvoid , 次に、コードはおそらく機能しません.これは複雑なデータ構造を構築するときにしばしば起こります.

JSONを使ってレポートを書く
JSONファイルでカスタマーオーダーを保存したとしましょうOrders.json , このように:
[
  {
    "OrderID": 10248,
    "OrderDate": "1996-07-04T00:00:00",
    "ShipCountry": "France"
  },
  {
    "OrderID": 10249,
    "OrderDate": "1996-07-05T00:00:00",
    "ShipCountry": "Germany"
  },
  ...
]
私たちは、このファイルをOrder クラス
class Order
{
    public int OrderID { get; set; }
    public DateTime OrderDate { get; set; }
    public string ShipCountry { get; set; }

    public static Order[] GetOrders()
    {
        var json = File.ReadAllText("Orders.json");
        return JsonSerializer.Deserialize<Order[]>(json);
    }
}
GetOrders JSONファイルの内容をOrder S .純粋な関数ではないということに注意してください.なぜなら、それが返す命令はOrders.json (これは時間とともに変化することができます).
我々の目標は、レポートが国によってIDを収集するレポートを作成することです.レポートには次の種類があります.1
Dictionary<string /*country*/, List<int> /*order IDs*/>
特に、私たちは入力として年のリストをとる関数を書きたいと思います.そして、レポートのリストを返します.

命令型
このような関数をCで使う伝統的な方法は入れ子になったループです.
static List<Dictionary<string, List<int>>> GetReports(IList<int> years)
{
    var dicts = new List<Dictionary<string, List<int>>>();
    foreach (var year in years)
    {
        var dict = new Dictionary<string, List<int>>();
        foreach (var order in Order.GetOrders())
        {
            if (order.OrderDate.Year == year)
            {
                if (!dict.TryGetValue(order.ShipCountry, out List<int> orderIDs))
                {
                    dict[order.ShipCountry] = orderIDs = new List<int>();
                }
                orderIDs.Add(order.OrderID);
            }
        }
        dicts.Add(dict);
    }
    return dicts;
}
このアプローチは動作しますが、それぞれのレポートを一度に1つの順序で構築しているので、エラーになりがちです.への呼び出しList.Add and Dictionary.Item (つまり、角括弧を使用してキーの値を設定する)void - 彼らは純粋な関数ではない.TryGetValue その恐ろしいサインがAを去ることができるので、Cnullout 慎重に扱わなければならないパラメータ.
このバージョンのGetReports 純粋ではない.それが呼ぶからOrder.GetOrders() 直接的に、その動作もまたOrders.json .

機能版
幸いにも、純粋な機能だけを使って純粋な関数そのものを書き直すことができます.LINQは関数型プログラミングAPIの例です.
static List<Dictionary<string, List<int>>> GetReports(IList<Order> orders, IList<int> years)
    => years
        .Select(year =>
            orders
                .Where(order => order.OrderDate.Year == year)
                .GroupBy(
                    order => order.ShipCountry,
                    order => order.OrderID)
                .ToDictionary(
                    group => group.Key,
                    group => group.ToList()))
        .ToList();
我々はもはや一度にレポートを1つの順序を構築していないため、このバージョンは大きな改善です.代わりに、データフローについて考えることができます.毎年のために、我々は注文の流れを取る、我々はしたくないものをフィルタリングWhere , 国別グループ分けGroupBy , し、結果のストリームを辞書に変換するToDictionary . コントロールの流れは些細なもので、ネストされたループやif 文.
我々はまた、明示的にorders 関数へのパラメータなので、今では常に注文と年の指定されたセットの同じレポートを返すことが保証されます.これは純粋な関数になります.
var years = new int[] { 1996, 1997, 1998 };
foreach (var dict in GetReports(Order.GetOrders(), years))
{
    Console.WriteLine();
    foreach (var pair in dict)
    {
        Console.WriteLine($"{pair.Key}: {pair.Value.Count}");
    }
}
このアプローチはまた、開始時に年単位で注文をグループ化することで実装を最適化することが容易になります.したがって、各年のすべての注文を再反復する必要はありません.誰もがコメントを試してみたいですか?
つの主要な行方不明の機能のいずれかtypedefs , したがって、この型の省略形を作成する簡単な方法はありません.Cがありますusing エイリアス、しかし、彼らは貧しい代用品です.沈