Functional Programming w/ C# ~ ラムダ式の出力からの型推論

気づき

TypeScript と違って C#ラムダ式の出力の型から型推論できない!

下記の定義 (2) のような、前項の結果を参照・利用する拡張メソッドを定義しようとしていて気づいた。

public static class ExtensionIEnumerable {
  // 定義 (1) ~ 一般形
  public static IEnumerable<TResult> SelectPrevRef<TSource, TResult>(this IEnumerable<TSource> source, TResult initResult, Func<TResult, TSource, TResult> func) {
    var prevResult = initResult;

    return source
      .Select( x => prevResult = func(prevResult, x) );
  }

  // 定義 (2) ~ 初期値省略形
  public static IEnumerable<TResult> SelectPrevRef<TSource, TResult>(this IEnumerable<TSource> source, Func<TResult, TSource, TResult> func) where TResult : new() =>
    source.SelectPrevRef(new TResult(), func)
}

// 利用 ~ AtCoder ARC149 (a)
// ある数字が最大 n 桁連なる数のうち m の倍数となるものの最大値を求める

var answer = Enumerable.Range(1, 9)                       // 数値 digit = 1 ~ 9 に対して実行
  .SelectMany( digit => Enumerable.Range(1, n)            // n 桁まで繰り返し
    .SelectPrevRef(new { Digit = 0, Length = 0, Rem = 0 }, (prev, length) => new {    // ← この初期値 new { Digit = 0, Length = 0, Rem = 0 } の記述を省略したい
      Digit  = digit                      ,               // 数値 digit
      Length = length                     ,               // 長さ length
      Rem    = (prev.Rem * 10 + digit) % m,               // 数値 digit を 1 桁増やした数の剰余 mod m
    } )
  )
  .Where( x => x.Rem == 0 )                               // Rem == 0 抽出
  .GroupMaxBy( x => x.Length )                            // 最大長   選択 (選択項目が最大となる要素を "すべて" 列挙する独自定義メソッド)
  .GroupMaxBy( x => x.Digit )                             // 最大基数 選択
  .FirstOrDefault() switch {
    null  => "-1",                                        // 解なし
    var x => new string((char)('0' + x.Digit), x.Length), // 文字列化 (overflow 対策として数値化回避)
  };

初期値を外から与えない場合は 0 埋めインスタンス new TResult() が与えられたものとしたい。これを隠し引数としてデフォルト引数にするオーバーロードとして定義しようと TResult initResult = new TResult() としたいところだが、new TResult() は定数ではないため、受け付けない。TResult initResult = default(TResult) としようとすると TResultstruct (0 埋めインスタンス) か class (null) で制約を場合分けしなければならず、(同一シグニチャで) 両立できない。

そこで引数で記述せず、この匿名型の初期値 new TResult() をメソッド定義内で与えたいという発想に至る。 Func から出力の型が匿名型 new { int Length, int Rem } であることは理論上は解るはず。しかし TResult 型を単独のメソッド引数として与えないと型推論できないようだ。new TResult() の代わりに source の第一要素を func に適用して TResultインスタンスを得てからリフレクションでデフォルトコンストラクタを呼び出してもやはりムリだった。
TypeScript ならできるのに。C# 言語仕様検討チームにプルリクエストしても、これまた、型推論の負荷が高まるからダメとか反対されるのだろうな。

考えてみるとラムダ式出力の匿名型の定義

      Rem    = (prev.Rem * 10 + digit) % m,

のところで、プロパティ Rem再帰構造になっており、匿名型の型決定において右辺評価が先 (SourceTyped 型推論) か左辺評価が先 (TargetTyped 型推論) かという論点はありそう。これは型推論をいろいろと試すように推論ロジックを改良しないとできそうにない。

この匿名型を事前にクラス定義しても型推論できないとエラーになるため、この再帰構造がエラーの直接の原因ではないが、匿名型+再帰はもう1つの壁となっている。