さんぽみち

なにか思いついたときとかに気まぐれに更新されます。

C# メソッドチェーンしたくて仕方なかった

はじめに

メソッドチェーン楽しいですよね!
Linqとか使ってると止まらなくなっちゃいますが、これに慣れるとどんなものからでもチェーンで書きたくなります。
そこで、ストレスを感じず楽しくコーディングするために、汎用的に使えるものを考えてみました。

実装

いきなり実装に入ります。

using System;

public static class ChainMethoads
{
    public static IChain<T> Wrap<T>(this T a){ return new Chain<T>(a); }
    
    public interface IChain<T>
    {
        IChain<U> Calc<U>(Func<T, U> f);
        IChain<T> Action(Action<T> f);
        IChain<T> Action<U>(Func<T, U> f);
        IChain<T> Repeat(int n, Func<T, T> f);
        T Tear();
    }
    private class Chain<T> : IChain<T>
    {
        private T a;
        public Chain(T a){ this.a = a; }
        
        public IChain<U> Calc<U>(Func<T, U> f){ return f(this.a).Wrap(); }
        public IChain<T> Action(Action<T> f){ f(this.a); return this; }
        public IChain<T> Action<U>(Func<T, U> f){ f(this.a); return this; }
        public IChain<T> Repeat(int n, Func<T, T> f)
        { return n > 0 ? this.Calc(f).Repeat(n - 1, f) : this; }
        
        public T Tear(){ return this.a; }
    }
}

 変数名ひどいとか気にしない、手続的なことはほとんどしてないのでこっちの方がわかりやすいはず。
 特に複雑なことはしてなくて、Chainクラスの実体を隠蔽したいから内部クラスにしてIChain越しに
操作させてるだけです。
 軽くだけ各メソッドの説明をすると、
  ・Wrapメソッド : チェーン中の値に直接触れられないよう隠蔽するためにIChainで包む。
  ・Calcメソッド : 関数を渡して適用した結果を返す。LinqのSelectみたいなイメージ。
  ・Actionメソッド : 副作用のみある処理(主に状態が変わる形のものを想定)を渡し、
   元の値を返すことでチェーンをつなぐ。
  ・Repeatメソッド : 指定回数、渡した関数を適用する。
  ・Tearメソッド : 計算結果の値を得る。IChainの膜を破いて中身を取り出すイメージ。
 です。
 思いついた処理内容がこれくらいだったので実装したメソッドは少ないですが、欲しくなったときに実装すればいいやくらいの感じで気楽に作りました。
 遅延評価させるかは考えましたが、副作用のある処理をチェーンしたかったのがことの発端なのでやめときました。
 使い方はこんな感じ。

using System;

public static class ChainTest{
    [STAThread]
    public static void Main()
    {
        Console.WriteLine(
            1.Wrap().Calc( x => x * 2)
                    .Action( x => Console.WriteLine(x) )
                    .Repeat( 10, x => x * 2 )
                    .Tear()
        );
    }
}

 この実装の前に、実は先に拡張メソッドを使って実装してたのでこちらものせときます。
 Haskell風に型シグネチャも書いときます。
 ただし、ジェネリック型はC#の記述に沿って大文字とさせてもらいます。
 また、C#で副作用を期待した使い方をする箇所があるため、値がないことをVoidと書きます。
 カリー化は特に必要を感じなかったためとりあえずしてません、いるときになればやります。

namespace AllChain
{
    public static class ChainExtendMethoads
    {
        // Calc :: T -> (T -> U) -> U
        public static U Calc<T, U>(this T a,  Func<T, U> f){ return f(a); }
        // Action :: T -> (T -> Void) -> T
        public static T Action<T>(this T a, Action<T> f){ f(a); return a; }
        // Action :: T -> (T -> U) -> T
        public static T Action<T, U>(this T a, Func<T, U> f){ f(a); return a; }
        // Repeat :: T -> int -> (T -> T) -> T
        public static T Repeat<T>(this T a, int n, Func<T, T> f)
        { return n > 0 ? f(a).Repeat(n - 1, f) : a; }
        
        // Left :: T -> (T -> U) -> T
        public static T Left<T, U>(this T a, Func<T, U> f){ f(a); return a; }
        // Right :: T -> (T -> U) -> T -> U
        public static Func<T, U> Right<T, U>(this T a, Func<T, U> f){ f(a); return f; }
        // All :: T -> (T -> U) -> (T, T -> U, U)
        public static Tuple<T, Func<T, U>, U> All<T, U>(this T a, Func<T, U> f)
        { return Tuple.Create(a, f, f(a)); }
    }
}

 こっちのが実装自体は綺麗。
 LeftとRightとAllはなんかあったら面白そうだから別で実装してみたけどどこかで使えるのだろうか。
 さすがに見える範囲が広いと混乱招きそうなのでnamespace区切っときました。
 使い方はこんな感じ。

using System;
using AllChain;

public static class TestAll
{
    [STAThread]
    public static void Main()
    {
        Console.WriteLine(
            1.Calc( x => x * 2)
             .Action( x => Console.WriteLine(x))
             .Repeat( 4, x => x * x)
             .Action( x => Console.WriteLine(x));
        );
    }
}

 すべての型に直接このメソッドが使える為、隠蔽したクラスを用いた方法とは違ってWrapメソッドやTearメソッドが不要です。
 すっきりシンプルにチェーンできて気持ちいいですね!

 最初は、

using System.Collections.Generic;
//using System.Linq;

public static class LinqExtendMethoads
{
    public static IEnumerable<T> Wrap<T>(this T a){ yield return a; }
    public static T Tear<T>(this IEnumerable<T> a){ a.ToArray[0]; }
}

 とかしておいてやればLinq使い放題でいいかもとか思ったけど、使ってみるとSelectくらいしか使わない上に遅延評価が悪い方向に働くことが多かったのでやめときました。

おわりに

 実は一番最初、C#で汎用的なモナド(IMonadを実装すればバインドできるような物)を実装しようとしたけど納得いく形に再現できずこんな感じの実装にしました。
 必要な関数をデリゲートとして渡してもらって動的に作ればあるいはとかも考えたけど、結局クラスメソッドの実装が保障されてないと無理なところが出てきて無理でした。
 そう聞くと妥協のようなものに見えますが、こっちはこっちで使用感自体は悪くないです!
 みなさんも楽しいメソッドチェーンライフを送ってください!
 今回は参考にするページも特にないので参考はなしです。