「変数に型がないということの利点について考える」の問題について考える

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関数呼び出し時の型に関するテストは不要ですし、漏れもないことが保証されます。 こういった言語を実際に使ってそれなりの大きさのシステムを構築してみれば、テストの絶対数が少なくなることを感じれるはずですよ。

*1:もちろん、型さえあればドキュメントなんて不要だ、と言いたいわけではないです

*2:Scalaのtraitや、Java8で導入予定のインターフェイスのデフォルト実装などで追加すればその限りではありません

SQLアンチパターン

献本いただきました。ありがとうございます。

当たり前を当たり前にできる可能性を秘めた本

この本の素晴らしいところは、よく見る「悪い」方法を、「悪いこと」としてまとめてくれたことです。 今までは、筋のよくない設計やSQLを考え直してもらうためにあれこれと言葉を尽くして説明する必要がありました。 その結果として、直してもらえることもありましたが、直してもらえないことも多くありました。

この本が出たことによって、今後は「SQLアンチパターンという本にアンチパターンとして載っていますよ」と、強力な理由づけの一つとなるでしょう。 この本は「自分の中の当たり前をみんなのあたりまえにできる可能性を秘めている」と感じます。

参考文献

付録Bに参考文献がまとめられているのですが、新しい版が出ているものもありますので、自分の把握している範囲で補足します。

Joe Celko's SQL for smarties

訳書としては、第二版のみですが、原著では第四版が最新です。 あぁ、まだ第三版全部読んでないのに・・・

ちなみに、第三版からかなり分厚くなっており、あれが訳される際は分冊になるんじゃないですかね・・・

Joe Celko's Trees and Hierarchies in SQL for Smarties

こちらは訳書は出ていませんが、原著はすでに第二版が出ています。 第一版からは、以下の内容が追加されているようです。

  • General Graphs
  • Petri Nets
  • State Transition Graphs

An introduction to database systems

訳書はすでに手に入れるのが難しい本ですが、原著は第八版まで出ています。

NULLの「予想に反する」挙動

P.151の「13.5 解決策:NULLを一意な値として使う」では、NULLの「予想に反する」挙動をまとめているのですが・・・
説明としては、13.5.1の本文で十分なのに、表が残念なうえ、13.5.2は全体的に残念です。

NULLの挙動が分かりにくいのは、TRUEや'string'、12345との比較や、論理式を組み合わせた場合ではないです。 これらは、NULLを「わからないもの」と考えれば素直に理解できます。

例えば、NULL = 0がNULLになるのは、NULLのことを「0かもしれないし、1かもしれないし、100かもしれない」と考えればいいです。 そんなものを0と比較しても、結果は「TRUEかもしれないし、FALSEかもしれない」としか言えません(つまり結果もNULLですね)。

論理式も同様です。 NULL AND TRUEがNULLになるのは、NULLのことを「TRUEかもしれないし、FALSEかもしれない」と考えれば、結果は「TRUEかもしれないし、FALSEかもしれない」ですよね。
それに対して、NULL AND FALSEがFALSEになるのは、NULLがTRUEであろうがFALSEであろうが、FALSEとANDすれば結果は常にFALSEです(TRUE AND FALSEも、FALSE AND FALSEも、どちらもFALSE)。 NULL OR TRUEがTRUEになるのも同じ考え方で導き出せます。

NULLが怖いのは、演算の結果一つ一つではなく、「NULLが式のどこかに紛れ込んだとき」であり、「頭から抜けてしまったNULL」です。 多くのプログラマは、

x < 10

なんて式を見たら真か偽が返ると思ってしまいます。 こんな単純な式なら大丈夫な人でも、式が複雑になればなるほど、その式の結果がNULLによって自分の想定とは違うものになる可能性が高くなっていきます。

それが嫌なら、NOT NULL制約はNULLが必要な場合のみ付けず、基本的に付けるという方針がいいでしょう。

経路列挙モデル

経路列挙モデル他、階層構造を表現する代表的な方法についての言及があり、さらに各手法の比較まで載っており、素晴らしいですね。 ちなみに、経路列挙モデルをサポートしたHierarchyIdという型がSQL Server 2008から実装されており、SQL Serverでは手軽に経路列挙モデルの設計が行えます。

そして再帰CTEについても言及があるのですが、「ツリーへのクエリ実行」が「簡単」になっていて、お、おう・・・という感じです。 まぁ、再帰CTEもそろそろもっと広まっていいと思いますね。

遅延評価いうなキャンペーンとかどうか

遅延評価については以前も書いてるんですが、そのときは結論なしでした。
が、ちょっと考えるところがあって、言語を Java に絞って自分の考えを明確にしておきます。
結論から書きましょう。


Java(とC#) で遅延評価って書いてあるものは遅延評価ではない」です。

Java における「評価」とは

まず一番最初に、Java で「評価」って言うと、どういうことを指すのかを確認しておきます。
言語仕様の該当部分を要約すると、こんな感じでしょうか。

プログラム中の式を評価すると、結果は

  • 変数
  • 無し

のうちのどれかとなる。

評価した結果が値になる、というのはいいでしょう。それ以外の 2 つを軽く説明します。

評価の結果が「変数」とは?

コメント欄で指摘が入っています。

代入の結果は変数ではありません(15.26)。
結果が変数となるのは、ローカル変数、現在のオブジェクトやクラスの変数、フィールドアクセス、配列アクセスを評価した場合です。

調べ直したところ、確かに「代入式の結果自体は変数ではない」とありました。以下の記述は誤りですので、無視してください。


評価の結果が変数になる場合というのは、例えば次のような場合です。

String line;
while ((line = in.readLine()) != null) {
    ...
}

このコードにおいて、(line = in.readLine()) は評価されると、line という変数となります。そして、 line != null として評価され、真偽値となります。

評価の結果が「無し」とは?

評価の結果が無しになる場合というのは、void を戻り値とするメソッドを呼び出した場合に限られます*1
例えば、

PrintStream ps = System.out;
ps.println("hello");

というコードにおいて、ps.println("hello") は評価されても結果としては「無し」となります。
式を評価しようとしたら例外が発生した、ということではないので注意してください。
まぁ、このエントリを読む分にはどうでもいいことですが・・・


完全に蛇足ですが、評価の完了は「通常の完了」と「急な完了」に分けられており、例外が発生した場合は「急な完了」となります(言語仕様の 15.6 より)。
となると、Java での評価は、

  • 通常の完了
    • 変数
    • 無し
  • 急な完了

のように分けられる、ということですかね。

で、評価とは

話を戻します。
Java での評価というのは「式」から「値」を生み出す(もしくは式を値に変換する)ことを言うと思っていいでしょう。
評価の結果が変数となっても、それを囲むより大きな式を評価する際には、その変数に格納されている値が必要であることが多いからです。
「無し」は今回の本筋とはあまり関係がないので、無視します。


重要なのは、Java においては式のみが評価の対象となる、ということです。
文は評価(evaluation)ではなく、実行(execution)と呼ぶようですね。

「(遅延)評価」の誤用の例

このエントリを書くきっかけとなったものですが、

Java Advent Calendar 1 日目 - Project Lambda の遅延評価

などです。
例えば、

というのも、Stream インタフェースで提供されているメソッドは遅延評価されるからなのです

Java in the Box Annex: Java Advent Calendar 1 日目 - Project Lambda の遅延評価

という記述は「Stream インタフェースで提供されているメソッド」が式ではないため、(Java 8 で evaluation の意味が変わらない限り) 評価という言葉の誤用です。
「メソッド(起動式)は遅延評価される」と補えば、誤用ではないということもできるでしょうが、この文が言いたいのはそこではないのでやはり駄目でしょう*2

遅延評価という言葉

この言葉が lazy evaluation を指すのか delayed evaluation を指すのかというのはひとまず置いておきましょう*3
ここで問題にしたいのは、「遅延評価」という言葉を、「遅延リスト」のことと思って使っている人が多いのではないか、という点です。

これ、Java プログラマに限らず結構な人がそう思ってるような気がします。
少なくとも Java の言語仕様では式を値に変換することを評価と呼んでいますので、Java で評価ですらないものを遅延評価と呼んでしまう(しかも Java 界でもかなりすごい人が)のはよくないのでは、と思うわけです。
ということで、「遅延評価いうなキャンペーン」どうですかね。
や、とくに何をやるわけでもないですが。

遅延リスト

では、遅延リスト*4は何を遅延しているのでしょうか。
それは、「要素の計算」です*5


上記ブログエントリから、例を拝借してみてみましょう(コメントは消してあります)。

List<Integer> nums = Arrays.asList(0, 1, 2, 3, 4, 5);
Stream<Integer> stream = nums.stream(); 
Stream<Integer> stream2 = stream.filter(s -> s%2 == 0);
stream2.forEach(s -> System.out.println(s));
Java in the Box Annex: Java Advent Calendar 1 日目 - Project Lambda の遅延評価

この例で、3 番目の文に注目します。

// sという名前はあまりよろしくないので、nにした
Stream<Integer> stream2 = stream.filter(n -> n%2 == 0);

この文を実行(評価ではないですよ)すると、stream2 には stream から偶数のみを取り出す Stream が格納されます。
偶数のみを取り出「した」Stream ではなく、偶数のみを取り出「す」Stream と表現したのがミソです。
そう、stream2 はこの時点ではまだ、stream をフィルタしておらず、「フィルタすることを予約した」とでも言うべき状態なのです。
この stream2 から実際に要素を取り出そうとするまで、フィルタという計算が遅延されるわけですね。


ここで注意してほしいのですが、この文に含まれる式で「評価されるべき式は評価されきっている」のです。
つまり、どの式も遅延評価されません。
それでは、文をばらして式にし、評価の結果どんな値になっていくのかを見てみましょう。

ローカル変数宣言文

上記の文は、全体としてローカル変数宣言文という種類の文に分類されます。
ローカル変数宣言文は、乱暴に言うと次の形の構文のことです(厳密には違うので、詳しく知りたければ 14.4 Local Variable Declaration Statements をどうぞ)。

型 変数名 = 式;

イコールの右側に式が出てきました。
上の文を当てはめると、型は Stream、変数名は stream2、式は stream.filter(n -> n%2 == 0) です。

メソッド起動式

stream.filter(n -> n%2 == 0) はメソッド起動式です。
メソッド起動式は

一次式.メソッド名(引数リスト)

の形をしており、stream が一次式、filter がメソッド名、n -> n%2 == 0 が引数リストです(厳密には 15.12 をどうぞ)。
引数リストは、

式
引数リスト, 式

のどちらかであり、今回は最初の方ですね。
つまり、n -> n%2 == 0 も式です。


そして、メソッド起動式を評価するためには一次式の評価と、引数リスト中の式の評価が必要となります(メソッド名は式ではないので、評価の対象ではありません)。
一次式は評価すると普通に値になるので、問題ないでしょう。
ここでは、stream を評価すると、stream という変数に格納されている Stream オブジェクトという値になります。

ラムダ式

さて、n -> n%2 == 0 を評価する必要がありますが、ラムダ式はまだ言語仕様として公式にリリースされていません。
そこで、意味的に同じものに置き換えて考えましょう。

public interface Filter<T> {
    boolean apply(T t);
}

こんなインターフェイスがあったとして、filter が

public Stream<T> filter(Filter<T> filter)

のようなシグネチャを持っていたとします*6
すると、次の 2 つの式は同じことを意味します。

/* Stream<Integer>なstreamがあったとして */
stream.filter(
    // 1. ラムダ式
    n -> n%2 == 0
)

stream.filter(
    // 2. クラスインスタンス生成式
    new Filter<Integer>() {
        public boolean apply(Integer n) {
            return n%2 == 0;
        }
    }
)

つまり、ラムダ式は、クラスインスタンス生成式(15.9)とみなすことができます。
クラスインスタンス生成式は、

new 型名(引数リスト) クラス本体

となっており、型名は Filter、引数リストはなし、クラス本体は { public boolean apply(Integer n) { return n%2 == 0; } } です。


何をしたかったかというと、メソッド起動式を評価するために、メソッドの引数リストであるラムダ式を評価して値にすることでした。
ここで、ラムダ式を値にすると何になるかはもう明白ですね。Filter オブジェクトです。
これで一次式も引数リストも評価が終わりましたので、メソッド起動式の評価に移れます。


メソッド起動式を評価すると、起動対象となるメソッドの本体を実行し、その結果がメソッド起動式の値となります。
字面に出てくる評価すべき式をすべて評価しきりましたが、ここまでに遅延された評価(値が必要になるまで評価が保留された式)は一つもありませんでした。
Java 8 で導入予定の Stream は、遅延リストであっても遅延評価ではないということです。

n%2 == 0 が残っているじゃないか?

ラムダ式の本体の式の評価が行われていないのは、それが「まだ評価すべきではない式」だからです。
これを遅延評価と呼ぶのであれば、全てのラムダ式を遅延評価と呼ぶことになります。
例えば、

stream.forEach(n -> System.out.println(n));

とか、

Integer found = stream.findFirst(n -> n < 5);

とかも遅延評価だ、ということになりますが、これはさすがに変じゃないでしょうか。
forEach メソッドを実行すると、stream の要素全てを計算してしまいますし、findFirst も見つかるまでの要素は計算してしまいます。
少なくとも、引き合いに出したエントリではこれらのメソッドは「遅延評価されずに即時評価される」とありましたので、今回はこれを遅延評価とするという意見は取り上げません。

遅延リストの実装パターン

ということで、先のブログのエントリは遅延評価実現のための手法の解説ではなく、遅延リストを実装するための手法の解説ということになります。
これは Iterator を使った遅延リストの実装パターンとでも呼べるでしょう。特別に名前を付けたければ、「遅延評価」などと既存の用語の使いまわしをせず、遅延計算パターンとでも言えばいいんじゃないでしょうか。
用語の使いまわしは、変数名の使いまわし同様、可能な限り避けるべきです。
皆さんも、安易に「遅延評価」言わないようにしましょう。

じゃぁ遅延評価って?

ここに書いてあることは自信ないので参考程度にしてください。


メソッド起動式の評価に、レシーバとなる一次式の評価と、引数リスト中の式の評価が必要になる、と書きました。
起動するメソッドの中でその引数を使っている、使っていないにかかわらず引数リスト中の式は評価する必要があります。
このような評価戦略を、正格な評価戦略と呼びます。


それに対して、メソッドの中で実際に引数を評価する必要が出てきてから引数の評価を行うような評価戦略を、非正格な評価戦略と呼びます。
さらにその中で、一度評価したものを再び評価しないようなものを遅延評価(lazy evaluation)と呼び、毎回評価するものを遅延評価(delayed evaluation)と呼びます。
どちらも遅延評価となっていて紛らわしいので、lazy の方を怠惰評価と訳する場合もあります。が、この呼び方はあまり広まっていないようです。この 2 つを使い分ける必要がある場合に怠惰評価という言葉を使えばいいでしょう。


正格評価と非正格評価で結果が異なる例を挙げておきます。

int infLoop() {
    while (true) { }
    return 0;
}
void f(int i) { }

このようなプログラムがあった際、f(infLoop()) という関数呼び出しを行った場合、

  • 正格評価では f の呼び出し前に infLoop の呼び出しが完了している必要があるため、infLoop を呼び出してしまって無限ループとなる
  • 非正格評価では f の定義中で i の評価が必要ない(i の値を使っていない)ため、infLoop を呼び出さずに済み、無限ループにならない

のように、非正格評価の場合、なんと無限ループになりません。

おまけ

今回は Java に限定しましたけど、C# も遅延リストを遅延評価と表現する人多い感じがします(LINQの仕組み&遅延評価の正しい基礎知識とか)。
C# の言語仕様では「評価」そのものに対する節はないのでもにょるんですが、評価という言葉が使われている部分から、評価の対象はやはり式に限定しています。
また、「7.1.1 式の値」を見ると、「最終的に式が"値"を表すことが求められます」とある*7ことと、評価という言葉の使い方から、Java とほぼ同様の意味で「評価」という言葉を使っているようです。
ということで、C# も遅延評価って言うのは避けたいです。


あと Ruby 方面にも言いたい(enumerable_lzによる遅延評価のススメとか)んですが、Ruby は言語仕様がアレでソレなので・・・ねぇ?持ってないんですよね・・・
Ruby で「遅延評価」って表現する場合は、何を評価と言っているかを明らかにしてほしいところです。


遅延評価とは関係ないですが、三項演算子とか参照渡しとかについても、こう・・・まぁいいや。

*1:と、実は続きを読むとそう書いてあります

*2:それに、後で述べますが、メソッド起動式は遅延評価されません

*3:この 2 つを区別するような人はそもそも誤用しないでしょう

*4:Java 8 では Stream と呼ぶらしい。InputStream とかあるのに Stream なんて名前付けちゃうのもいらぬ混乱の元となりそうなんでどうかと思うんですが・・・

*5:Java 言語仕様では calcurate や calcuration という単語が含まれる節はないので、「計算」という言葉に身構えなくて大丈夫です。たぶん。

*6:例を簡単にするために単純化してありますが、実際には Predicate というインターフェイスだし、シグネチャも微妙に違います

*7:この直前に「式が関係する構造のほとんどでは、」のほとんどが何を言っているのかは気になるが・・・

高専カンファ in 三重2 行ってきた

高専カンファレンス in 三重2 - 高専カンファレンス Wiki
授業発表ということで、発表もしてきました。

高専生だし、このくらい飛ばしても大丈夫やろー」と思って飛ばしすぎましたごめんなさい。
想定聴衆を、

としていたんですが、完全に読み違えました。
それならそれで、Visitor の説明とかせずに「面倒だね」くらいで流してしまえばよかったかもしれません。


それでも、判別共用体のところはなんとなくわかってもらえたようでした。
懇親会では、「F# 使ってみたい!」という人も出てきたので発表してよかったです。


発表が難しかったようであれば、前提知識ほとんどなくてもそれなりに分かるように作ったものがありますのでどうぞ(ページ数圧縮してなくてすみません・・・)。

あと、忘れかけてたんですけど、

F#で初めての関数型プログラミング − @IT

こんなのも書いていますのでどうぞ。
ここら辺までで F# 気になった方は、以下の書籍をお勧めしておきます。

日本語で読める範囲であれば、この 2 冊以外は考えなくていいです。
F# とは異なりますが、F# の元となった OCaml という言語の light な部分を使って書かれた

もお勧めです。

第2回 関数型言語勉強会 大阪でしゃべってきてた

第2回 関数型言語勉強会 大阪 : ATND

もう一か月以上前じゃないですか・・・
ということで、しゃべってきました。

C# (や Java) を使っているときよりも型の力を借りましょう、ということを主軸に話しました。
C# では色々と「型を定義することをためらわせる」力が働きます。
それに慣れていると、F# を使っても型を全然定義せずに string や int といった型をそのまま使ってしまいます。
Better C# として F# を使うのであればそれでも全然かまわないと思うのですが、やはり F# であることの恩恵は知ってほしいし感じてほしいです。


で、発表では簡単な値クラスを例にとって説明しました。
Twitter の反応を見ると、「そもそも Equals や GetHashCode を実装するようなことがない」との意見がありましたが、これの原因を考えてみてほしいのです。
もしそれが、面倒だからという理由なのだとしたら、それこそが「型を定義することをためらわせる力」です。
値クラスを導入することで、コードの表現力は増し、より説明的な記述をすることができ、型チェックも付いてきます。
型を定義したくないがためにコメントや変数名にその情報をエンコードしているのであれば、それはその言語が持つ問題として認識しましょう。

LL/ML Advent Calendar を振り返って

こんにちは、bleis (狼) です。
狼の由来はこんなかんじです。





やばいですね。
さて、LL/ML Advent Calendar を振り返ります。

ふりかえりということで、やはりここは KPT でしょうかね。

Keep

  • 来年もやる
  • 自動登録システム(手動)
  • 参加者の皆さん案外ノリがよくて大変よろしいですね

Problem

  • よんたの暴走
  • やっぱり LL 的なイベント開きたい
  • もっとちゃんと運用する
    • 書いたらどこに (誰に) 通知するの?
    • カレンダーへの反映は誰がどうやるの?
    • 参加表明の仕方

Try

  • めざせ 3 トラック!
  • クリスマソンとの合同企画?

ということで、来年もやりたいです。
あ、そういえばこれってクリスマスまでを数える系イベントでしたね。
みなさんクリスマスはいかがお過ごしでしょうか?楽しかったですか?
ぼくは普通に仕事してました。
はぁ・・・