筆者:村田
こんにちは。
今回も最初はXcodeのアップデートからです。beta6が出ました。アップデートしてコンパイルしたところ大きなエラーはでませんでした。一応リリースノートを眺めてみるとOptional型を使ったいくつかのAPIに変更があるようです。その内容について触れたいと思います。
「第25回の続き」という記事でOptional型を紹介しましたが、もう少し続きがあるのです。まずはInt?型の復習から。Int?型とはIntの数値かnilを値として持てる型です。Int?型からInt型を取り出すためには、nilじゃないことを確認してから「!」で取り出すのでした。
var optApples: Int? // OptionalなInt
optApples = 3
if optApples != nil {
var apples = optApples! // 「!」で安全な取り出し
...
}
もう少しスッキリしたif文の使い方もありました。
if let apples = optApples {
... // optApplesがnilではない場合にifの中に入る
}
上記はInt?型から「明示的」にInt型へ変換する手順です。しかしプログラムの構造によってはOptional型変数に確実に値が入っていると見なせる場合もあります。こんな場合でも参照する度に「!」で型変換を行うのは少々面倒です。そこで登場するのがInt!型です。型名の後に「!」がついています。Int!型はOptional型ですが「暗黙的」にInt型へ変換されます。
var optApples: Int! // 暗黙的なOptinal
var apples: Int
optApples = 3
apples = optApples // 暗黙的にInt!からIntへ変換されます
optApples = nil // Optional型なのでnilを設定できます
apples = optApples // 実行時エラー発生。nilからIntへは変換できません
普通のOptional型と同じようにnilチェックしてから明示的に「!」で取り出すこともできます。
if optApples != nil {
var apples = optApples!
...
}
スッキリ版も同様に書けます。
if let apples = optApples {
...
}
というわけでOptional型には2通りあり、中身を取り出す際に「!」を付けなければならない明示的なInt?型と、「!」を付けなくても取り出せる暗黙的なInt!型があります。この暗黙のOptional型は実際にどんなところで使われているのでしょう。「一応nilが入ることがあるかもしれないけど、実際はただのInt」というようなニュアンスで使われているのだと思います。例えば画面にタッチした際のコールバックメソッドを見てみましょう。
func touchesBegan(touches: NSSet!, withEvent event: UIEvent!)
引数の型を見るとNSSet!、UIEvent!と暗黙のOptionalが使われています。しかし実際にnilが入ることはあり得ないでしょう。もしも明示的なNSSet?型だったらアクセスに一手間かかってしまいますが、暗黙のNSSet!型なので通常のNSSet型とみなしてアクセスできるわけです。(まあ本当にnilが入っていたらアウトですけど)
では話をbeta6のリリースノートに戻します。暗黙のOptional型が使われている一部の箇所について見直されました。その結果、「ほんとにもうっ、全然nilなんて入らないんだからねっ!」という場所は通常型に修正され、「いや〜結構nilが入るのでちゃんとnilチェックして欲しいな・・」という場所は明示的なOptional型に修正されました。前回までのオセロプログラムでもこのエラーが1箇所でました。
init(coder aDecoder: NSCoder!) { ... }
上のメソッドは前回話題にしたUIViewのイニシャライザですが、以下のように通常型の引数に修正されました。
init(coder aDecoder: NSCoder) { ... }
aDecoderにnilなんて入らないというわけです。ちょっと補足します。NSCoderはクラスですのでaDecoderは参照型変数です。JavaやC#であればnullを設定できる変数ですね。しかしSwiftではaDecoder変数が書き換え可能だったとしてもnilを設定できません。SwiftのnilはJavaやC#のnull、CやObjCのNULLとは異なります。nilというリテラル値はOptional型変数にしか設定できません。Swiftの参照型変数はnilにならないので値として扱いやすく、またOptional型のおかげでnilのあるところが分かりやすくていいですね。
えっと、今回もVerUpネタに時間をとってしましましたが、気を取り直してオセロの続きをやりたいと思います。オセロは自分の石で相手の石を挟むとひっくり返すことができます。この部分を作りたいと思います。石を置いた場所から上下左右+斜めの8方向に対してひっくり返せるかチェックする必要があります。まずは左方向にひっくり返せるかを例にして考えましょう。
●○○○
という状態で白石の隣に黒石を置く場合のアルゴリズムを考えます。
①すぐ左隣が黒石ではなく白石があることを確認したら、
②白石が途切れるまで、つまり黒石が見つかるまで繰り返し左隣のチェックを行います。黒石が見つからなければひっくり返せません。
③黒石が見つかったら今度は間にある白石を繰り返しひっくり返します。
②と③は繰り返し処理を含んでいますね。②のループで順番にチェックし、③のループで今度はひっくり返すという2回のループで書くことができそうです。ですが、もうちょっと動きを観察すると、②で左隣を順にチェックし、③はその逆を辿るようにひっくり返しながら戻ってくるという方法も考えられます。後者は再帰関数で書くとうまくいきそうです。関数名はひっくり返すループですのでflipLoop関数にします。
書き始める前にもう少し細かい点を説明します。なるべくメソッドにはせず関数で書いてみようと考えているので、グローバルスコープ関数として書きます。また盤面上の石をひっくり返す方法ですが、OthelloViewのメンバにいるboardをflipLoop関数から書き換えてしまうことは副作用になりますので避けようと思います。一つの方法として、関数内で新しいboardのコピーを用意して、その上でひっくり返した結果を返すという方法が考えられます。しかし、flipLoopは頻繁に呼ばれそうです。さらにboardをリストではなく配列として定義しているため、Immutableに扱うためには部分変更でも全体をコピーすることになりパフォーマンスが悪そうです。そこで別の方法として、ひっくり返す相手の石の位置(x,y)を配列にして返すようにして、実際の副作用(ひっくり返し)は後でやることにします。ではまずは関数のI/Fから。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int) -> [(Int, Int)]? {
return nil
}
現在のboardと石を置く位置xyと石の種類(黒/白)をもらって、ひっくり返す場所を配列にして返すという関数です。戻り値型はOptionalにしておいて、ひっくり返す場所が無い場合はnilを返すようにします。では中身を書いていきましょう。まずは再帰的にチェックしていった場合に、黒でも白でもなく空のマスに辿り着いてしまったら、ひっくり返せないのでnilを返して再帰ループを停止します。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int) -> [(Int, Int)]? {
if board[y][x] == EMPTY {
return nil
}
return nil
}
次は再帰チェックで自分の石を見つけた場合です。ここまでひっくり返せるので再帰を停止します。再帰の巻き戻し時に相手の石の場所を格納するための空配列[]を返しましょう。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int) -> [(Int, Int)]? {
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return [] // 再帰の果てに自分の石を発見(ひっくり返せる!)
}
return nil
}
最後は相手の石を見つけた場合です。さらに左隣をチェックするため再帰呼び出しをしましょう。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int) -> [(Int, Int)]? {
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else {
var result = flipLoop(board, x-1, y, stone) // さらに左隣を調べる
}
return nil
}
再帰呼び出しの戻り値resultはどうしましょう。nilが入っていたら再帰の果てに自分の石が無かったのですから、そのままnilを返しましょう。配列が返ってきた場合、再帰の果てに自分の石があったということですので、今調べた相手の石をひっくり返すために配列に加えましょう。というわけで以下のようになりました。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int) -> [(Int, Int)]? {
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else if var result = flipLoop(board, x-1, y, stone) {
result += [(x, y)] // nilじゃなければ相手の石を配列に追加して返す
return result
}
return nil
}
再帰呼び出しの停止条件を確認しておきましょう。自分の石が見つかるか空っぽのマスが見つかれば停止します。boardの定義を思い出して下さい。8x8マスの周囲にぐるりと余分な領域を持たせていました。ここには石を置けませんので、必ず最後には空っぽのマスが見つかり無事停止します。
さて、まだまだいじります。今のところ左隣だけしか調べられませんので、走査する方向を引数でもらいましょう。x方向とy方向の増減分ということでdxとdyを引数に追加しました。
func flipLoop(board:[[Int]], x:Int, y:Int, stone:Int, dx:Int, dy:Int) -> [(Int, Int)]? {
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else if var result = flipLoop(board, x+dx, y+dy, stone, dx, dy) { // dx,dy方向に進む
result += [(x, y)]
return result
}
return nil
}
このままでもいいのですがちょっと突っ込みます。再帰関数のルールとして、呼び出し時にパラメータが変化しなければなりません。flipLoop関数も第2、3パラメータが変化します。ところがそれ以外の4つのパラメータは変わっていません。これらは再帰に本質的には関わっておらず、再帰パラメータである必要がないと言えます。こういった変数はパラメータの外に出すことが可能です。というわけでクロージャを使って外側の関数スコープに移してみましょう。外側の関数の名前は、ええと、ある方向に対してひっくり返し処理を行うという意味でflipLine関数とします。flipLine関数のスコープに再帰に不要な変数を保持するとして、クロージャはどうやって作りましょう。SwiftはJavaScriptやC++のような関数内関数を定義することもできますが、今回は関数リテラル(無名関数)で書いてみます。(そう言えばC++11からC++もクロージャが使えると聞いたことがあるのですが試したことはないです。どんな風に書くのかな・・)
Swiftの関数リテラルは以下のように書きます。
{ (引数) -> 戻り値型 in 本体 }
中括弧で全体を囲み、inの前に関数の型、inの後に本体を書きます。上記は基本形ですが、Swiftの関数リテラルは条件が揃えば色々記述を省略することができ、{本体だけ}のように短く書くこともできます(Scalaっぽい書き方です)。またRubyのコードブロックのように高階メソッドの直後にくっつける記法もあります。興味のある方は言語マニュアルを参照してみて下さい。
ではflipLine関数の中にflipLoopを書いてみます。
func flipLine(board:[[Int]], x:Int, y:Int, stone:Int, dx:Int, dy:Int) -> [(Int, Int)]? {
var flipLoop = { (x: Int, y: Int) -> [(Int, Int)]? in
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else if var result = flipLoop(x+dx, y+dy) {
result += [(x, y)]
return result
}
return nil
}
return nil
}
どうでしょうか。flipLoopをクロージャにしたので引数が再帰パラメータだけになりました。それではクロージャを呼び出すコードも追加しておきましょう。
func flipLine(board:[[Int]], x:Int, y:Int, stone:Int, dx:Int, dy:Int) -> [(Int, Int)] {
var flipLoop = { (x: Int, y: Int) -> [(Int, Int)]? in
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else if var result = flipLoop(x+dx, y+dy) {
result += [(x, y)]
return result
}
return nil
}
if let result = flipLoop(x+dx, y+dy) { // クロージャの起動
return result
}
return [] // flipLineの戻りはnilにする必要はない。ひっくり返す場所が無いなら空配列にします。
}
flipLine関数の戻り値型を見て下さい。Optional型をやめました。flipLoopの戻り値ではnilと[]の区別が必要なのでOptionalにしていますが、flipLineはひっくり返す位置が無い場合は空配列を返せば十分です。さて、以上で完成ならばいいのですが、なんとこれコンパイルが通りません:P 上のコードの7行目でflipLoopを再帰呼び出ししていますが、flipLoopが見つからないって怒られます。どうやら関数リテラルは再帰呼び出しできないみたいです。beta6のリリースノートを見ると「関数内関数で再帰呼び出し不可」という既知の問題がありました。こんな感じのです。
func foo() {
func bar() { bar() } // 再帰呼び出しはコンパイルエラー
}
関数内関数と関数リテラルの問題が同根原因かどうか分かりませんが、現在はflipLoopのような再帰関数リテラルはコンパイルできません。そこでStackOverflowを検索してみると回避策が見つかりました。まずは同型のダミー関数オブジェクトを生成し、関数リテラルの中から前方参照することで回避できるようです。こんな感じでようやく最終版です。
func flipLine(board:[[Int]], x:Int, y:Int, stone:Int, dx:Int, dy:Int) -> [(Int, Int)] {
var flipLoop: (Int, Int) -> [(Int, Int)]? = { _ in nil } // ダミー関数オブジェクト
flipLoop = { (x: Int, y: Int) -> [(Int, Int)]? in
if board[y][x] == EMPTY {
return nil
} else if board[y][x] == stone {
return []
} else if var result = flipLoop(x+dx, y+dy) {
result += [(x, y)]
return result
}
return nil
}
if let result = flipLoop(x+dx, y+dy) {
return result
}
return []
}
再帰関数リテラルを書く前に変数を同じ型で宣言し、初期値として適当な関数オブジェクトを入れておきます。inの前のアンダースコアですが、プレースホルダとして機能する点はいいとして、なぜ1個でいいのか。言語マニュアルには書かれていませんが、Swiftでは引数が複数あっても実はタプルとして一つにまとめられているとのこと。詳しくはQiitaのdankogaiさんの記事で分析されています。興味のある方は検索してみて下さい。ともかく、これでようやく「一方向のひっくり返し関数」が完成しました。ふう。
今回はここまでにして、そろそろですね!iPhone6の発表イベント。9月9日(米時間)だそうです。関連情報を随分見かけるようになりました。大きなiPadやiWatchなんてのも期待されています。本体ハードウエアにも興味がありますが、発売の少し前になればiOS8のリリースがあるはずですね!iOS8はiOS7のようなUIデザインの大きな変更は無いでしょうが、APIがたくさん増えるようです。HomeKit(家電)、HealthKit(健康)のような新しいフレームワークを使ってどんなアプリやサービスが登場するのでしょうね。他にもWebKitやCloudKitなるものや、フォトライブラリやカメラ、CoreAudioにも手が入るとか。PromiseKitという非同期処理フレームワークは本コラムでのネタになりそうです。個人的に興味があるのはSceneKitというApple純正3D描画+物理演算フレームワークです。OSXだけでなくiOSでも動くようになるとのことで、この機会にトライしてみたいです。描画と言えばMetalというA7チップ用の高速グラフィックスAPIも発表され、これからどんなアプリが登場するのかとても楽しみです。
以上