Swift REPLでいろいろなものを再定義
以前の記事でSwiftのREPLモードを紹介しました。
REPLについてSwift Blogでさらに突っ込んだ解説が出ています。
Swift REPLでいろいろなものを再定義
最初のREPLでの記事でSwiftという言語を学ぶためのSwiftを試せる方法を示しただけでした。今回はREPLのもう一つの使い方、通常のコーディングルールを超えて開発をより強力にする方法を探ります。
識別子の再定義
Swiftのコンパイラは多くのプログラムミスを自動的に防いでくれます。例えば、同じ識別子を二度定義することによって曖昧さを意図的でなく引き起こすことがあります。
swiftc -
var x = "The Answer"
var x = 42
^D
error: invalid redeclaration of 'x'
このふるまいは非対話型エディタでコーディングをするときは理にかなっています。しかしREPLの対話型環境では簡単に変更できるようになっているのが便利です。REPLは明確にこのような利便性を意識してデザインされています。
1> var x = "The Answer"
x: String = "The Answer"
2> var x = 42
x: Int = 42
3> x + 10
$R0: Int = 52
新しい定義はその後のすべての参照に対して、既存の定義を置き換えます。上記の通り、定義の型でさえもプロセス内で変更可能です。このおかげで改良を繰り返しながらさまざまな試みをすることができます。再帰的な関数の実装をしてみましょう。
4> func fib(index: Int) -> Int {
5. if index <= 1 {
6. return 1
7. }
8. return fib(index - 1) + fib(index - 2)
9. }
10> fib(40)
$R1: Int = 165580141
この実装はこの関数を記述する一つの方法に過ぎません。違ったアルゴリズムやAPIを用いて書いたコードを試すことができます。REPLでは新しく改良された実装を定義するのが簡単です。
11> func fib(index: Int) -> Int {
12. var lastValue = 1
13. var currentValue = 1
14. for var iteration = 2; iteration <= index; ++iteration {
15. let newValue = lastValue + currentValue
16. lastValue = currentValue
17. currentValue = newValue
18. }
19. return currentValue
20. }
21> fib(40)
$R2: Int = 165580141
REPLで同じ式を入力すると新しい実装が実行されます。これは単純な例ですが、REPLでは繰り返し試すことが容易にできるようにデザインされているということが分かります。
再定義かオーバーロードか
定数、変数、型の再定義はすべて直感的に動作します。上にあったように関数の再定義も可能です。ここで、関数の再定義はオーバーロードと影響しあわないのかという疑問が出てきます。REPLでは、上記のフィボナッチ数列の例にあるように既存の定義と同じ名前とシグネチャを持つときに、既存の定義を置き換えます。関数が同じ名前で異なるシグネチャが既に存在している場合には、新しくオーバーロードとして定義されます。Swiftでは2つのシグネチャの戻り値だけが違うときにもオーバーロードすることができることに注意して下さい。
22> func foo() {
23. println("Foo!")
24. }
25> func foo() -> String {
26. return "Foo!"
27. }
28> foo()
error: ambiguous use of 'foo'
この宣言は2つの異なる関数を定義していますが、有効なオーバーロードのうちの一つだけが互換性のある型を返すと推定できるときに使用しなければなりません。
28> var foo: String = foo()
foo: String = "Foo!"
29> foo() as Void
Foo!
定義を捉える
識別子を再定義することは強力ですが、その後に識別子が使用されるときに有効なだけです。すでにREPLでコンパイルされたコードは以前の定義への参照を保持しています。新しい定義が古い定義を覆い隠しているように見えますがそうではありません。次の例で実際にどのように機能するのか見てみます。
30> var message = "Hello, World!"
message: String = "Hello, World!"
31> func printMessage() {
32. println(message)
33. }
34> printMessage()
Hello, World!
35> message = "Goodbye"
36> printMessage()
Goodbye
37> var message = "New Message"
38> printMessage()
Goodbye
39> println(message)
New Message
何が起きているのかを理解するために、例を一行ずつ追っていくのがよいです。30行目は message という変数を宣言して、挨拶文を代入しています。31から33行目は printMessage() という関数を宣言して、この関数では30行目で宣言した変数の内容を表示します。34行目はメソッドを呼び出して、期待通りの結果が得られています。これまでのところは非常に簡単です。
小さな違いは35行目から始まります。35行目では新しい値を30行目で宣言された変数に代入しています。36行目では新しい変数を期待通りに出力します。さて、37行目では同じ名前の変数を宣言しています。これはこの後のコードから元の変数を事実上隠してしまいますが、38行目で呼び出された関数は再定義前にコンパイルされたものです。この関数は元の内容を保持し、新しく宣言された変数ではなく元の変数の値を出力します。39行目では、新しく宣言された変数が新しいコードで期待通りに参照できているということが分かります。
関数でも変数でも型でも、再定義はすべてこのように動作します。REPLでは制限なく自由に識別子を再定義することができますが。以前の参照はコンパイルされたその時点での強くチェックされた意味を保持しています。先ほどの例で message 識別子が変数ではなく型として再定義されたらどうなるでしょうか。printMessage() 関数は再度コンパイルされません。REPLは一貫性のある世界観に忠実に従っていて、このような無限にある潜在的な特殊ケースを見分けることを開発者に期待することはしていません。