LoginSignup
6
3

More than 3 years have passed since last update.

C#にパイプライン演算子もどきの拡張メソッドを作って書き味を向上させる

Last updated at Posted at 2020-12-25

概要

C#にはパイプライン演算子が導入されていません。しかし拡張メソッドを用いることで似た書き味を実現することができます。

この記事では最初に使用例を提示し、どのような書き味が実現されたか提示します。
次にパイプライン演算子とはどのようなもので、どのような要望に応える演算子かを説明します。
そしてパイプライン演算子もどきの拡張メソッドをどのように定義するか説明します。
最後に雑感を述べてから今回使用したコード全体を掲載します。

使用例

以下は自前で定義したPipe拡張メソッドを用いたFizzBuzzの例です。

static void Main(string[] args) =>
    Enumerable.Range(1, 30)
        .Select(
            n =>
                n % 15 == 0 ? "FizzBuzz":
                n % 3 == 0 ? "Fizz":
                n % 5 == 0 ? "Buzz":
                n.ToString())
        .Pipe(l => string.Join("\n", l))
        .Pipe(Console.WriteLine); 
  • 1から30の数字に対して
  • それぞれFizzBuzz変換を施して
  • 文字列のリストを改行で連結して
  • 最後にWriteLineする

と言う操作が、その順番通りに書けています。
記述量もそれほど多くなく、まあまあすっきりと書けているのではないでしょうか。

パイプライン演算子とは

F# や OCamlなどに採用されている演算子です。
今、「値vを関数funcFに渡して、返り値をその次に関数funcGに渡して、その返り値を関数funcHに渡して、その返り値を関数funcIに渡す」ことをしたいとします。素朴に書けばfuncI(funcH(funcG(funcF(v))))ですが、

  • 実行順と関数を書く順が逆になっていて直感的でない
  • 関数のネストが深くなっている

などにとっつきづらさを感じる人もいるかと思われます。
これを解決して、「vをfuncFして、funcGして、funcHして、funcIする」をその順番通り書くためには

var fedValue = funcF(v);
var gedValue = funcG(fedValue);
var hedValue = funcH(gedValue);
funcI(hedValue);

などと書くのも手です。しかし

  • 命名が面倒
  • 記述量が増える

などのトレードオフがあります。

パイプライン演算子 |> を持つ上述の言語たちでは、「vをfuncFして、funcGして、funcHして、funcIする」を以下のように言葉の順番通りに、かつシンプルに書くことができます。

v
|> funcF
|> funcG
|> funcH
|> funcI

パイプライン演算子もどき拡張メソッドの定義

今回はPipeと言う名前で、以下のように定義しました。

static class PipeExtensions {
   public static B Pipe<A, B>(this A data, Func<A, B> f) => f(data);
   public static void Pipe<A>(this A data, Action<A> f) => f(data);
}

雑感

Pipe拡張メソッドを定義する是非について

全ての型に対して見かけ上のメソッドを生やすものであるため、個人開発のプロダクトならまだしも複数人で開発するときに個人の判断でこんなメソッドをほいほい生やすのはためらわれますね...
標準ライブラリに梱包してくれれば安心して使えるのですが。

定義の仕方について

返り値がvoid以外の場合と、voidの場合とで定義を分けなければならないのは億劫ですよね...

書き味の向上について

今回定義したPipe拡張メソッドはまあまあ便利だと思っています。
今回用いた例で言うと「得られたデータを最後にWriteLineする」を.Pipe(Console.WriteLine))で表せているのはだいぶすっきりとした印象を受けます。
しかし、「listをJoinする」部分が.Pipe(l => String.Join("\n", l))となっているのは若干冗長な気がしないでもありません。
OCaml的なCurry化や部分適用の厚い構文上のサポートがあったら.Pipe(String.Json "\n")で済ませられそうですし、
Scala的なプレースホルダの記法があれば.Pipe(String.Json("\n", _)) みたいな書き方で済ませられそうです。
そういう構文上のサポートがない以上、C#においては今回のパイプライン演算子もどきの拡張メソッドを定義しても嬉しさが少し減っていそうです。

比較

.Pipe(l => String.Join("\n", l)) // 現行
.Pipe(String.Join "\n") // こうとか
.Pipe(String.Join("\n", _)) // こうとか書けたらもっと楽だなあという妄想

コード全体

今回用いたコードは以下の通りです。そのままOnline Compilerとかに貼り付ければ動きます。

using System;
using System.Linq;

namespace FizzBuzz
{
    class Program
    {
        static void Main(string[] args) =>
            Enumerable.Range(1, 30)
                .Select(
                    n =>
                        n % 15 == 0 ? "FizzBuzz":
                        n % 3 == 0 ? "Fizz":
                        n % 5 == 0 ? "Buzz":
                        n.ToString())
                .Pipe(l => string.Join("\n", l))
                .Pipe(Console.WriteLine); 
    }

    static class PipeExtensions {
       public static B Pipe<A, B>(this A data, Func<A, B> f) => f(data);
       public static void Pipe<A>(this A data, Action<A> f) => f(data);
    }
}
6
3
0

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
6
3