クラウド温泉4.0@小樽 - The Return of F#
今更シリーズその1。 7月の25から28日に、 クラウド温泉4.0@小樽 - The Return of F#に参加するために北海道に行ってきました。
発表資料はこちら。
内容はコンピュテーション式のyieldとreturnについての話で、
あたりの流れの一つのまとめになっています。 上であげた、「コンピュテーション式におけるreturnとyield」では状態引数による実装のみを紹介していましたが、 今回の発表資料の中では、
- 例外による実装
- 状態変数による実装
- 状態引数による実装
- 継続による実装
と、多数の実装方法を紹介しています。
また、FSharpxやExtCoreといったF#の有名ライブラリが、コンピュテーション式を正しく実装できていない点(yieldやreturn以前の問題)も明らかにしました。 詳しくは追っていませんが、実は標準のAsyncコンピュテーション式がそもそもダメという話も・・・ そもそも、コンピュテーション式を展開して考えている実装者はおそらく皆無であり、 ネット上にある間違った情報をもとにコンピュテーション式を実装している場合が多いように思います。 モナドやモナドプラス以上の表現力のものを実装する場合は、せめてコンピュテーション式の展開規則を理解したうえで実装しましょう。
Ruby嫌いがアンダースタンディングコンピュテーションを読んで
アンダースタンディング コンピュテーション―単純な機械から不可能なプログラムまで
- 作者: Tom Stuart,笹田耕一(監訳),笹井崇司
- 出版社/メーカー: オライリージャパン
- 発売日: 2014/09/18
- メディア: 大型本
- この商品を含むブログ (2件) を見る
一番最初にはっきりさせておきますが、Rubyは嫌いな言語です。 が、この本はRubyが嫌いな自分でもいい本だと言える*1本でした。 自分が対象読者に入っているかどうかは実際に読んでみるまで微妙かな、と思っていましたが、とても楽しめました。 以下、書評です。
Rubyという選択
説明用のコードとして本書はRubyを使っていますが、 これに関してはその理由が1章にあります。
私はその明瞭さと柔軟さに魅かれてRubyを選びました
また、続けて
本書にはRuby独自の機能に依存しているところはありません。 そのため、もしあなたにとってわかりやすくなるなら、本書のサンプルコードをお好みの言語、特にPythonやJavaScriptといった動的言語に変換してもよいでしょう。
とあります。
ここで注目すべきは「特にPythonやJavaScriptといった動的言語」という風に、静的型付き言語を省いている(少なくとも積極的に推奨はしていない)点です。 で、これにはまっとうな理由がある(少なくとも自分は納得できる)のですが、この説明だけだとそれが全然わからないんですよね。 その理由をここに書いてしまうのは、ある意味この本を読む楽しさの一部を削いでしまうことになりかねない気もするのですが、そうなったとしてもこの本は依然として面白いであろうということで、書きます。 もちろん、「こういう理由があるからだ」という風に書きますが、すべて想像です。
対話環境があり、モンキーパッチングが可能であり、定数が削除できる
この本の素晴らしい点の一つに、実際に対話環境で一つ一つコードを入力していき、動作が確認できるというのがあります。 対話環境がない言語の場合はいちいち実行し直す必要があり、しかもこれまでに見てきた最早不要の出力(ようはゴミ)も表示されてしまいます。 面倒に思うかもしれませんが、きちんと理解したい方はRubyの環境を整えて*2、実際に対話環境でコードを入力して結果を確認しつつ読み進めるのがいいでしょう。
また対話環境前提で構成されているため、「以前定義したクラスのコードに戻ってこのメソッドを追加しましょう」ということができません。 そのため、モンキーパッチングができるような言語でないと実際に試しながら読むのはつらいです。
更には、「以前定義したクラスのコードに戻ってこの部分を書き換えましょう」もできません。 なので、定数を削除(というより、クラスを削除)できる必要があるのです。
もちろん、モンキーパッチングの代わりに別のモジュール内に既存のモジュールと同じものを定義してそれをopenするだとか、定数の削除の代わりにシャドウイングを使う等、このあたりはこの機能そのものがないと厳しいというわけではありません。
evalを持つ
eval的な、渡されたコードを実行する仕組みが欲しくなる場所があります。 本書で変換対象として挙げられていた言語(や、動的言語の多く?)は、eval的なものが用意されています。 これが気軽に使える言語でないと、evalを自分で実装・・・ということになりかねません。 すると当然のことながらすべてを本文に載せることは出来なくなり、本文のコードで試せないものができてしまいます。
もちろんこれにも回避方法はありますが、説明をシンプルに保つ、という点において本書がRubyのような言語を採用したのは正解だったと思います。
ちなみに、ここで挙げた理由を持つPythonやJavaScriptが本書の言語として選ばれなかった理由としては、著者の好みもあるでしょうが、一番の理由はP.188からP.191な気がしています。
クラス分割の教材という観点
Rubyのメタプログラミング的な技法などを使えば本書のコードはよりDRYに書けるでしょう。 ですが、それをすることなく一般的なOOPLで扱える程度の記述にとどめているため、他のOOPLでもクラス分割のひとつのお手本としてみることができます。
そういう観点でのオススメは3章です。 3章をすべてやると(正確には3.3まで)、基本的な機能をもった正規表現の処理系が手に入ります。 正規表現の処理系を「じゃぁ作って」と言われても、難しいと感じるプログラマは多いと思います。 3章では、それをボトムアップで作っていきます。 この際のクラスの分割方法や命名のセンスは素晴らしいです*3。
3章の動機づけの弱さ
3章はコードは素晴らしいのですが、構成にもったいなさを感じました。 最終的には基本的な機能を持った正規表現の処理系が手に入るにもかかわらず、導入部分には
計算する機械というアイディアにある本質を明らかにし、それがどんな用途に使えるのか紹介しながら、単純なコンピュータにできることの制限について調べます。
としかないのです。 3章の最初の方はこの本の対象読者にとって「これが一体何に使えるんだ・・・」という感じの話が続きます。 人によっては退屈に感じて途中でやめてしまうかもしれません。 なので、個人的には最初の方に「この章では単純な例から始め、基本的な正規表現の処理系を作り、単純なコンピュータでも役に立つことを示します」くらいの人参をぶら下げた方がよかったのではないかな、と思います。
6章のバランス
この本は非常に内容の詰まった本であり、技術書の中ではページ数も少な目か普通程度のものです。 これだけの内容をこれだけ分かりやすくこれだけのページ数に収めているのは驚異的だと思うのですが、6章は少しアンバランスな気がしました。 もう数ページ増やして、リストの説明をより詳細に行ってもよかったのではないかな、と思うのです。 これまで丁寧に解説をしていたのに、リストでは解説なしに5つの関数が与えられてあとはその動作の確認にとどまっています。 本書のもったいないと思ったところで一番大きな部分が(言語の話ではなく)ここでした。
日本語としての読みやすさ
とても読みやすかったです。 「的」をあまり使わない方針なのか、一部ひっかかりを覚えた部分もありますが、好みの問題でしょう。 何か所か原文を確認したくなったところはありましたが、とても少ないです。 意味がよく分からないな、と思った個所は一つだけです。 P.169に、
ブール値を将来のコードが読める決まったデータとして考えるのではなく、2つの選択肢を引数として呼び出し、1番目と2番目の選択肢のどちらかを選ぶコードとして、直接的に実装しましょう。
とありますが、この前半部分の意味が分かりませんでした。 ただ、ここが分からなくても後半部分だけで実際にやりたいことはわかるため、それほど問題とも思いませんでした。
本書を読むために必要なレベル
人は自分がわかっていることに対してそのレベルを低く設定しがちな気がします。 「わかっている人にはわかる説明」というのはいくつもあり、分かってしまえばその説明で確かに十分だ、と思ってしまうことはそれなりにあります。
自分はある程度知識がある状態でこの本を読んでいるため、本書を読むために必要なレベルを本来よりも低く見積もってしまう可能性があります。 という言い訳を置いたうえで、この本を読むために必要なレベルはそこまで高くないと感じています。 ただし、最初の方にも書きましたが「実際にコードを試しながら読む」のを前提として、です。 逆に、実際に試さずに「難しかった」というのは、この本の読み方としては難易度の高い方法を選んだからかもしれませんよ?
汚れ?
P.210ページの右下付近、「partial」の「a」の上にななめ線が入っていました。 自分の周りの3冊を確認したところ、3冊すべてで入っていたので、多くの本で同じようなものが入っているでしょう。 内容には関係ない話なのでこれがあるからと言って本書の価値が下がるわけではありませんが。 むしろ、将来直った場合には自慢ポイントになるかも?
全体
全体を通して、「そういう説明をするのか、なるほど」と感じた個所が多い本でした。 これまで理解できていなかった部分が理解できるようになったり、新たな知識(薀蓄的なもの)が知れたりして勉強にもなりました。 Ruby嫌いは直りそうにありませんが、この本は好きになりました。
追記
TypeProviderについて、勝手に補足
昨日行われたF# Meetup in TokyoのPart 1で@kos59125さんがTypeProviderに関する素晴らしい発表をしてくれました。
今日の発表資料アップロードしました。 commpass からもアクセス可能です。 http://t.co/XrdiVppTpM #fsugjp
— kos59125 (@kos59125) 2014, 8月 3
TypeProviderを作りたい場合に、最初の入り口として素晴らしい発表資料だと思います。 何より、日本語で読めるのがすばらしいですね!
この資料について勝手に補足します。
ProvidedTypes.fsについて
ProvidedTypes.fsというのは、TypeProviderを作るうえであると便利なものを提供してくれる素敵なファイルです。 正直、これがない状態でTypeProviderを作るのは至難の業であり、事実上必須のファイルです。
しかし、このファイルは資料にもある通り、F# 3.0 Sample Packの中にあるものをTypeProviderを提供する各ライブラリがコピーして使っているという状況です。 さらに、各々がこのファイルをいじくり、独自の進化を遂げているような状態になっています。 これは、F# 3.0 Sample Packのリポジトリが全然更新されないので仕方ないと言えば仕方ないです。
この状況を打破する可能性となるプロジェクトとして、FSharp.TypeProviders.StarterPackというものがあります。 このプロジェクトではNuGetパッケージを提供しており、NuGet参照としてこれを追加するだけでProvidedTypes.fsが自分のプロジェクトに追加されます。 まだ試してはいませんが、NuGetパッケージの更新をすれば、最新版に置き換えてくれたりするのでしょう。
消去型の使い道
20枚目に載っている「消去型」ですが、発表では「ほとんど使わないのでunitでもいい」という風に説明されていました。 こいつの使い道ですが、例えばExcelHouganshiTypeProviderでは
ProvidedTypeDefinition(asm, ns, typeName, Some typeof<ExcelFile>, HideObjectMethods = true)
引用: Houganshi.fs
のように、ExcelFileという型を指定しています。 こうすると何が嬉しいかと言うと、
type MyHouganshi = Houganshi<"Def.fs"> let h = MyHouganshi("sample.xls", NPOI.NpoiBook.Load) (* hを使った操作 *) h.Save()
と書けます。 最後の行のSaveメソッド呼出しは、ExcelFileが持っているメソッドです。 全てをProvidedMethodで定義するのではなく、生成する必要のないものは消去型に指定した型に定義すると便利です。
他にも、既存の型を元にしたTypeProviderを作る場合、既存の型を受け取る関数にTypeProviderによって提供された型のインスタンスも渡せると便利ですよね。 そういう場合には、既存の型を消去型として指定することになります。
生成型について
TypeProviderには消去型と生成型という2つの種類があります。 発表されていたのは、消去型についてでした。 自分も最近までは消去型のTypeProviderしか作っていなかったのですが、 最近生成型の有用さに気付いたので書いておきます*1。
消去型の欠点
多くのケースでは、消去型で問題ありません。 しかし、消去型は「型が提供される」といってもILレベルではその型は消えているので、 「提供された型」自体やその型情報が欲しい場合は消去型では対応できません。
そこで使えるのが生成型のTypeProviderです。
生成型のTypeProviderの作り方
参考になるのは、F#の標準ライブラリとして提供されているEdmxFileや、SqlDataConnectionの元となる、 DataProvidersです。 このTypeProviderは、一つのTypeProviderで複数の似たTypeProviderを提供するような構成になっており、このテクニックは消去型でも参考になると思います*2。
生成型のTypeProviderで提供される型は、実際にどこかのアセンブリに存在する必要があります。 DataProvidersでは、最終的にcscを呼出すことで実際にdllを作り、 生成したdllを読み込んでいます。
他にも、ProvidedTypeDefinition
のIsErased
プロパティをfalse
に設定する必要もあります。
生成型のTypeProviderの活用例は、アイディアがあるので近いうちに見せれたらな、と思います。
*1:ちなみにVisual Studio 2012がVisual Studio 11 Developer Previewだったころには、情報がなさ過ぎて生成型の方を使っていました。そのときとはGenerateという属性が型を提供される側で必要だったのですが、現在はこの属性は消えているようです
*2:ただし、その場合ProvidedTypes.fsのTypeProviderForNamespacesを継承せずに、ITypeProviderやIProvidedNamespaceを自分で実装する必要がある
値制限
なんかTLで偶然そんな話題があったので。 コードはF#です。
まずrefについて
値制限の話題に行く前に、ref
について説明します。
ref
は、「変更可能な値」を実現するために使える型で、大体こんな感じに実装されています。
type 'a ref = { mutable contents: 'a } (* refの生成 *) let ref x = { contents = x } (* refが格納している値を参照 *) let (!) x = x.contents (* refが格納している値に再代入 *) let (:=) x y = x.contents <- y
これによって、以下のようなコードが書けます。
let a = ref "hoge" printfn "%A" !a a := "piyo" printfn "%A" !a
これを実行すると、hogeとpiyoが出力されます。
a
の指す値が途中で変わっているということですね。
では、こんなコードは許されるでしょうか?
let xs = ref []
これが許されるとするならば、xs
の型は'a list ref
となるでしょう。
ではさらにコードを追加してみましょう。
(* xsの型が'a list refとするならば・・・ *) let xs = ref [] let f (ints: int list) = (* xsは'a list refなんだから、これは合法 *) xs := ints let g (strs: string list) = (* xsは'a list refなんだから、これは合法 *) xs := strs
あるぇー?f
を実行したらxs
の中にはint list
が、次にg
を実行したらxs
の中にはstring list
が入っていることになってしまう!
静的な型とはいったい何だったのか・・・
値制限とは
と言うことで、この問題を回避するために値制限と言うものがあります。 要は、
let xs = ref []
を自動ジェネリック化しない、ということですね。
(書きかけ。あとで何か書くかも?)
JavaでTupleってみる
可変長な型変数の表現より、タプルの話です。
.NETのタプル
.NETでは、型引数の数が違うクラスを定義できるので、Tuple
という名前で7要素まで対応しています。
public class Tuple<T1> { ... } public class Tuple<T1, T2> { ... } public class Tuple<T1, T2, T3> { ... } public class Tuple<T1, T2, T3, T4> { ... } public class Tuple<T1, T2, T3, T4, T5> { ... } public class Tuple<T1, T2, T3, T4, T5, T6> { ... } public class Tuple<T1, T2, T3, T4, T5, T6, T7> { ... }
そして、各要素にアクセスするために、Item1
, Item2
のようなプロパティを使います。
さて、では.NETのタプルは8要素以上は対応していないかと言うと、そうではありません。
タプルのインスタンスを作るためには、Tuple.Create
というメソッドを使って作るのが楽です。
そしてこのTuple.Create
は、なんと8引数版まで用意されているのです。
var t = Tuple.Create(true, 2, 3.0, "4", new[] { 5 }, 6M, 7L, 8);
このt
の型は、こうなります。
Tuple<bool, int, double, string, int[], decimal, long, Tuple<int>>
実は、Tuple
は8番目の型引数として、「残りの要素を保持するタプルの型」が受け取れるようになっています。
ちなみにTuple<T1>
という謎のクラスはこの時のみ使われるクラスです。
public class Tuple<T1, T2, T3, T4, T5, T6, T7, TRest> { ... }
「残りの要素を保持するタプル」にアクセスするためには、Item8
ではなく、Rest
という名前のプロパティを使います。
そのため、8要素タプルの8番目の要素にアクセスしたい場合は、
var value = t.Rest.Item1;
となります。 さてさて、ここからが.NETの残念なところなのですが、実は9要素以上のタプルを作る簡単な方法は用意されていません*1。 ので、コンストラクタを使うことになります。
var t = new Tuple<int, int, int, int, int, int, int, Tuple<int, int>>( 1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));
コンストラクタの型パラメータは省略できないので、悲惨なことになっていますね・・・
これは、Tuple.Create
の8要素版が8要素タプルを作るためではなく、ネストしたタプルを作るためのヘルパーになっていればもっと楽が出来たのです。
var t = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9)); // こうはなっていない
残念すぎる・・・
Restという考え方
さて、.NETのタプルは残念ですが、「足りない部分はRestで」というのはどこかで聞いたことのあるような話です。 そう、コンスセルですね!
ということで、コンスセルの考え方を使ってタプルのないJavaにタプルを作ってみましょう。 まずは、コンスセルの終端を表すためのクラスを導入します。 以降では、特にパッケージとか書きませんけど、全部同一パッケージに入っていると思ってください。
public final class TupleNil { static final TupleNil nil = new TupleNil(); private TupleNil() {} }
簡単ですね。
こいつは状態を持っておらず、インスタンスも外部からは生成できず、
さらには唯一の静的フィールドであるnil
もパッケージプライベートなので、
パッケージ外からはこのフィールドにすらアクセスできません。
これだけでは本当に全く何の役にも立たないクラスです。
次に、2つの型パラメータを取って実際に要素を保持するクラスを作ります。
public final class TupleCons<T, Rest> { public final T value; public final Rest rest; TupleCons(T value, Rest rest) { this.value = value; this.rest = rest; } }
これも簡単ですね。
単に、値を2つ持っているだけのクラスです。
このクラスのvalue
に値が、rest
に残りの要素を表すものが格納されます。
コンストラクタがパッケージプライベートなので、パッケージ外からこのクラスのインスタンスを生成することはできません。
最後にタプルを作るためのメソッドを持ったクラスです。
public final class Tuple { private Tuple() {} public static <T> TupleCons<T, TupleNil> singleton(T value) { // こことか return new TupleCons<T, TupleNil>(value, TupleNil.nil); } public static <T1, T2, Rest> TupleCons<T1, TupleCons<T2, Rest>> cons(T1 value, TupleCons<T2, TupleNil> rest) { // ここってダイアモンド演算子使えるんですか?使えそうだけどJavaとかわかりません>< return new TupleCons<T1, TupleCons<T2, Rest>>(value, rest); } }
あとは、これを使ったコードです。
TupleCons<Integer, TupleCons<String, TupleNil>> t2 = Tuple.cons(42, Tuple.singleton("hoge")); System.out.println(t2.value); // 42 System.out.println(t2.rest.value); // hoge
やったー!(何が
まとめ(ない)
- .NETのタプルは割と現実見て、7要素までは自然に扱える
- 大量要素のタプル使うな
- でももし扱う場合があるといけないから、一応対応しとくぜ!
- .NETの
Tuple.Create
はクソ - タプルってコンパイル時のリスト(コンスセル)だよね!
*1:まぁ、そんなタプル作るなと言われればその通り、としか言えないですが
コンピュテーション式におけるreturnとyield
今日、id:htid46 とF#の話をしつつ帰った時のまとめです。
前提条件
次の2つのエントリを読んでいることが前提です。
returnとyieldの変換規則
先日のエントリでも書いたように、ReturnとYield、ReturnFromとYieldFromは全く同じ変換のされ方をします。
T(return e, C) = C(b.Return(e)) T(yield e, C) = C(b.Yield(e)) T(return! e, C) = C(b.ReturnFrom(src(e))) T(yield! e, C) = C(b.YieldFrom(src(e)))
つまり、ReturnもYieldも同じ実装にしたとしても、コンパイルは通ります。 ということは、そのコンピュテーション式により合うと思う方を実装すればいい・・・?
returnの意味とyieldの意味の違い
ところで、return
とyield
ではそれを使ったコードの持つ意味が違うように思えます。
例として、listコンピュテーション式を作ったとしましょう。
let f x = list { if x = 0 then return -1 return 1 } let g x = list { if x = 0 then yield -1 yield 1 } let f0, f1 = (f 0, f 1) let g0, g1 = (g 0, g 1)
さて、f0
, f1
, g0
, g1
のそれぞれの値は、どうなっていてほしいでしょうか?
こうなっていることを期待しませんか?
val f0 : int list = [-1] val f1 : int list = [1] val g0 : int list = [-1; 1] val g1 : int list = [1]
つまり、return
はそこで処理を打ち切るけど、yield
は打ち切らない、という違いがあるように思うのです。
これ、C#で考えてみた場合、yield break
とyield return
の関係に似ていませんか?
F#にはyield breakがない
そういえばありませんでした。
が、OptionBuilderでの実装を使うと、return
がyield break
の代わりになるのでは!?
ということで、こういう実装を考えてみました。
open System open Basis.Core.ComputationExpr (* 最初にpredを満たさなかった要素は結果に含めるtakeWhileの別バージョン *) module Seq = let takeWhileButFirst pred xs = seq { let cont = ref true use itor = (xs :> _ seq).GetEnumerator() while itor.MoveNext() && !cont do let x = itor.Current if not (pred x) then cont := false yield x } type ListBuilder internal () = member this.Zero() = [], Continue (* returnはBreak、yieldはContinue *) member this.Return(x) = [x], Break member this.ReturnFrom(xs: _ list) = xs, Break member this.Yield(x) = [x], Continue member this.YieldFrom(xs: _ list) = xs, Continue (* fしていって、Breakが出たら後ろは捨てる(isBreakをtrueにしたうえでfalseを返す) *) member this.Bind(xs, f: _ -> _ list * FlowControl) = let isBreak = ref false let res = xs |> Seq.map f |> Seq.takeWhileButFirst (function _, Continue -> true | _ -> isBreak := true; false) |> Seq.collect fst |> Seq.toList (res, if !isBreak then Break else Continue) (* Continueだったらrestを実行してappend、Breakだったらrestは捨てる *) member this.Combine((x: _ list, cont), rest: unit -> _ list * FlowControl) = match cont with | Break -> x, Break | Continue -> let rest, cont = rest () in List.append x rest, cont (* 以降、OptionBuilderと型以外は同じ定義 *) member this.While(guard, f) = if not (guard ()) then this.Zero() else let x = f () in this.Combine(x, fun () -> this.While(guard, f)) member this.For(xs: #seq<_>, f) = this.Using( xs.GetEnumerator(), fun itor -> this.While(itor.MoveNext, fun () -> f itor.Current)) member this.Delay(f: unit -> _ list * FlowControl) = f member this.Run(f) = f () |> fst let list = List.ListBuilder()
Bindは複雑ですが、まぁ、こんなもんでしょう。 最初に出てきたBreakは結果に含めたいので、takeWhileではなく別バージョンを定義して呼び出しています。
Combineも難しくはないでしょう。
ミソは、Return系とYield系で、タプルの第二要素が違う点です。 こうしてみると、シグネチャや変換規則が同じなだけで、ReturnとYieldは別のものだという感じがしませんか?
C#のyield break
は式をとれないため、yield return
してからyield break
する必要がありました。
ですが、今回実装したlistコンピュテーション式は、return expr
とすることで値を返しつつ処理を抜けることができます。
ちなみに、C#でのyield break
がしたい場合は、return! []
とでもするといいでしょう。
カスタムオペレータが使えたら素敵!
queryコンピュテーション式のhead
などのように、yieldBreak
のようなカスタムオペレータが作れれば、よりそれっぽいと思いませんか?
・・・が、これは出来ません。
先日は簡単のために省略したq
というパラメータを覚えているでしょうか?
これは、「その中でカスタムオペレータが使えるかどうか」を示すフラグです。
通常、yieldBreak
したいのはif
の中ですが・・・お気づきですね。
最後のパラメータがq
です。
T(if e then ce1 else ce2, V, C, q) = Assert(not q); C(if e then {| ce1 |}0 else {| ce2 |}0)
if
を使うためには、q
がfalse
である必要があります。
つまり、カスタムオペレータが使えない場所にしかif
は書けないんですね。残念・・・
ちなみに、else
を伴わないif
は、else
を伴うif
に変換されたうえでさらに変換が走るので、結局上のAssert(not q)
から逃れることは出来ません。
無念・・・
最後に
return
の挙動をどうするのがいいのか、実は2通り考えたんですが、どういう方針で行けばいいのか指針がないのでつらかったです。
yield break
をまねる、という方針があったのでよかったのですが、それがなかった場合はもう一方の(実装が楽な方)を実装してしまっていたでしょうね。
たぶん、それがなかったら今回のエントリの挙動ではなく、簡単な方を実装して済ませていたと思われます。
さて、もう一つの挙動はどんなものでしょうか。 これは読者への課題にしますね!