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

かたちづくり

つれづれに、だらだらと、おきらくに

列を操作する関数は値をオブジェクトにくるんで受け取るといいかもしれない

一つテクニックを思いついたので書いてみます。
…まあ大した話じゃないですし、もっといい方法が知られているのかもしれません。考えを整理するという意味でメモしておきます。

さて、次のようなド単純なインターフェイスを用意しておきます。

// F#
type IRef<'a> = abstract Value : 'a
// C#
interface IRef<T> { T Value { get; } }

値をこのインターフェイスにくるんで受け取るようにすると良いことがあるよ、というお話です。

特に、何らかの列からある条件に合致するものだけを取り出す、みたいな関数が対象です。ここでは例として、整数の列を「偶数だけを取り出して昇順にソートする」関数を考えてみます。次のように定義できるでしょう。

// F#
let toSortedEvens1 (items : int seq) =
  items
  |> Seq.filter (fun item -> item % 2 = 0)
  |> Seq.sort
// C#
static IEnumerable<int> ToSortedEvens1( this IEnumerable<int> items )
{
  return items.Where( item => item % 2 == 0 ).OrderBy( item => item );
}

何の問題もないように見えますが、この関数にもう少し汎用性を持たせたいのです。例として次のようなクラスがあるとしましょう。

// F#
type Hoge (piyo : int) =
  member this.Piyo = piyo
// C#
class Hoge
{
  public readonly int Piyo;
  public Hoge( int piyo ) { this.Piyo = piyo; }
}

この Hoge 型オブジェクトの列を「Piyoプロパティが偶数のものだけを取り出して昇順にソート」したい場合に、先ほど定義した toSortedEvens1 関数は利用できるでしょうか。いえ、残念ながら出来ません。先ほどの関数では int の列が返って来るだけですが、今回は Hoge の列が結果として欲しいのです。
もちろん Hoge 型を受け取る関数を定義しなおせば良いのですが、その設計はイマイチだと思います。関数型プログラミングではデータを出来るだけ不変な(immutableな)値として扱うことが推奨されます。それにしたがって考えると、システム全体を「不変な値を扱う下位レイヤー」と「可変なオブジェクトを扱う上位レイヤー」の2層に大きく分けて捉えた時、出来るだけ下位レイヤーが厚くなるようにするべきです。今回の例ですと、Hoge クラスは可変で上位レイヤーに属するクラスかもしれません。しかし「偶数だけを取り出して昇順にソート」というロジックは下位レイヤーに押し込めてしまいたいところです。

そこで、これを少し書き換えて int の列ではなく IRef<int> の列を受け取るように修正してみます。

// F#
let toSortedEvens2 (items : #IRef<int> seq) =
  items
  |> Seq.filter (fun item -> item.Value % 2 = 0)
  |> Seq.sortBy (fun item -> item.Value)
// C#
static IEnumerable<T> ToSortedEvens2<T>( this IEnumerable<T> items )
  where T : IRef<int>
{
  return items.Where( item => item.Value % 2 == 0 ).OrderBy( item => item.Value );
}

このように int 列を直接操作するのではなく IRef<int> にくるんで操作するようにすれば、一段抽象化が行われていることになりますので先ほどの問題を解決することが出来ます。

もっとも単純な方法は、Hoge クラスに IRef<int> インターフェイスを実装してしまうことです。もちろんそれでも問題は解決するのですが、代わりに Hoge クラスの定義が汚染されてしまいますので望ましくないケースも考えられます。ここでは Hoge クラスを汚染しないアプローチを考えてみます。

まず準備として、次のようなモジュール(C#は static class)を定義しておきましょう。

// F#
module Ref =
  type Sourced<'src, 'value> =
    { Source : 'src; Value : 'value } with
    interface IRef<'value> with member this.Value = this.Value
  let inline sourced src value = { Source = src; Value = value }
  let inline ofSource mapper src = { Source = src; Value = mapper src }
  let inline toSource ref = ref.Source
  let inline value (ref : #IRef<_>) = ref.Value
// C#
static class Ref
{
  public class Sourced<TSource, TValue> : IRef<TValue>
  {
    public readonly TSource Source;
    public readonly TValue Value;
    public Sourced(TSource src, TValue value) { this.Source = src; this.Value = value; }
    TValue IRef<TValue>.Value { get { return this.Value; } }
  }

  public static Sourced<TSource, TValue> OfSource<TSource, TValue>( TSource src, TValue value )
  {
    return new Sourced<TSource, TValue>( src, value );
  }
}

これらを利用して、Hoge オブジェクトの列を「Piyoの値が偶数のものだけを取り出して昇順にソート」してみたいと思います。

// F#
let items = [3; 5; 2; 9; 7; 0; 8; 6 ] |> List.map (fun item -> Hoge item)

let newItems =
  items
  |> Seq.map (Ref.ofSource (fun hoge -> hoge.Piyo)) // #IRef<int> に変換
  |> toSortedEvens2  // 偶数のものだけを取り出して昇順にソート
  |> Seq.map Ref.toSource // Hoge に戻す
  |> Seq.toList
// C#
var items = Array.ConvertAll( new[] { 3, 5, 2, 9, 7, 0, 8, 6 }, item => new Hoge( item ) );
var newItems = items
  .Select( h => Ref.OfSource( h, h.Piyo ) )
  .ToSortedEvens2()
  .Select( r => r.Source )
  .ToArray();

いかがなものでしょう?
個人的には使える場面は結構あるんじゃないかと思っているのですが…。

追記

Twitter で「セレクタ使えば十分では」というご指摘を頂きました。つまりこういうことですね。

let toSortedEvens3 selector items =
  items
  |> Seq.filter (fun item -> (selector item) % 2 = 0)
  |> Seq.sortBy (fun item -> (selector item))

むむ、この簡単な例では確かにその通りなのですが・・・。
この関数が実際はもっと複雑な関数で、他の関数へも selector を引き回さないといけない場合とかを考えると、インターフェイスに括りだすほうがシステムの構造をうまく表現するという場面もあるんじゃないかなぁ…と思ってみたり。あるいは、同じようなセレクタ関数を受け取る関数が沢山必要になる場合は、インターフェイスに括りだすほうが全体として見たときシンプル、みたいな場面を想像しています。
あとは selector の中身が重たい処理になる場合とか。。。
もうちっと考えてみます。

追記2

@igeta さんがオブジェクト式を利用して書きなおしてくれました。ありがとうございます!
https://gist.github.com/igeta/9183923

オブジェクト式、面白いですね!私も今後機会があったら使っていきたいです。そしてアクティブパターンの使い方にも目からウロコが落ちました。そうかーこういう使い方もできるんですね。アクティブパターンは無理矢理使ってみたことはあるものの、まだイマイチ使いこなせていないです…。