LangExtでのOptionの設計
このエントリの最新版はGithubにあります。
Optionの意味を理解していることを前提に、直和型をC#で実現する方法についてを説明し、 LangExtではどういう方針を採用しているのかと、その理由について明らかにします。
バリアント型のC#での設計方針
Optionなどのバリアント型(VBのVariant型のことではありません)をC#で実現する場合、大まかに次の2つの方針があります。
- 型の階層で表現する
- タグを判別する値を持つようにする
一つ目の方法は、Option(もしくはMaybe)型の実現方法としてよく使われている方法です。
+-----------+ | Option[T] | +-----------+ △ | +-----+-----+ | | +---------+ +---------+ | Some[T] | | None[T] | +---------+ +---------+ | - value | +---------+
二つ目の方法は、LangExtのOption型が採用している方法です。
+------------+ | Option[T] | +------------+ | - hasValue | | - value | +------------+
これらの特徴を、Option型を実現する場合を例に見ていきます。
型の階層で表現する方法
バリアント型を型の階層で表現する方法は、実際にはOption型をinterfaceとして定義することが多いようです。 この方法では、Option型に対する操作はポリモーフィズムによって実現され、分岐は型階層に隠蔽されます。
public interface IOption<out T> { bool HasValue { get; } IOption<U> Map<U>(Func<T, U> f); IOption<U> Bind<U>(Func<T, IOption<U>> f); T GetOr(T defaultValue); } public sealed class Some<T> : IOption<T> { readonly T value; public Some(T value) { this.value = value; } public bool HasValue { get { return true; } } public IOption<U> Map<U>(Func<T, U> f) { return new Some<U>(f(this.value)); } public IOption<U> Bind<U>(Func<T, IOption<U>> f) { return f(this.value); } public T GetOr(T defaultValue) { return this.value; } } public sealed class None<T> : IOption<T> { public bool HasValue { get { return false; } } public IOption<U> Map<U>(Func<T, U> f) { return new None<T>(); } public IOption<U> Bind<U>(Func<T, IOption<U>> f) { return new None<T>(); } public T GetOr(T defaultValue) { return defaultValue; } }
実際には拡張メソッドで提供するメソッドについては、その中で分岐が発生しますが、
一番コアの部分はif
による分岐を(それがいいことかどうかは置いておいて)完全に排除できます。
この方法は、オブジェクト指向プログラミング的な方法であり、 オブジェクト指向プログラマにとっては自然であり、美しい解決方法でしょう。
タグを判別する値を持つ方法
Option型にはSome
とNone
という2つのラベルがあります。
これを判別するのに必要な情報は1bitでいいため、boolを使えば十分です。
public struct Option<T> { readonly bool hasValue; readonly T value; public Option(T value) { this.hasValue = true; this.value = value; } public bool HasValue { get { return this.hasValue; } } public Option<U> Map<U>(Func<T, U> f) { if (this.hasValue) return new Option<U>(); return new Option<U>(f(this.value)); } public Option<U> Bind<U>(Func<T, U> f) { if (this.hasValue) return new Option<U>(); return f(this.value); } public T GetOr(T defaultValue) { return this.hasValue ? this.value : defaultValue; } }
バリアント型をタグを判別する値を持つことによって表現する方法は、先の方法に比べると泥臭い方法に見えます。 LangExtでは、この2つの方法のメリットとデメリットを考えたうえで、泥臭く思える後者の方法を採用しています。
それぞれの利点と欠点
まずは、型の階層で表現する方法の利点を考えてみます。
None
の場合に余分なフィールドを持つ必要がないSome
やNone
にも型を与えることになるため、「Some
だけを受け取れるような関数」のように、柔軟な関数を定義可能- (Optionを
interface
とすることで)型パラメータにout
が指定できる - 実装が綺麗に書ける
特に、1つ目はラベルが多くなればなるほど効果を感じることができる利点でしょう。
2つ目も、Optionではそれほどメリットを感じないかもしれませんが、 例えばファイルとディレクトリを扱う型階層を作った場合などは大きな利点になります。
3つ目はout
を付けることのできないようなものも考えられますが、Option型の場合には付けることができます。
TがSを継承している場合に、Option[T]がOption[S]として使えるというのは、オブジェクト指向言語においては非常に魅力的です。
4つ目は、実際にミニマムな実装をしたコードを見ても、if
が全く出てこないため、オブジェクト指向プログラミングとして綺麗なコードと言えるでしょう。
では、タグを判別する値を持つことによって実現する方法の利点はどうでしょう。
- Option型の変数に対して
null
を入れることを防げている(struct
のため) - 勝手に独自のラベルを作ることができない(型階層で実現する方法の場合、IOptionを実装することで可能)
の2つです。 後者は一概には利点/欠点と判断できるものではないですが、少なくともOption型には不要な機能であり、それができてしまうと色々面倒です。
また、前者の利点は絶大です。
Option型が達成したいのは、「null
の排除」です。
にもかかわらず、型階層による実現方法では、「IOption[T]
型の変数そのものにnull
を入れられてしまう」という可能性を排除できません。
これは、別に
IOption<string> opt = null;
という直接的なコードが書かれてしまう、ということではありません。
そうではなく、例えばIOption[T]
を受け取るような関数に「とりあえず」null
を渡しておいて、「後で」直そう、と思って忘れていたような、
「うっかりnull
」の方を心配しています。
LangExtの公開以前のかなり初期のバージョンでは、型階層によってOptionを実現していました。 しかし、この手のミスがそれなりに発生したため、設計方針を切り替えたという経緯があります。
もちろん、これを単体テストやDbCによって排除すべきだ、という立場もあるでしょう。 しかし、単体テストで見つけるよりもコンパイル時に見つけた方が発見のコスト(単体テストではテストコードを書いたり、実行したりが必要)も修正のコストも小さくなりますし、 そもそも考慮漏れを起こす心配がありません。
また、DbC(というかCode Contracts)で「この型の変数にはnull
は格納できない」のような契約の書き方がわかりませんでした。
もしかすると方法はあるのかもしれないですが、LangExtではCode Contractsでどうこうする、という方向は諦めています。
どちらを採用するか
これらの利点と欠点を考えたうえで、LangExtではnull
を徹底的に排除するために、タグを判別する値を持つ方法を採用しました。
これにより、LangExtのOptionでは(変数にしろ、Optionの高階関数に渡された関数の引数にしろ)null
が来ることは一切考慮する必要がないという安心感を獲得しています。
Option以外のバリアント型
これまで考えてきたのは、あくまでOption型を実現する場合についてです。 そのため、Option以外のバリアント型もすべてOption型と同じように実装するかどうかは別問題です。
Option型に失敗時の原因も持たせることができるように拡張したResult型では、Option型と同じ方針を採用していますが、
Option型と違ってstruct
にすることができませんので、今後のバージョンで型階層による実現方法に変更される可能性もあります。
ただし、あくまで実現方法のみの変更になるため、変更があった場合でもユーザコードは修正不要です。
Option型の値の生成
Option型の値を生成する方法としては、コンストラクタを呼び出すのが.NETプログラマとしては最もわかりやすいでしょう。 しかし、この方法では常に型パラメータを明示する必要があるため、面倒です。
public void F(Option<string> opt) { ... } F(new Option<string>("hoge")); // Some "hoge" F(new Option<string>()); // None
これを解決するために、ほとんどのライブラリのOptionと同様、staticメソッドを提供しています。
F(Option.Some("hoge"));
さらに、LangExtではこれに加えて、型引数なしでNone
を使うことができるようにしています。
F(Option.None);
Option.None
はOption[Placeholder]
型のNone
を生成するようになっており、
Option[Placeholder]
型から任意のT
型のOption型への暗黙型変換を提供することでこれを実現しています。
それ以外の方法
それ以外の方法として、任意のT
型からOption[T]
への暗黙型変換を提供することも考えられます。
また、null
を任意のT
型のOption型のNone
に暗黙変換を提供するのも便利かもしれません。
LangExtでは、これらの変換を提供することも考えましたが、どちらも却下しました。
任意のT
型からOption[T]
型への暗黙の型変換を提供しない理由
これを提供しないのは、null
の扱いにあります。
public Option<string> F(string str) { return str; }
さて、単純なコードではありますが、str
にはnull
が格納されている可能性があります。
その場合にこのコードは何を返すべきでしょうか?Some null
でしょうか?それとも、None
でしょうか?
これは暗黙の型変換の仕様さえ覚えてしまえばそれでいいのですが、 それを知らない人が「こっちだ」と思い込んでしまう可能性は捨てきれません。
そのため、LangExtではこの変換を提供していません(さらに、LangExtではOption.Some(null)
は例外を投げるようになっており、null
を格納するSome
は作ることができません。これによって、Optionの高階関数に渡す関数の引数にnull
が渡されることがないことも保証しています。もしSome null
のようなことがしたいのであれば、Optionをネストするか、別の型を作るようにしてください。)。
public Option<string> F(string str) { return Option.Some(str); } // もしstrがnullの場合にNoneが返したい場合は、明示的に書く public Option<string> G(string str) { if (str == null) return Option.None; return Option.Some(str); }
null
を任意のT
型のOption型のNone
に暗黙変換しない理由
これには、上の問題以上の問題があります。
例えば、当初はOption[string]
を受け取っていた関数Fの仕様が変わり、string
を受け取ることになったとした場合、
null
をNone
に暗黙変換している個所はコンパイルエラーにならずに単にnull
を渡してしまうことになります。
// 暗黙の型変換を提供していれば、F(Option<string> opt)でもF(string str)でもコンパイルできてしまう F(null);
実はこのような暗黙の型変換を公開前のLangExtは提供していたことがありました。
その当時はまだOption.None
とは書けず、Option.None<string>()
のように書く必要があったこともあり、
手軽に書けるnull
からの変換を多用していました。
しかしというか、やはりというか、上のような問題がそれなりに発生し、
さらにOption.None
という書き方ができるようになったため、null
をNone
として扱う機能は削除しました。
クエリ式への対応
Option型をクエリ式に対応させる最も手軽な方法は、IEnumerable[T]
を実装することでしょう。
そうすれば、IEnumerable[T]
に対して定義されたSelect
やSelectMany
によって、Optionをクエリ式で扱えるようになります。
しかし、この方法では最終的に得られる型がOption[T]
ではなくIEnumerable[T]
になってしまいます。
そのため、LangExtではOption.QueryExpr.csでOption用のクエリ式用メソッドを提供しています。
IEnumerable[T]
を実装していないのは、Option型をIEnumerable[T]
として使いたいことがないからです。
match式の模倣
探した限りはあまり他で同じようなことをやっているライブラリがなかったのですが、 LangExtは(F#のような)match式をある程度模倣するMatchメソッドを提供しています。
int F(Option<string> opt) { return opt.Match( str => int.Parse(str), // 値があった場合の処理 () => -1); // 値がなかった場合の処理 }
また、名前付き引数を使って、以下のように記述することもできます。
int F(Option<string> opt) { return opt.Match( Some: str => int.Parse(str), // 値があった場合の処理 None: () => -1); // 値がなかった場合の処理 }
名前付き引数なので、順番を入れ替えることもできます(Match関数で名前付き引数を使うアイディアは、ころくんのつぶやきから借りています)。 このようなMatchメソッドは、Optionに限らず便利に使えるため、提供できる場合は提供しておくといいでしょう。
LangExtでは、Optionのほかに、ResultやTupleでもMatchメソッドを提供しています。 シーケンスに対しては実験的に実装してみた ものの、このままでは複雑すぎて使い物にならないので、アイディア募集中です。
Optionから値を取り出す方法
Option型のオブジェクトから値を取り出す操作(GetメソッドやValueプロパティ)を提供しているライブラリは多くあります。 しかし、これを提供してしまうと、せっかく「値がないことの考慮」を強制できるOptionの利点を損ねてしまいます。 そのため、LangExtではLangExt名前空間をusingしただけでは、Optionから直接値を取り出すことは出来ないようになっています。
LangExtOptionから値を取り出すには、デフォルト値を指定するGetOrメソッド(もしくはその派生のGetOrElseメソッド)を使うか、 LangExt.Unsafe名前空間をusingして(危険なコードを書くと宣言したうえで)GetValueメソッドを使います。
現在のバージョンのLangExtでは、Unsafe名前空間の中にOptionクラスを格納しているため、 Unsafe名前空間をusingしてしまうとOptionモジュールが解決できなくなってしまうという問題があります。 次のバージョンでは、Unsafe名前空間の中のOptionクラスは、UnsafeOptionという名前に変更予定です。
また、現在のバージョンではNone
の場合に例外を投げずにdefault(T)
を返すものしか提供されていませんが、
今後のバージョンでは例外を投げるメソッドも追加予定です(名前はおそらくGetになります)。
C# 使いから見てうらやましい Java8 の default 実装の使い方
Java8 から追加されるインターフェイスの default 実装ですが、C# の拡張メソッドに似てますよね。 実際、このどちらも「シンインターフェイス」を定義するだけで「リッチインターフェイス」が手に入ります。
しかし、C# の拡張メソッドと Java のインターフェイスの default 実装には、それぞれの利点と欠点があります。
拡張メソッドの利点
拡張メソッドの利点は、インターフェイスの実装者だけでなく、 インターフェイスの使用者に対してもインターフェイスの拡張が開かれている点です。 既存の型ですら、後付けでメソッドを追加することができるということです。
using System; public static class StringExtension { // インターフェイスでなくても、どんな型に対しても拡張可能 public static int ToInt(this string self) { return int.Parse(self); } }
var num = "100".ToInt();
それに対して Java8 のインターフェイスの default 実装は、 あくまでインターフェイスの提供者しか提供できません。
これは、Java8 の default 実装に対する、拡張メソッドの大きなメリットです。
default 実装の利点
では default 実装には利点がないか、というと、そうではありません。 default 実装は、リッチインターフェイスをシンインターフェイスに変換してしまうこともできるのです。
import java.util.*; // Appendableは3つのメソッドを提供するインターフェイスだが、 // SamAppendable(SamはSingle Abstract Method)はそのうち2つをdefault実装として提供することで、 // 1つの抽象メソッドを実装すればいいようにしている interface SamAppendable extends Appendable { Appendable append(CharSequence csq, int start, int end); default Appendable append(CharSequence csq) { this.append(csq, 0, csq == null ? 0 : csq.length()); return this; } default Appendable append(char c) { this.append("" + c, 0, 1); return this; } } public class Main { static void appendHoge(SamAppendable a) { a.append("hoge"); } public static void main(String[] args) { // (実はあまりいい例ではないのだけれど) // SAMインターフェイスにしたおかげで、ラムダ式が使える! appendHoge((csq, start, end) -> { System.out.printf("csq: %s, start: %d, end: %d%n", csq, start, end); return null; }); } }
それに対して、拡張メソッドではこのようなことは出来ません。
ジェネリックな IEnumerable は非ジェネリックな IEnumerable を継承しているため、 GetEnumerator を 2 種類実装する必要があって面倒なんですが、 「あれ、Java 8 の default 実装だとこの問題解決できるんじゃ?」 ・・・と思ったのがこのエントリを書くことになったきっかけです。 なるほど、拡張メソッドにしなかったのには理由がありそうですね。
そんなことより Scala ですよ
こんな感じで、どちらも一長一短であって優劣つけがたい感じなのですが・・・ なんと、Scala の trait はこのどちらにも対応しています。
既存の型へのメンバの追加
object Extensions { implicit class StringExtension(val value: String) { def toInteger: Int = value.toInt } }
2.10 からかなり簡単に書けるようになりました。
リッチインターフェイスのシンインターフェイス化
trait SamAppendable extends Appendable { def append(csq: CharSequence, start: Int, end: Int): Appendable def append(csq: CharSequence): Appendable = { this.append(csq, 0, if (csq == null) 0 else csq.length) this } def append(c: Char): Appendable = { this.append("" + c, 0, 1) this } }
Scala では Appendable を SAM インターフェイスにする意味はないですが、 リッチでファットなインターフェイスをシンインターフェイスにできるのは、 既存の Java コードとのやり取りを考えるとありがたいかもしれません。
Java8
Java8 に期待するより、今使える Scala 使おう! でも、default 実装が拡張メソッドの劣化コピーとか言っちゃダメ!
LangExt2.0 リリースしました
しました。
メジャーバージョンが上がって、1.xとは互換性のないものになっています。 そこらへんについては、「なぜそうしたのか」等をまとめて公開していくつもりです。
で、リリースしておいてなんですが、使用にあたっていくつかの注意点があります。
- 標準クエリ演算子(SelectとかWhereとか)を捨ててます。ふつうのC#erにはお勧めしません。
- Grouping系のメソッドは未実装です。実装できてないものがいっぱいあって、それを使って実装したいのでまだありません。いつか作ります。
- Join系のメソッドは未実装です。え、これ、必要・・・?
Lenはおとーふさんの要望により、Lengthになるかもしれません。自分としてはLenで十分だと思うのですが・・・→ LenはLenのままで、より汎用的に使える名前であるSizeを追加することになりました- なんとかWithIndex系のメソッドはなくなる可能性があります。結果をMapする関数を受け取る版を作った方が、色々と柔軟なのでは、と思ったので。そっちも用意しつつ、なんとかWithIndex系のメソッドも残す可能性もあります。議論が必要です。
注意点はこんなところです。
LangExtって何?
C#での関数プログラミングをサポートするためのライブラリです。 無理やり感ありますし、F#だったら・・・!と思うこともよくあるのですが、そこは諦める感じでお願いします。無理です。
もっと知りたい人は、コードを読むか、 https://github.com/LangExt/LangExt/blob/master/doc/LangExt.markdown あたりをどうぞ。
あ、一応これ、仕事で使う用のライブラリでもあります。 実際に仕事で発生した問題に対応するためのあれこれが入っています。
なぜ○○は××になってるの?
今後、まとめていく予定です。現在予定している項目としては、
- なぜ標準クエリ演算子を捨てたのか?
- なぜIEnumerable
を捨てたのか? - Optionの実装はなぜああなっているのか?→書いた
- EitherではなくResultにしたのはなぜか?(もしくは、1.xではEitherだったのに、2.0ではなぜResultにしたのか?)
- タプルを独自実装から標準ライブラリのものにしたのはなぜか?
- 拡張メソッドとインスタンスメソッドはどう使い分けているのか?
- OptionやUnitは、F#の型をそのまま使うのではいけなかったのか?
です。 LangExtに限った話だけでなく、C#の言語寄りの話も盛り込むと思います。
今後の予定
GitHubのマイルストーンあたりを見ていただければ大体わかるかと。
あと、LangExt以外にもなんとかExtをいくつか作る予定です。 そのあたりは、LangExt (LangExt) · GitHubからたどって情報を拾ってください。
Java 8を関数型っぽく使うためのおまじないをF#でやってみた
Java 8を関数型っぽく使うためのおまじない - きしだのはてな
Java 8を関数型っぽく使うためのおまじないをC#でやってみた - ぐるぐる~
Java も C# も大変ですね。 F# さんは、ラムダ式も関数型も最初から使えたので、似たようなことはすでにできます。 上記の記事のパクリなので、上記の記事をまずは読んでから読むことをおすすめします。
関数型(関数を表す型の方)
F# では FSharpFunc という型があります。名前空間や型パラメータまで含めると、Microsoft.FSharp.Core.FSharpFunc<'T, 'U>
です。
ただ、この型を直接使うことはありませんし、見ることもそうそうないです。
その代わりに、'T -> 'U
という表記が使えます。「'T
を受け取って 'U
を返す関数」と読みます。
ちなみに、型パラメータの最初に「'」が付いているのが割と大事なことなのですが、
それはまた機会があればということで今回は「型パラメータの一文字目は「'」が必要」くらいに思っておいてください。
こんな感じで使います。
(* F#では//形式の一行コメントも使えるけど、 はてなブログがF#のシンタックスハイライトに対応してないので、 この記事ではブロックコメントを使います(OCamlのシンタックスハイライトを使ってます)。 *) (* Java: Function<String, String> enclose = s -> "[" + s + "]"; *) (* C#: Func<string, string> enclose = s => "[" + s + "]"; *) let enclose = fun s -> "[" + s + "]"
型を全然書いてませんね。変数に型を明記すると、こうなります。
let enclose: string -> string = fun s -> "[" + s + "]"
これを呼び出そうとすると、こんな感じになります。
(* 引数を()で囲む必要はない *) System.Console.WriteLine(enclose "foo") (* System.Console.WriteLineの引数をカッコで囲っているように思えるけど、 どちらかというとenclose "foo"全体をカッコで囲っている感じ *)
こうすると、次のような表示になります。
[foo]
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。
(* sの型を明示しないと、メソッド呼出しが出来ないので明示している *) let capitalize = fun (s: string) -> s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower()
2文字未満の文字列を与えると死にます。
呼び出してみます。
System.Console.WriteLine(capitalize "foo") (* => Foo *)
この2つを順に呼び出して、capitalize して enclose しようとすると、こんな感じになりますね。
System.Console.WriteLine(enclose (capitalize "foo")) (* => [Foo] *)
こういう場合、Java では andThen を使って連結し、C# では拡張メソッドを定義しましたが、
F# では >>
という関数が用意されています。
使ってみましょう。
System.Console.WriteLine((capitalize >> enclose) "foo");
これは、関数合成ですね。
let capEnc = capitalize >> enclose System.Console.WriteLine(capEnc "foo")
F# は関数型っぽいことが簡単にできます。べんり!
関数
ところで、F# では let f = fun x -> ...
は let f x = ...
と書くことが出来ます。
これを使うと、今までのコードはこう書けます。
let enclose s = "[" + s + "]" let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() let capEnc = capitalize >> enclose System.Console.WriteLine(capEnc "foo")
そして、この let f x = ...
は関数定義です。
関数型は 'T -> 'U
でしたが、上で定義した enclose 関数のシグネチャも、string -> string
のように表します。
ラムダ式と関数がシームレスにつながっていて、とても良い感じです。
別名を使う?
'T -> 'U
がすでに Microsoft.FSharp.Core.FSharpFunc<'T, 'U>
の別名のようなものなので、必要ないですね。
さらに、型推論が効くので型を書く必要はほとんどの場合でありません。
unit
Java でも C# でも、戻り値を返さない関数は Function/Func とは別のものが必要でした。
これは、void というのが特殊なものなのが原因です。
しかし、F# で void に相当する unit は、他の型同様値を持つ(「()」という唯一の値を持つ)ので、特別扱いは不要です。
何の問題もなく、string -> unit
が使えます。
(* let writeLine (str: string) = System.Console.WriteLine(str) とかでも可 *) let writeLine: string -> unit = System.Console.WriteLine let enclose s = "[" + s + "]" writeLine (enclose "foo") let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() writeLine (capitalize "foo")
ここまでで、1引数の関数型しか使っていませんが、2引数以上の関数は甘えなので、1引数の関数が定義できれば問題ありません。
あと F# の関数である FSharpFunc<'T, 'U>
には BiFunction
やら Func<TArg1, TArg2, TResult>
なんてものはありません。何の問題もありません。
関数合成
関数合成は次のように書けます。
writeLine ((capitalize >> enclose) "foo")
writeLine も特別扱いしなくていいので、さらに合成できますね。
(capitalize >> enclose >> writeLine) "foo"
さらにもうひとつ関数を用意して、次のように書いてみます。真ん中あたりを取り出す関数です。
let middle = (fun s: string) -> s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3) (middle >> capitalize >> enclose >> writeLine) "yoofoobar"
関数合成を使わないと、次のようになりますね。
writeLine (enclose (capitalize (middle "foobaryah")))
このように、実際に呼び出す順と記述が逆になります。middle して capitalize して enclose して writeLine するのに、先に writeLine から書く必要があります。 また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数が分からなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね?
F# ではさらに、パイプライン演算子というものがあるので、こう書くこともできます。
"yoofoobar" |> middle |> capitalize |> enclose |> writeLine
「"yoofoobar"をmiddleしてcapitalizeしてencloseしてwriteLineする」ことがコードで自然に表せていますね。 これを改行すると・・・
"yoofoobar" |> middle |> capitalize |> enclose |> writeLine
おぉ。
カリー化形式
さて、2引数以上の関数は甘えと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう? こういうときに使うのがカリー化です。カリー化は、ひとつの引数を取って関数を返すことで、複数のパラメータに対応します。
例えば、はさむ文字列とはさまれる文字列を指定して、文字列をはさむ関数は、C# の2引数関数であらわすならこうなるでしょう。
string Sandwich(string tag, string str) { return tag + str + tag; }
これをカリー化形式の関数で書くと、次のようになります。
(* 一部型指定をしてますが、気にしたら負けです *) let sandwich (tag: string) = fun str -> tag + str + tag
sandwich 自体は、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』になっています。
呼び出しは次のようになります。
writeLine (sandwich "***" "sanded!") (* => ***sanded!*** *)
3引数だとこんな感じですね。
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
encloseC は、【文字列を取って、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』を返す関数】になっています。
呼び出しはこんな感じで。
writeLine (encloseC "{" "}" "enclosed!") (* => {enclosed!} *)
ところで、このカリー化形式の encloseC、引数を部分的に渡しておくことが出来ます。
let encloseCurly = encloseC "{" "}" writeLine (encloseCurly "囲まれた!")
こうやって部分適用することで、新しい関数が作れるわけです。 ちなみに Curly は、波カッコ=カーリーブラケット「{~}」のことで、カリー化とは関係ないのであしからず。
ところでところで、F# ではネストした fun をまとめることができます。 例えば、encloseC はこんな定義でした。
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
2つも fun がありますね。まとめてしまいましょう。
let encloseC (openStr: string) = fun closeStr str -> openStr + str + closeStr
スッキリしましたね!
ところでところでところで、F# では let f = fun x -> ...
は let f x = fun x -> ...
と書けるのでした。
fun をまとめる前の定義に戻ると、
let encloseC (openStr: string) = fun closeStr -> fun str -> openStr + str + closeStr
でしたので、「f」の部分が「encloseC (openStr: string)」だと思ってみると・・・
let encloseC (openStr: string) closeStr = fun str -> openStr + str + closeStr
となって、さらに
let encloseC (openStr: string) closeStr str = openStr + str + closeStr
こうですね!
まとめ
Action 以降のソース、こんな感じです。
let writeLine (str: string) = System.Console.WriteLine(str) let enclose s = "[" + s + "]" writeLine (enclose "foo") let capitalize (s: string) = s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower() writeLine (capitalize "foo") (* 関数合成 *) (capitalize >> enclose >> writeLine) "foo" let middle (s: string) = s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3) (middle >> capitalize >> enclose >> writeLine) "yoofoobar" writeLine (enclose (capitalize (middle "foobaryah"))) (* カリー化形式 *) let sandwich (tag: string) str = tag + str + tag writeLine (sandwich "***" "sanded!") let encloseC (openStr: string) closeStr str = openStr + str + closeStr writeLine (encloseC "{" "}" "enclosed!") (* 部分適用 *) let encloseCurly = encloseC "{" "}" writeLine (encloseCurly "囲まれた!")
おまけ
Java や C# では、ラムダ式と通常のメソッドの間には大きな差がありました。 その一例として、ジェネリックな関数を定義できない、というものがありました。 しかし、F# ではラムダ式と通常の関数はシームレスにつながっています。 そのため、
(* 関数 *) let id x = x
はもちろんできるし、
(* ラムダ式 *) let id = fun x -> x
Java 8を関数型っぽく使うためのおまじないをC#でやってみた
Java 8を関数型っぽく使うためのおまじない - きしだのはてな
Java は大変ですね。 C# さんは、ラムダ式も Func 型(Java の Function 型に大体対応)も Visual Studio 2008 時代(5年前)から使えたので、似たようなことはすでにできます。 上記の記事のパクリなので、上記の記事をまずは読んでから読むことをおすすめします。
Func 型
C# では Func デリゲートというものがあります。名前空間名や型パラメータまで含めると、System.Func<TArg, TResult>
です。
こんな感じで使います。
Func<string, string> enclose = s => "[" + s + "]";
これを呼び出そうとすると、こんな感じになります。
// Javaだとenclose.apply("foo")と、applyが必要 System.Console.WriteLine(enclose("foo"));
こうすると、次のような表示になります。
[foo]
もうひとつ関数を定義してみましょう。最初の文字を大文字にする関数です。
Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower();
2文字未満の文字列を与えると死にます。
呼び出してみます。
// applyは不要 System.Console.WriteLine(capitalize("foo")); // => Foo
この2つを順に呼び出して、capitalize して enclose しようとすると、こんな感じになりますね。
System.Console.WriteLine(enclose(capitalize("foo"))); // => [Foo]
こういう場合、Java では andThen を使って連結できるみたいですが、C# にはそんなものはありません。 拡張メソッドで追加しましょう。
using System; public static class Func { public static Func<T, V> AndThen<T, U, V>(this Func<T, U> f, Func<U, V> g) { return t => g(f(t)); } }
使ってみましょう。
System.Console.WriteLine(capitalize.AndThen(enclose)("foo"));
これは、関数合成ですね。
Func<string, string> capEnc = capitalize.AndThen(enclose); System.Console.WriteLine(capEnc("foo"));
C# でも関数型っぽいことができることがわかりました。やったね!
別名を使う?
Func はそんなに長くなくてそれなりにいい感じだと思います。 あと、Func オブジェクトをすでに持っているのであれば、var が使えます。
// これは怒られちゃう // var enclose = s => "[" + s + "]"; Func<string, string> enclose = s => "[" + s + "]"; Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); // Funcオブジェクトに対する操作の結果であれば、引数の型も戻り値の型も不要! var capEnc = capitalize.AndThen(enclose);
Action
戻り値を返さない関数は、C# では Func が使えません(Func<string, void>
がエラー)。
仕方ないので Action を使います。
Action を使うと、System.Console.WriteLine も扱えます。
Action<string> writeLine = System.Console.WriteLine; var enclose = (string s) => "[" + s + "]"; writeLine(enclose("foo")); var capitalize = (string s) => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); writeLine(capitalize("foo"));
ここで、1引数の Func しか使ってませんが、C# は型パラメータの数が違えば同じ名前で型が定義できます。 それを利用して、標準で 16 引数まで対応した Func/Action が用意されています。 が、2引数以上の関数は甘えなので、1引数の Func/Action さえ定義されていれば問題ありません。
関数合成
関数合成は次のように書けます。
writeLine(capitalize.AndThen(enclose)("foo"));
さらにもうひとつ関数を用意して、次のように書いてみます。真ん中あたりを取り出す関数です。
// Substringの第二引数の意味が、Javaとは違うので注意! var middle = (string s) => s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3); writeLine(middle.AndThen(capitalize).AndThen(enclose)("yoofoobar"));
関数合成を使わないと、次のようになりますね。
writeLine(enclose(capitalize(middle("foobaryah"))));
このように、実際に呼び出す順と記述が逆になります。middle して capitalize して enclose するのに、先に enclose から書く必要があります。 また、カッコがたまっていくので、最後にまとめてカッコを閉じる必要があります。ちょっと関数呼び出しが増えると、閉じカッコの数が分からなくなってコンパイルエラーがなくなるところまで「)」を増やしたり減らしたりなんてこと、ありますよね?
元記事には日本語名のメソッド定義してますが、省略します。
カリー化形式
さて、2引数以上の関数は甘えと書きましたが、実際2つ以上のパラメータを渡したいときはどうすればいいんでしょう? こういうときに使うのがカリー化です。カリー化は、ひとつの引数を取って関数を返すことで、複数のパラメータに対応します。
例えば、はさむ文字列とはさまれる文字列を指定して、文字列をはさむ関数は、通常の2引数関数であらわすならこうなるでしょう。
string Sandwich(string tag, string str) { return tag + str + tag; }
これをカリー化形式の関数で書く(もしくは、1引数関数のみを使って書く)と、次のようになります。
Func<string, Func<string, string>> sandwich = tag => str => tag + str + tag;
sandwich 自体は、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』になっています。
呼び出しは次のようになります。
writeLine(sandwich("***")("sanded!")); // => ***sanded!***
3引数だとこんな感じですね。
Func<string, Func<string, Func<string, string>>> encloseC = open => close => str => open + str + close;
encloseC は、【文字列を取って、『文字列を取って、「文字列を取って文字列を返す関数」を返す関数』を返す関数】になっています。
呼び出しはこんな感じで。
writeLine(encloseC("{")("}")("enclosed!")); // => {enclosed!}
ところで、このカリー化形式の encloseC、引数を部分的に渡しておくことが出来ます。
var encloseCurly = encloseC("{")("}"); writeLine(encloseCurly("囲まれた!"));
こうやって部分適用することで、新しい関数が作れるわけです。 ちなみに Curly は、波カッコ=カーリーブラケット「{~}」のことで、カリー化とは関係ないのであしからず。
まとめ
Action 以降のソース、こんな感じです。
Action<string> writeLine = System.Console.WriteLine; Func<string, string> enclose = s => "[" + s + "]"; writeLine(enclose("foo")); Func<string, string> capitalize = s => s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); writeLine(capitalize("foo")); // 関数合成 writeLine(capitalize.AndThen(enclose)("foo")); Func<string, string> middle = (string s) => s.Substring(s.Length / 3, s.Length - s.Length * 2 / 3); writeLine(middle.AndThen(capitalize).AndThen(enclose)("yoofoobar")); writeLine(enclose(capitalize(middle("foobaryah")))); // カリー化形式 Func<string, Func<string, string>> sandwich = tag => str => tag + str + tag; writeLine(sandwich("***")("sanded!")); Func<string, Func<string, Func<string, string>>> encloseC = open => close => str => open + str + close; writeLine(encloseC("{")("}")("enclosed!")); // 部分適用 var encloseCurly = encloseC("{")("}"); writeLine(encloseCurly("囲まれた!"));
おまけ:限界について
ラムダ式を変数で受けるスタイルは、書き方の問題を除けば、このくらいであれば問題なく使えます。 ですが、メソッドで実現できることをすべて実現できるわけではありません。 最も大きいのが、ジェネリックな関数を定義できない、というものです。 例えば、こんな簡単なメソッドを考えてみましょう。
T id<T>(T x) { return x; }
引数をそのまま返すだけの関数ですが、ジェネリックな定義になっており、string だろうが int だろうが使えます。
string s = id("hoge"); int i = id(42);
これを、ラムダ式を変数で受け取るスタイルで書こうとしても出来ません。
Func<T, T> id = x => x; // Tって何
無理やり回避するなら、クラスを作る必要があります。
public static class Id<T> { public static readonly Func<T, T> Apply = x => x; }
var str = Id<string>.Apply("hoge"); var i = Id<int>.Apply(42);
・・・面倒ですね。 そこで F# ですよ!
並列/並行基礎勉強会でasync/awaitをDisってきた
3/23 に開催された、並列/並行基礎勉強会で「async/await 不要論」という発表をしてきました。
一番言いたかったこと
一番言いたかったことは、実は並列とかとは全く関係ないことです。 それは、言語への機能追加に関することです。
C# は 5.0 で非同期処理のための専用の構文として、async/await を導入しました。 これは、F# が計算一般という抽象度の高いものための汎用的な構文として、コンピュテーション式を採用しているのとは対照的です。 専用の構文を用意するかしないかは、その言語を取り巻く環境次第です。
専用の構文の利点と欠点
専用の構文の利点は、その使用用途が明確であるというところにあります。 そのため、書き方さえ覚えてしまえば、その裏で何がどうなるかなどを一切気にせずに使えてしまえたりします。
欠点は、専用の構文なのでそれ以外には使いにくくなるところです。 例えば、クエリ式はその名の通り、クエリのための構文として導入されており、SQL に近い書き方をしますが、 これをクエリ以外のために使う場合は、どうしても不自然になってしまいます。
拡張メソッドがLINQだけじゃなく様々に使えるように、クエリ式もクエリとしての構文にこだわらず汎用のモナド用の構文として入れて欲しかった
これには完全に同意です。 クエリ式が専用の構文としてではなく、汎用的な構文として導入されていれば、async/await は不要だったでしょう。
クエリ式をクエリ式として導入しなければ成功はしなかった、という考え方もあるでしょう。 しかし、クエリ式は単純な構文であれば SQL に似ている、と言えなくもないですが、 複雑なクエリを「SQL に似ている」とするのは辛いものがあり、 クエリ式はクエリの専用構文としての導入でなくとも成功したのでは、という思いがあります。
また、専用の構文をどんどん追加していくと、その言語にはどんどんゴミの山が積みあがっていきます。 言語自体の複雑さもどんどん向上していくのも問題です。 これが限界まで進むと、「便利な機能を追加したいのに既存文法とバッティングして追加できない」ようになります。 そして、C# はかなり限界に近づいてきている言語ではないか、と思うのです。
確かに async/await は便利な機能です。 もう導入されてしまったのだから、使える環境であればどんどん使っていけばいいです。 ですが、これからもこういう機能追加を「是」とし続けると、いずれどこかで限界が来ます。 もうちょっと、ストッパー的な立場の人が必要なのではないかな、というのが個人的な思いです。
async/awaitが死ぬとき
async は Task と強く結びついています。 async を付けたメソッド (やラムダ式) は、Task を返すことしかできません(もしくは何も返さない)。 そのため、async/await は Task が古い方法となったその時に死にます。 Task や現状の async/await で対応できなくなった場合、また新たに構文を導入するのでしょうか?
async/await と互換性を保ったまま、より汎用的な構文を入れる、ということも考えられなくはないです。
以下妄想です。
現状の async/await が Task と結びついているのは、Awaitable パターンが要求するシグネチャに戻り値の型に対する規定がないことから来ています。 つまり、Awaitable パターンを実装する Awaitable/Awaiter に「Task」が出てこないのに、裏で勝手に「Task」にラップされてしまうのです。
// Awaitableパターンが要求するAwaiterの例 public class SomeAwaiter<T> : INotifyCompletion { public bool IsCompleted { get; set; } public void OnCompleted(Action cont) { ... } // Task<T>ではなく、単なるTを返す public T GetResult() { ... } }
この問題を解決するには、どこかに async キーワードと Task を関連付ける仕組みを用意する必要があります。 例えば、
[ContinuationKeyword( "async", "await", Builders = new[] { typeof(AsyncVoidMethodBuilder), typeof(AsyncTaskMethodBuilder), typeof(AsyncTaskMethodBuilder<>) })] public class AsyncKeyword { }
のようなイメージです(AsyncなんとかMethodBuilder
がGetResult
の結果と Task を関連付けています)。
こんな感じで、async/await の位置にくるキーワードをユーザ定義できるようにするわけです。
それなんてコンピュテーション式?という感じですね。 まぁ、実現されることはないでしょうが・・・
それReactive Extensionsでできるよ
こんな感じで、一番言いたかったことは実は構文のお話なのでした(そういう意味では、「async/await 不要論」というタイトルは釣りです)。 なので、「それ Rx でできるよ!」というのは、今回はほとんど言ってません。
一応、40 枚目のスライドで名前を出してはいるんですが、正直 Rx は全然調べてなかったので、名前しか出してません。 その後ちょっと調べてみたら、APM がベースで、TAP も IObservable に変換して使えるみたいです。 なので、F# や async/await は使えないけど、Rx が使える場合は、独自で実装せずに Rx を使う、というのが現実的でしょうか。
マルチコア時代とasync/await
これから「どんどん」コア数が増えていくのか、というと、「それは違うのではないか」とスライドに書きました。 しかし、緩やかにはコア数は増えていくでしょうし、現状、複数コアが使える環境というのは多くあるでしょう。 これにより、「マルチコア時代だから async/await だ!」という意見を見かけることも多くなっていくような気がしています。
これについてはスライド中でも「マルチコア化の流れとは無関係に async/await は重要だ」という話をしました。 たとえ 1コアしかなくても、async/await に意味はあるのです。
今回の発表では、この「マルチコア時代だから async/await だ!」という意見が出てくるのを防ぎたかった、 というのが、二番目に言いたかったことでした。 杞憂であるならそれに越したことはないのですが、一応ここでも明言しておきます。
次回以降
アクターに関する話が最初にちょろっと出ただけだったので、次回があるならそのあたりも聞きたいですね。 それか、CPU 内部の並列の話とか。
「変数に型がないということの利点について考える」の問題について考える
id:perlcodesample さんの 変数に型がないということの利点について考える - サンプルコードによるPerl入門 から。
ううむ。
けれども、型がないということは、本当に素晴らしいことです。
型がないことによって、たくさんの面倒から解放されるからです。
冒頭のこれが、「静的型付き言語にはメリットが(ほとんど)ない」と言っているように思えてしまいます。 コメントのやり取りを見ても、ある程度そう考えているように受け取れます。
勘違いなどが多く見られたので、補足というか、反論というか、そんな感じのことを書きます。
追記:
ごく一部、このエントリを「動的型付き言語と静的型付き言語を比べて、静的型付き言語の方が素晴らしい言語である」ということを言うためのものだと勘違いしている人を見かけました。
このエントリは、そこについては言及していません。
あくまで、元記事で「動的型付き言語のメリット」とされている部分について、「そうではないよ」と指摘するためのエントリです。
どのような型の値でも代入できる?
これは、「変数に型を明示する必要がない」ということですよね。 型推論によって、静的型付き言語であっても変数の型は明示不要です。 例えば、Perlで
my $str = 'Hello'; my $num = 1; my $nums = [1, 2, 3]; my $person = {age => 2, name => 'taro'}; my $ua = LWP::UserAgent->new;
という例を挙げていますが、静的型付き言語であるF#でも、
let str = "Hello" let num = 1 let nums = [1; 2; 3] (* 追記: Perlの方の$personをクラス的な何かと勘違いしてたので、連想配列に合わせました。 let person = { Age = 2; Name = "taro" } *) let person = Map.ofList [("age", box 2); ("name", box "taro")] let ua = LWP.UserAgent()
と、ほとんど変わらない記述が可能です。
記述量がとても短くなる?
これは、上の例で説明したように、型推論機能さえあればほとんど問題になりません。 型推論が不完全のくだりは取り下げているので触れません。
また、コンパイル時間のことを挙げていますが、対話環境を持った処理系であれば、 一部を手軽に確認することが可能です。 そして、ScalaやF#など、対話環境を持った静的型付き言語はいろいろと存在します。
統合開発環境でのメソッドの自動補完機能を実装したことがないので、 その実装が難しくなるかどうかは判断がつかないです。
変数に型がないと変更に強い?
ここで言っているのは、おそらくこういうことでしょう。
ClientA ua = c.client();
こういうJavaのコードがあったとして、clientの返す型がClientBに変更された場合、
ClientB ua = c.client();
という変更が必要になる、と。 そうであれば、ここでも型推論により解決されます。
let ua = c.client()
それはともかくとして、このように局所的な例だけで変更に強い、としてしまうのには違和感が残ります。 「変更に強い場合がある」程度ならいいのですが、それだとメリットになりませんよね。
関数のオーバーロードが不要になる?
動的型付き言語ではオーバーロードが不要になるのではなく、(型による)オーバーロードが実現できないのでは・・・
というのは置いといて、
変数に型がないことによって、関数の重複を減らすことができるという大きなメリットがあります。
これはどういうことかよくわかりません。 関数のシグネチャのことを言っている・・・?
public T sum(A a) { ... } // このpublic T sumと、 public T sum(B b) { ... } // こっちのpublic T sumが重複している?
これを「大きなメリット」と呼ぶのはつらい気がします。
それと、オーバーロードを持たない静的型付き言語のことも、たまには思い出してあげてください。 ちなみに、F#ではメソッドでのみオーバーロードが可能で、関数でのオーバーロードはできません。 ではどうするか?判別共用体というものを使います。
(* sumに渡せる判別共用体を定義する *) type SumType = | A of int list | B of float list let sum = function | A value -> (* Aの場合の処理 *) | B value -> (* Bの場合の処理 *)
「sumに渡すための型を定義しなければならず面倒」と言われてしまうかもしれませんが、 型を定義したことによって大きなメリットが生まれます。 それは、考慮漏れや、一致しない条件をコンパイル時に発見してもらえるようになることです。
例えば、「Aの処理は汎用的でいいんだけど、(高速化のために)2要素以下の時は直接計算したい」という要望が上がったとします。
(* sumはSumTypeを受け取って、intを返す関数だとする *) let sum = function | A value -> (* Aの場合の処理 *) | B value -> (* Bの場合の処理 *) | A [x; y] -> x + y | A [x] -> x | A [ ] -> 0
こう書いてしまった場合、後ろ3行のケースにはどうやってもたどり着けません。
なぜならば、| A value -> ...
のケースがそれらのケースでも当てはまってしまうからです。
これを、F#コンパイラは警告として知らせてくれます。
では修正しましょう。
let sum = function | A [x; y] -> x + y | A [x] -> x | A [ ] -> 0 | A value -> (* Aの場合の処理 *)
今度は、修正を間違ってBのケースを消してしまいました。 この場合でも、F#コンパイラは「Bのケースが考慮されていない」という警告で知らせてくれます。
これに対しても「発見が早いか遅いかの違いだ」と言うことはできるでしょう。 しかし、たとえ「何かバグを埋め込んだとしても絶対にテストで検知できる」というありえない仮定をしても、 バグの発見は早ければ早いほどその修正は楽であることが多いのです。
条件の考慮漏れや、一致しない条件を書いてしまうことがあるのと同じくらい、 その条件分岐に対するテストを書き忘れるというミスも起こりえます。 こういうミスをはじくことができる言語があり、 そういう言語を使うことである種のバグはなくなるのです。
その中にはほかの言語では「値レベルの問題」である、NullPointerExceptionの問題も含まれます。 OptionやMaybeについて調べてみるといいでしょう。
複数の型を受け取りたいときに、インターフェースを実装する必要がない?
ここでは「Java」と限定しているので深くは踏み込みませんが、
変数に型がないことによって、クラスの実装が重複がなくとてもシンプルになります。
は気になります。 変数に型があるなしと、クラスの実装の重複にどのような関係があるのでしょうか?
C++のテンプレートのような機能も必要がない?
ここは、「何でもかんでも1つの関数に詰め込むことができる」と言っているように思えます。 これをメリットに含めるのは無理があります。
変数に型がないとどのような型の値が代入されているかわからないという批判に答える?
静的型付き言語のメリットの一つとして、「型がドキュメントとして使える」というものがあります *1。 型があれば「その関数にどんなものが渡せ、どんなものが返ってくるのか」程度の情報は得られます。
例えば、
val f: 'a list -> int
であれば、「リスト(入っている要素は何でもいい)を受け取って、intを返す」ことがわかります。 他にも、「(よほどマジカルなことをしていない限りは)この関数はリストの各要素には用はないのだな」ということもわかりますし、「引数は一つしかとらない」という確信を得ることもできます。
ここではf
なんていう適当な名前を付けましたが、これも適切な名前にすればさらにドキュメント性は高まります。
この種の判断を、動的型付き言語ではドキュメントに頼るか、自分で関数を読み解く必要があります。 記事中では後者の方法を紹介していましたが、関数が複雑になればなるほど、その作業は難しくなり、 また間違いも犯しやすくなっていきます。
変数に型がないことのメリットは重複を少なくソースコードがかけること?
ここでの重複は何を指しているのでしょうか? それに、
静的言語はインターフェースやクラスをそのたびに実装しなければならないので、修正や変更が行いづらいです。その点では、保守性は低いといえます。
というのは色々と勘違いが含まれています。
インターフェイスやクラスを実装することは必須ではない
静的型付き言語であってもインターフェイスやクラスをそのたびに実装しなければいけないわけではありません。 例えば、Scalaには構造的部分型があるので、
def getName(x: { def name: String }) = x.name
のように、「nameを持つ型」という指定が可能です。 nameさえ持っていれば、どんなものでもこのメソッドに渡せます。 SML#には、多相レコードがあり、
fun getName x = #name x
のように、同じく「nameを持つ型」という指定が可能です。
静的型付き言語の方が修正が容易なものも多い
静的型付きの言語は、外部に影響を与えるような修正は動的型付き言語に比べて容易です。 例えば、
- クラス名を変更する
- メソッド名を変更する
- メソッドの引数を変更する
- メソッドの戻り値を変更する
のような修正は、静的型付き言語では影響の及ぶ範囲をコンパイラがチェックしてくれますので、 変更してエラーになった部分を潰していけば修正作業は終了です。 コンパイルのチェックはテストにも及ぶため、テスト側の修正漏れの恐れもありません。 IDEによっては、そもそもこの種の変更をIDEが自動でやってくれるものもあります。
それに対して、動的型付き言語の場合、どこかのテストに不備があった場合、バグを埋め込んでしまうことになります。
思うに、動的型付き言語の「修正の行いやすさ」は、メソッド内や、クラス内に閉じている場合のみに言えるのではないでしょうか。
その「変更の強さ」は「バグの埋め込みやすさ」に直結している
例えばあるインターフェイスにメソッドを追加した場合、その実装クラスすべてにメソッドを実装する必要があります*2。 そして、これを指して「静的型付き言語は変更に弱く、動的型付き言語は変更に強い」としているのであれば、「変更の強さ」についての考えが甘いでしょう。
確かに、動的型付き言語であれば実装クラスにメソッドを追加するだけで「そのまま動かす」ことは可能です。 が、それは「正しく」動き続けているわけではありません。 実装クラスにメソッドを追加したということは、それがどこかで使われる、ということです。 どこかでその追加したメソッドを呼び出すようにして、そのメソッドを呼び出すためのレシーバとして使っているインスタンスがほとんどの場合メソッドを追加した実装クラスだったとしても、たった1パターンでも違うクラスのインスタンスが入ってくるような場合、それをどうやって修正するのでしょうか。
静的型付き言語では、確かにインターフェイスにメソッドを追加しただけでは「そのまま動かす」ことは無理ですが、コンパイルエラーを潰せば「正しく」することは容易ですし、IDEの力によって「このメソッドを呼び出している個所を洗い出す」ことも容易です。
全体を通して
コメント欄含め、全体を通してみると、テストを過信しすぎです。 全数テストでもやらない限り、テストで「保証」を得ることはできません。
また、静的な型があればバグが減るのかどうかですが、最終的なバグの数(システムに残ってしまう見つからなかったor放置されたバグの数)は同じくらいになるかもしれません。 ですが、バグの絶対数(システムが死に絶えるまでに見つかったバグの数)は減るはずです。 なぜなら、動的型付き言語であれば埋め込んだであろうバグを、そもそも入れ込まなくなるから。 品質どうこうは置いておいたとしても、この点は非常に重要です。
例えば、例に挙げられていたsum関数のテストですが、sum関数自体のテストはそれほど手間は変わらないでしょう。 しかし、sum関数を呼び出している側がsum関数が対応している型を渡しているかどうか、という確認は、 直接的でないにしても行う必要があります。 この確認に一つでも漏れがあった場合、それは静的型付き言語では埋め込みえなかったバグです。
静的型付き言語では、sum関数呼び出し時の型に関するテストは不要ですし、漏れもないことが保証されます。 こういった言語を実際に使ってそれなりの大きさのシステムを構築してみれば、テストの絶対数が少なくなることを感じれるはずですよ。