LoginSignup
43

More than 5 years have passed since last update.

C#での高階関数のすゝめ

Last updated at Posted at 2016-10-10

はじめに

 高階関数とは、引数や戻り値に関数を使う関数のことです。
 関数型言語では当たり前のように使われていますが、手続き型言語ではそれほど使用されていないように感じます。さらにいうと言語によっては高階関数そのものが実装されていない場合もあります。
 手続き型言語になれた人からしたら、高階関数の書き方には癖があると思うのですが、一度覚えると表現の幅が広がるので覚えておいて損はない考え方です。

 今回はC#の例で話を進めていきます。C#ではデリゲートと呼ばれる関数を格納するオブジェクトを用いて高階関数を書けるようになっています。C#ではC言語の関数にあたるもののことをメソッドと言うので、コードの説明には「関数」でなく「メソッド」と書いていますが意味合いは同じです。
 記述してあるコードですが動作確認を一切していません。そのままでは動かないかもしれないのであしからず。
 みんなが好きなLinqの話題は出てきません。

高階関数の何がそんなに便利なの?

 高階関数を用いる最大の利点は、処理のネストを浅くすることができることです。

無題.png
 このような構造になっているクラスたちを…
 
 
無題.png
 こんな感じに、フラットにできます。

利用例

 上の図だけだと何が嬉しいのかわからないので、実際にどのような利用例があるかを2つほど書いておきます。

リトライ処理で高階関数を用いるときのパターン

 ファイル処理やwebサーバへのアクセスといった処理は、特に理由もなく失敗することがあるので、リトライをかけることがあります。
 そのような場合に高階関数を用いることによって、リトライが必要な処理と、リトライを管理する処理の2つを分けて書くことができます。

 では実際に、高階関数を用いた場合と用いない場合ではどう違うかを見ていきたいと思います。

高階関数を用いない場合

        // メソッド利用部分
        static void Main(string[] args)
        {
            FileCopyRetry(sorcePass: "sorce/abc.txt", destPass: "dest/abc.txt", retryCounts: 5, waitMilliSeconds: 500);
        }

        // ファイルコピーを行なうメソッド
        public static void FileCopyRetry(string sorcePass, string destPass, int retryCounts, int waitMilliSeconds)
        {
            Exception exStack = new Exception();

            foreach (var i in Enumerable.Range(1, retryCounts + 1))
            {
                try
                {
                    File.Copy(sorcePass, destPass);
                    return;
                }
                catch (Exception ex)
                {
                    exStack = ex;
                }

                if (i <= retryCounts) Thread.Sleep(waitMilliSeconds);
            }

            throw exStack;
        }

 高階関数を用いない場合ですが、

 Main → FileCopyRetry → File.Copy

 という階層になっています。

高階関数を用いた場合
        static void Main(string[] args)
        {
            SomeRetry(someMethod:() => File.Copy("sorce/abc.txt", "dest/abc.txt"), retryCounts: 5, waitMilliSeconds: 500);
        }

        public static void SomeRetry(Action someMethod, int retryCounts, int waitMilliSeconds)
        {
            Exception exStack = null;

            foreach (var i in Enumerable.Range(1, retryCounts + 1))
            {
                try
                {
                    someMethod();
                    return;
                }
                catch (Exception ex)
                {
                    exStack = exStack ?? ex;
                }

                Thread.Sleep(waitMilliSeconds);
            }

            ExceptionDispatchInfo.Capture(exStack).Throw();
        }

 高階関数を用いた場合ですが、

 Main → File.Copy
 Main → SomeRetry

 という階層になっています。

 高階関数を用いないコードと比べて

  • SomeRetryメソッドで凝集度が上がっている(File.Copyを意識しなくて済むようになったので)
  • "sorce/abc.txt", "dest/abc.txt"の受け渡しが一回ですむようになっている

 という利点が挙げられます。
 あと高階関数を用いたほうが、「File.Moveもリトライを実装してくれ」と言われた場合に、楽に作ることができます。

staticメソッドからstaticでないメソッドを呼び出すパターン

 staicメソッド中は、staticでないメソッドの記述はできないのですが、メソッドを引数とすることで、staticでないメソッドを呼び出すことができます。
 staticメソッドで書きたいけど、staticでないメソッドを呼び出さなければいけないときに利用できるでしょう。

        // メソッド呼び出し部
        private void Form1_Load(object sender, EventArgs e)
        {
            SomeClass.someStaticMethod(() => button1_Click(sender, e));
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 何らかの処理
        }        

        public class SomeClass
        {
            public static void someStaticMethod(Action someNonStaticMethod)
            {
                // 何らかの処理

                someNonStaticMethod();
            }
        }

まとめ

  • 高階関数を用いてクラス間の関係をフラットにできるという考え方は応用が効くので覚えておいたほうがいい。
  • リトライ処理用の高階関数と、staticでない関数を呼び出すstaticな高階関数の2つの例を挙げて、実際に高階関数が利用できることを確認した。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
43