平々毎々(アーカイブ)

はてなダイアリーのアーカイブです。

Scalaコップ本に書いてあるC#の記述は何か変だ

コップ本というのはScalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)のことなんだけど。

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)

第21章「暗黙の型変換とパラメーター」にRandomAccessSeqトレイトを例にした説明がある。

Scalaのtraitってのは実装の多重継承というかミクシン(mixin)というか、まあそんな感じのことができる言語機能なのだけど、ここではC#の抽象クラスに翻訳してみる。

親トレイトのSeqと、子トレイトのRandomAccessSeq。ランダムアクセスできるシーケンスだから、要は配列みたいなもの。

public abstract class Seq<T> : IEnumerable<T>
{
    public abstract T Apply(int i);

    public abstract int Length { get; }

    public abstract IEnumerator<T> GetEnumerator();

    public bool Contains(T item)
    {
        using (var iter = this.GetEnumerator())
        {
            while (iter.MoveNext())
                if (object.Equals(item, iter.Current))
                    return true;
            return false;
        }
    }

    public bool Exists(Predicate<T> p)
    {
        using (var iter = this.GetEnumerator())
        {
            while (iter.MoveNext())
                if (p(iter.Current))
                    return true;
            return false;
        }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return this.GetEnumerator();
    }
}
public abstract class RandomAccessSeq<T> : Seq<T>
{
    public override IEnumerator<T> GetEnumerator()
    {
        for (int i = 0; i < this.Length; i++)
            yield return this.Apply(i);
    }

    public T this[int i]
    {
        get { return this.Apply(i); }
    }
}

まあこんな感じで、ApplyメソッドとLengthプロパティの実装はまだ提供されてないわけ。

で、文字列をRandomAccessSeqとして扱いたいんだけど、java.lang.StringクラスはRandomAccessSeqを継承してないわけだ。さて困ったねといった所で、Scalaには暗黙の型変換があるよという話になる。

さっきのRandomAccessSeqだが、Scalaの実装だと文字列を引数にしてRandomAccessSeqを返すメソッドが定義されている。で、そのメソッドにはimplicitキーワードがついているので、こんなコードが書ける(下のScalaコードはplasticscafeさんの日記を参考にした)。

implicit def stringWrapper(s:String) = {
  new RandomAccessSeq[Char] {
    def length = s.length
    def apply(i:Int) = s.charAt(i)
  }
}
var isDight = "abc123" exists (_.isDigit)  // true

つまり、"abc123"はStringなんだけど、暗黙の型変換メソッドのおかげで、いきなりexistsメソッドを呼び出してもOKだということ。

ここまではいいんだけど、コップ本はここでC#の話を例に出す。曰く、「C#では拡張メソッドがあって、同じように既存のクラスに新しいメソッドを追加できるが、Scalaのimplicitはもっと便利だ。拡張メソッドだとすべてのメソッドを再定義しないといけない。その点Scalaのimplicitはlengthとapplyを書くだけでいい。将来RandomAccessSeqにメソッドが増えても問題ない」と(手元にコップ本がないので正確な引用ではないが、おおむねこういう事が書かれていた)。

うーん。そうだけど、拡張メソッドってそんな使い方するか?

上のstringWrapperメソッドは、Javaでいうインナークラスを返してるのだから、C#ならこんな感じで型を作らないといけない。確かに、このままだと使うのが不恰好になる。

public class StringWrapper : RandomAccessSeq<char>
{
    private string s;

    public StringWrapper(string s)
    {
        this.s = s;
    }

    public override char Apply(int i)
    {
        return s[i];
    }

    public override int Length
    {
        get { return s.Length; }
    }
}
var isDight = (new StringWrapper("abc123")).Exists(x => char.IsDigit(x));

C#にも暗黙の型変換はあるのだけど、Scalaほど強力じゃない。メソッド呼び出しでは暗黙の変換は働かなくて、変数への代入とかメソッドの引数とか、そういうとこでしか使えない。しかも、暗黙の型変換は演算子オーバーロードで、RandomAccessSeqという閉じた型には直接定義することができない。なのでStringWrapper型に定義するしかなく、そうすると利用側で明示的にStringWrapper型を書かないといけない。

public class StringWrapper : RandomAccessSeq<char>
{
    // 略

    // StringWrapper型の演算子オーバーロードとして定義せざるを得ない。
    // なので、引数か戻り値がStringWrapper型でないといけない。
    public static implicit operator StringWrapper(string s)
    {
        return new StringWrapper(s);
    }
}
// StringWrapperにはキャストせずに代入できるが、嬉しくない。
StringWrapper str = "abc123";
var isDight = str.Exists(x => char.IsDigit(x));

そこで、コップ本でも触れていた拡張メソッドを使うんだけど、拡張メソッドでRandomAccessSeqの全メソッドを提供するような実装はしないだろう。普通はこうなんじゃないの?

public static class SeqEx
{
    public static RandomAccessSeq<char> ToSeq(this string s)
    {
        return new StringWrapper(s);
    }

    private class StringWrapper : RandomAccessSeq<char> { ... }
}
var isDight = "abc123".ToSeq().Exists(x => char.IsDigit(x));

だいたい、拡張メソッドでRandomAccessSeqの全メソッドを提供したところで、string型をRandomAccessSeq型として扱えているわけではないからねえ。