Scalaコップ本に書いてあるC#の記述は何か変だ
コップ本というのはScalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)のことなんだけど。
Scalaスケーラブルプログラミング[コンセプト&コーディング] (Programming in Scala)
- 作者: Martin Odersky,Lex Spoon、Bill Venners,羽生田栄一,長尾高弘
- 出版社/メーカー: インプレス
- 発売日: 2009/08/21
- メディア: 単行本
- 購入: 18人 クリック: 687回
- この商品を含むブログ (121件) を見る
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
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型として扱えているわけではないからねえ。