C#関数式プログラミング


関数式プログラミングといえば、文法が高度に柔軟でダイナミックなLISP、Haskellという古い関数式言語を思い浮かべ、ruby、javascript、F#も関数式プログラミングの流行言語に近づいたに違いない.しかし.netがlambda式をサポートして以来、C#は命令式プログラム設計言語として関数式プログラミングにおいても遜色がない.我々はc#を用いてコードを記述する過程で,意図的に高次関数,組合せ関数,純関数キャッシュなどの思想を用い,式ツリーのようなideaも関数式プログラミング思想から来ている.そこで、次によく使われる関数式プログラミングシーンをまとめ、プログラム設計の過程でこれらの技術を柔軟に応用し、私たちの設計構想を広げ、コードの品質を高めるのに役立ちます.
一、高次関数
高次関数は一般的に、ある関数にパラメータとして関数が使用され、このような関数を高次関数と呼ぶ.このような定義によると、.netで多く使われているLINQ式、Where、Select、SelectMany、Firstなどの方法はいずれも高次関数に属しているが、私たちが自分でコードを書くときにこのような設計を使うのはいつだろうか.
例:不動産費を計算する関数を設計し、var fee=square*priceであり、面積(square)は不動産の性質によって計算方法が異なる.民用住宅、商業住宅などは異なる係数を乗じる必要があります.このようなニーズに基づいて、次の関数を設計してみましょう.
民用住宅面積:
 public Func<int,int,decimal> SquareForCivil()
        {
            return (width,hight)=>width*hight;
        }

商業住宅面積:
public Func<int, int, decimal> SquareForBusiness()
        {
            return (width, hight) => width * hight*1.2m;
        }

これらの関数には共通の署名があります.Funcです.この関数を使用して、不動産費を計算する関数を設計することができます.
public decimal PropertyFee(decimal price,int width,int hight, Func<int, int, decimal> square)
        {
            return price*square(width, hight);
        }

とてもeasyではありませんて、テストを書いてみます
[Test]
        public void Should_calculate_propertyFee_for_two_area()
        {
            //Arrange
            var calculator = new PropertyFeeCalculator();
            //Act
            var feeForBusiness= calculator.PropertyFee(2m,2, 2, calculator.SquareForBusiness());
            var feeForCivil = calculator.PropertyFee(1m, 2, 2, calculator.SquareForCivil());
            //Assert
            feeForBusiness.Should().Be(9.6m);
            feeForCivil.Should().Be(4m);
        }

二、不活性評価
C#実行プロセスでは、厳密な評価ポリシーが使用されます.厳密な評価とは、パラメータが関数に渡される前に評価されることを意味します.この解釈はまだ少しはっきりしていないのではないでしょうか.シーンを見てみましょう.現在のメモリ使用率が80%未満で、前のステップで計算した結果<100で、この条件を満たしてからタスクを実行する必要があるタスクがあります.
この要件に合致するC#コードをすぐに書くことができます.
public double MemoryUtilization()
        {
            //         
            var pcInfo = new ComputerInfo();
            var usedMem = pcInfo.TotalPhysicalMemory - pcInfo.AvailablePhysicalMemory; 
            return (double)(usedMem / Convert.ToDecimal(pcInfo.TotalPhysicalMemory));
        }

        public int BigCalculatationForFirstStep()
        {
            //     
            System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
            Console.WriteLine("big calulation");
            FirstStepExecuted = true;
            return 10;
        }

        public void NextStep(double memoryUtilization,int firstStepDistance)
        {
	   //     
            if(memoryUtilization<0.8&&firstStepDistance<100)
            {
                Console.WriteLine("Next step");
            }
        }

NextStepを実行するときにメモリ使用率と第1ステップ(関数BigCalculationForFirstStep)の計算結果を入力する必要があります.コードに示すように、第1ステップの操作は時間のかかる演算ですが、C#の厳密な評価ポリシーのため、文if(memoryUtilization<0.8&&firstStep Distance<100)では、メモリ使用率が80%を超えていても、最初のステップは、メモリ使用率が80%より大きい場合、firstStep Distanceの値は重要ではなく、計算を必要としないことは明らかです.
したがって、不活性評価とは、式または式の一部が、実際に結果を必要とする場合にのみ評価されることを意味します.この要件を高次関数で書き換えてみます.
public void NextStepWithOrderFunction(Func<double> memoryUtilization,Func<int> firstStep)
        {
            if (memoryUtilization() < 0.8 && firstStep() < 100)
            {
                Console.WriteLine("Next step");
            }
        }

コードは簡単で、関数値の代わりに関数式を使用します.if(memoryUtilization)<0.8.という文が満たされていなければ、後の関数も実行されません.マイクロソフトは.net 4.0バージョンにLazyクラスを追加しました.このようなニーズのあるシーンでこのメカニズムを使用することができます.
三、関数コリー化(Curry)
コリー化は局所套用とも呼ばれる.定義:複数のパラメータを受け入れる関数を1つの単一パラメータ(最初の関数の最初のパラメータ)を受け入れる関数に変換し、残りのパラメータを受け入れて結果を返す新しい関数を返す技術であり、ps:なぜ公式解釈はこのように迂回するのか.
このような定義を見ても理解しにくいと思いますので、curryの原理からお話しします.
2つの数を加算する関数を書きます.
public Func<int, int, int> AddTwoNumber()
        {
            return (x, y) => x + y;
        }

OK、この関数はどうやって使いますか?
var result= _curringReasoning.AddTwoNumber()(1,2);

1+2=3、呼び出しは簡単です.アップグレードが必要です.パラメータ(number)を入力し、10+入力パラメータ(number)を算出する関数が必要です.という結果になりました.誰かが言いたいと思いますが、この需要の上のコードは完全に実現できるでしょう.最初のパラメータは10に伝われば終わりではないでしょうか.ok、もしあなたがそう思っていたら、私も仕方がありません.もう一つのリロードを書いて、パラメータさえあれば、実際の状況は許されません.私たちは他の人から提供されたapiを呼び出して、リロードを追加できません.ローカル・スイートの使用シーンは一般的なシーンではありませんので、適切なシーンで適切な技術を組み合わせるのが最善の設計です.ローカル・スイートの実現を見てみましょう.
public Func<int, Func<int, int>> AddTwoNumberCurrying()
        {
            Func<int, Func<int, int>> addCurrying = x => y => x + y;
            return addCurrying;
        }

式x=>y=>x+yで得られた関数署名はFunc>であり、この関数署名は非常に明確であり、intタイプのパラメータを受信し、Funcタイプの関数を得る.この場合、次のように呼び出す.
//Act
            var curringResult = curringReasoning.AddTwoNumberCurrying()(10);
            var result = curringResult(2);

            //Assert
            result.Should().Be(12);

この文:var curringResult=curringReasoning.AddTwoNumberCurrying()は、1つのパラメータ(number)のみを受信し、10+numberを計算できる関数です.
同じ理屈で、3つの数を加算する関数:
public Func<int,int,int,int> AddThreeNumber()
        {
            return (x, y, z) => x + y + z;
        }

ローカル・スイート・バージョン:
public Func<int,Func<int,Func<int,int>>> AddThreeNumberCurrying()
        {
            Func<int, Func<int, Func<int, int>>> addCurring = x => y => z => x + y + z;
            return addCurring;
        } 

プロシージャの呼び出し:
 [Test]
        public void Three_number_add_test()
        {
            //Arrange
            var curringReasoning = new CurryingReasoning();

            //Act
            var result1 = curringReasoning.AddThreeNumber()(1, 2, 3);
            var curringResult = curringReasoning.AddThreeNumberCurrying()(1);
            var curringResult2 = curringResult(2);
            var result2 = curringResult2(3);
            
            //Assert
            result1.Should().Be(6);
            result2.Should().Be(6);
        }

関数パラメータが多くなると、手動ローカル・スイートはますます書きにくくなり、拡張メソッドを使用して自動的にローカル・スイートを使用することができます.
 public static Func<T1, Func<T2, TResult>> Curry<T1, T2, TResult>(this Func<T1, T2, TResult> func)
        {
            return x => y => func(x, y);
        }

        public static Func<T1, Func<T2, Func<T3, TResult>>> Curry<T1, T2, T3, TResult>(this Func<T1, T2, T3,TResult> func)
        {
            return x => y => z=>func(x, y,z);
        }

同様にAction<>署名の関数も自動的に適用できる
これらの拡張方法があれば、ローカルスイートを使用するとさらにeasyになります.
[Test]
        public void Should_auto_curry_two_number_add_function()
        {
            //Arrange
            var add = _curringReasoning.AddTwoNumber();
            var addCurrying = add.Curry();

            //Act
            var result = addCurrying(1)(2);

            //Assert
            result.Should().Be(3);
        }

では、局所的な使い方はここまでです.stackoverflowにはcurryingが使用するシーンと定義に関するいくつかの文章があります。、皆さんは引き続き理解することができます.
関数式プログラミングにはいくつかの重要な考え方があります.例えば、純関数のキャッシュです.純関数とは、関数の呼び出しが外部の影響を受けず、同じパラメータ呼び出しで得られる値が常に同じであることを意味します.末尾再帰、単子、コード、すなわちデータ(.netの式ツリー)一部の応用、関数の組み合わせ、これらの思想は私もまだ勉強中で、あるものはまだその最適な使用シーンを考えているので、もう総括しないで、いつかその思想を理解したら補充します.
四、設計ケース
最後に私はやはり1つのシーンを設計したいと思って、高次関数、lambdaの表現式、汎型の方法を結びつけて、私がこのような例を設計したのは今多くのフレームワーク、オープンソースのプロジェクトがすべて類似の書き方があるためで、各種の技術と思想が結合しているため、やっと表現力があってとても優雅なコードがあります.
需要:入力されたmodelの一部のフィールドに単語が含まれているかどうかを検索する単語検索器を設計します.異なるmodelには異なるフィールドがあるため、この検索には構成が必要であり、vsのスマートヒントを十分に利用することができます.
この機能には2つの方法があります
private readonly List<Func<string, bool>> _conditions; 

public WordFinder<TModel> Find<TProperty>(Func<TModel,TProperty> expression)
        {
            Func<string, bool> searchCondition = word => expression(_model).ToString().Split(' ').Contains(word);
            _conditions.Add(searchCondition);
            return this;
        }

        public bool Execute(string wordList)
        {
            return _conditions.Any(x=>x(wordList));
        }

次の操作を行います.
 [Test]
        public void Should_find_a_word()
        {
            //Arrange
            var article = new Article()
            {
                Title = "this is a title",
                Content = "this is content",
                Comment = "this is comment",
                Author = "this is author"
            };

            //Act
            var result = Finder.For(article)
                .Find(x => x.Title)
                .Find(x => x.Content)
                .Find(x => x.Comment)
                .Find(x => x.Author)
                .Execute( "content");

            //Assert
            result.Should().Be(true);
        }

このケース自体は実用性がありませんが、様々な技術の総合応用こそ意味のあるapiを設計していることがわかります.関数パラメータがExpressionタイプに変更されると、具体的な属性名などの情報も読み取ることができます.
最终语:本文は比较的によく使う関数式のプログラミングの思想を総括して、これらの设计の思想があなたのプログラミングの構想を拡充することができて、更にすばらしいコードを书くことにも有利です.