読者です 読者をやめる 読者になる 読者になる

バインド

C#

撃ち終わると、バインドってのも解けちゃうんだね。
C# でバインドするアレなコードを書いてみました。やっぱり T4 Template の力を借りてます。でも T4 Template ももうだめですね。n = 10 とかにするとひどいことに・・・あ、やめたほうがいいですよ。メモリ使用量とかぐいんぐいんあがっていきますし*1


使い方はこんな感じ。

Func<int, int, int, int> hoge = (a, b, c) => a - b / c;
Console.WriteLine(hoge.Bind(100, 20, 2)()); // 90
Console.WriteLine(hoge.Bind(PH._1, 20, PH._2)(100, 2)); // 90
Console.WriteLine(hoge.Bind(PH._2, 20, PH._1)(2, 100)); // 90
Console.WriteLine(hoge.Bind(PH._1, PH._2, PH._3)(100, 20, 2)); // 90
Console.WriteLine(hoge.Bind(PH._1, PH._3, PH._2)(100, 2, 20)); // 90
Console.WriteLine(hoge.Bind(PH._2, PH._1, PH._3)(20, 100, 2)); // 90
Console.WriteLine(hoge.Bind(PH._2, PH._3, PH._1)(2, 100, 20)); // 90
Console.WriteLine(hoge.Bind(PH._3, PH._1, PH._2)(20, 2, 100)); // 90
Console.WriteLine(hoge.Bind(PH._3, PH._2, PH._1)(2, 20, 100)); // 90

// 以下コンパイルエラー
//var err = hoge.Bind(100, 20, PH._2); // PH._1がないのでPH._2は使えない
//var err = hoge.Bind(PH._1, PH._2, PH._4); // 3引数なのでPH._4は使えない

PH は PlaceHolder のつもりです。


以下、これを実現するコードです。

<#@ template inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation" language="C#v3.5" debug="true" hostSpecific="true" #>
<#@ output extension=".cs" encoding="UTF-8" #>
<#@ Assembly Name="System.dll" #>
<#@ Assembly Name="System.Core.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<# int n = 5; #>
using System;

namespace CommonsSharp.Lang
{
    using PlaceHolderImpl;
    namespace PlaceHolderImpl
    {
<#
for (int i = 0; i < n; i++) {
    var className = string.Format("PH{0}", i + 1);
#>
        /// <summary>
        /// <#= i + 1 #>つ目の引数を表すプレースホルダーです。
        /// </summary>
        public sealed class <#= className #>
        {
            <#= className #>() {}
            internal static readonly <#= className #> Instance = new <#= className #>();
        }
    
<# } #>
    }

    /// <summary>
    /// プレースホルダーを保持するクラスです。
    /// </summary>
    public static class PH
    {
<#
for (int i = 0; i < n; i++) {
    var className = string.Format("PH{0}", i + 1);
#>
        /// <summary>
        /// <#= i + 1 #>つ目の引数を表すプレースホルダーです。
        /// </summary>
        public static readonly <#= className #> _<#= i + 1 #> = <#= className #>.Instance;
<# } #>
    }

    public static class FuncExtension
    {   
<#
for (int i = 1; i <= n; i++)
{
    var range = Enumerable.Range(1, i);
    var actualArgs = string.Join(", ", range.Select(j => "arg" + j).ToArray());
    var inputTypes = string.Join(", ", range.Select(j => "TArg" + j).Concat(new[] { "TResult" }).ToArray());
    
    foreach (var at in ArgTemplates(i))
    {
#>
        /// <summary>
        /// 関数の引数をバインド(固定)した関数を返します。
        /// </summary>
        public static Func<<#= RetTypes(at) #>> Bind<<#= inputTypes #>>(this Func<<#= inputTypes #>> self, <#= Args(at) #>)
        {
            return (<#= LambdaArgs(at) #>) => self(<#= actualArgs #>);
        }
        
<#
    }
}
#>
    }
}
<#+
public static IEnumerable<string> ArgTemplates(int crnt)
{
    return CrossJoin(crnt).Where(arg => IsValidSequence(MakeUniqSortedSeq(arg)));
}

// cross-joinされたコレクションを生成
private static IEnumerable<string> CrossJoin(int argsCount)
{
    // PH0, PH1, ..., PHN | N=argsCount
    var phSeq = Enumerable.Range(0, argsCount + 1).Select(j => "PH" + j);
    var args = phSeq.Select(e => e == "PH0" ? "TArg1" : e);
    // 引数の個数分組み合わせる
    for (int i = 2; i <= argsCount; i++)
    {
        var tmp = new List<string>();
        // cross-joinの本体
        foreach (var arg in args)
        {
            foreach (var ph in phSeq.Select(ph => ph == "PH0" ? "TArg" + i : ph))
            {
                // 同じプレースホルダーは複数回使わない
                if (arg.Contains(ph) == false)
                    tmp.Add(arg + "," + ph);
            }
        }
        args = tmp;
    }
    return args;
}

// TArgNを0と、PHNをNとみなして重複なしのソートされたシーケンスを生成
private static int[] MakeUniqSortedSeq(string arg)
{
    return arg.Split(',')
                .Select(a => a.StartsWith("TArg") ? 0 : (a[a.Length - 1] - '0'))
                .Distinct()
                .OrderBy(i => i)
                .ToArray();
}

// 1ずつ増加しているかをチェック
private static bool IsValidSequence(int[] seq)
{
    for (int i = 1; i < seq.Length; i++)
    {
        if (seq[i - 1] + 1 != seq[i])
            return false;
    }
    return true;
}

// cross-joinで生成されたコレクションの要素を戻り値のFuncの型パラメータに変換
public static string RetTypes(string seed)
{
    var elems = seed.Split(',');
    var retTypes = new string[elems.Count(e => e.StartsWith("PH"))];
    for (int i = 1; i <= retTypes.Length; i++)
    {
        var pos = IndexOf(elems, "PH" + i) + 1;
        retTypes[i - 1] = "TArg" + pos;
    }
    return string.Join(", ", retTypes.Concat(new[] { "TResult" }).ToArray());
}

// cross-joinで生成されたコレクションの要素をラムダ式の引数に変換
public static string LambdaArgs(string seed)
{
    var elems = seed.Split(',');
    var retTypes = new string[elems.Count(e => e.StartsWith("PH"))];
    for (int i = 1; i <= retTypes.Length; i++)
    {
        var pos = IndexOf(elems, "PH" + i) + 1;
        retTypes[i - 1] = "arg" + pos;
    }
    return string.Join(", ", retTypes.ToArray());
}

// cross-joinで生成されたコレクションの要素をメソッドの引数に変換
public static string Args(string seed)
{
    return string.Join(
            ", ",
            seed.Split(',')
                .Select(arg =>
                    arg + " " + (arg.StartsWith("T") ? arg.Substring(1) : arg).ToLower()).ToArray());
}

private static int IndexOf(IEnumerable<string> es, string str)
{
    int i = -1;
    foreach (var e in es)
    {
        if (e == str)
            return i + 1;
        i++;
    }
    return i;
}
#>

本当は hoge.Bind(PH._1, PH._1, PH._1) とかも実現したいんだけど、行数がひどいことになりそうなので考えてません。今の状態でも 3,000 行超えちゃうので・・・

*1:やるとしたら実装を見直したほうがいいです