いものやま。

雑多な知識の寄せ集め

変種オセロをSwiftに移植してみた。(その9)

昨日の記事は以下。

残すは実際のプレイヤーの実装。

プレイヤーの実装をするためのTips

標準入力、標準出力の扱い

さて、プレイヤーを実装しようとするとまず必要となるのが、標準入力から文字列を受け取るという処理。
ただ、なんということか・・・そんな基本的な機能がデフォルトで用意されていないorz

まず、標準入力、標準出力のオブジェクトは、NSFileHandleのタイプメソッドを使って、以下のようにして取得できる。

// 標準入力
let stdin: NSFileHandle = NSFileHandle.fileHandleWithStandardInput()
// 標準出力
let stdout: NSFileHandle = NSFileHandle.fileHandleWithStandardOutput()

NSFileHandleのメソッドにはavailableDataというメソッドがあって、これを使うと入力された文字のデータを取得できる。
ただし、NSDataオブジェクトで。
なので、これを文字列にするには、次のようにしないといけない。

// コンソールで改行が押されるまで待ち、
// 改行が押されたら一行分のデータ(改行を含む)を返す。
let data: NSData = stdin.availableData
// NSDataからNSStringを得る。
let string: NSString = NSString(data: data, encoding: NSUTF8StringEncoding)!

なお、NSString#init?(data: NSData, encoding: UInt)は、失敗するかもしれないイニシャライザで、オプショナル型のオブジェクトを返すので、アンラップする必要がある。

さらに、改行や行頭/行末の半角スペースは不要なので、これを取り除いた文字列を取得するために、次のようにする。

let trimmedString: NSString = string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())

無駄に長いw

ここまでしてやっと使える文字列が手に入る感じ。

一方、出力については、NSFileHandleにはwriteDataというメソッドが用意されているのだけど、これまた引数はNSDataのオブジェクト。
なので、NSStringをNSDataオブジェクトに変換してやる必要がある。

let data: NSData = string.dataUsingEncoding(NSUTF8StringEncoding)!
stdout.writeData(data)

なお、NSString#dataUsingEncoding(_: UInt)もオプショナル型のオブジェクトを返すので、アンラップする必要がある。

それと、NSFileHandle#writeData(_: NSData)はflushの必要はないっぽい。
(最初、フラッシュするためにNSFileHandle#synchronizeFile()が必要かと思ったのだけど、不要だった)

NSFileHandleの拡張

これらの処理をいちいち書いていたら面倒なので、NSFileHandleを拡張して、NSFileHandle#readString()というメソッドと、NSFileHandle#writeString(_: String)というメソッドを用意した。

/* Human.swift */

import Foundation

extension NSFileHandle {
  func readString() -> String {
    let data = self.availableData
    let string = NSString(data: data, encoding: NSUTF8StringEncoding)!
    return string.stringByTrimmingCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet())
  }

  func writeString(string: String) -> Void {
    let data = string.dataUsingEncoding(NSUTF8StringEncoding)!
    self.writeData(data)
  }
}

// 続く

これで、次のように標準入出力を使うことが出来る。

// 一行読み込む
let stdin = NSFileHandle.fileHandleWithStandardInput()
let string = stdin.readString()

// 一行書き出す
let stdout = NSFileHandle.fileHandleWithStandardOutput()
stdout.writeString("Hello, world!")  // 改行されないので注意(出力自体はすぐにされる)

なお、別の方法として、組込み変数のstdin: UnsafeMutablePointer<FILE>と標準Cライブラリの関数を使うという方法もあるけど、そちらはそちらでメモリを管理しないといけないとかがあって、かなり面倒。

正規表現による文字列の分割

標準入力から文字列を受け取ったら、今度はそれをパースしてやらないといけない。
そのために、正規表現を使って文字列を分割したいと思ったのだけど、これもない・・・orz

StackOverflowなどを見てみると、正規表現にマッチする部分をセパレータとなる別の文字列に置き換えて、そのセパレータの文字列を使って文字列の分割をする方法が書かれていたのだけど、なんと原始的な・・・
Perlで昔、データベースをファイルに保存するときにはよくそんな手法が取られていたけど(データベースの1レコードを<>という文字列で連結して一行の文字列にして書き出しておいて、読み出したときには<>をセパレータとして分割することで元のデータを得る)、今の時代になってそんな手法を見ることになるとは思わなかった。
それにこの手法の場合、仮にセパレータの文字列が元の文字列に含まれていた場合、正しい結果が得られなくなってしまう。

そこで、NSStringを拡張して、ちゃんとしたメソッドを書いてみた。

// 続き

extension NSString {
  func split(regexp: NSRegularExpression) -> [String] {
    var ret = [String]()

    var searchRange = NSRange(location: 0, length: self.length)
    while searchRange.location < self.length {
      var resultRange = regexp.rangeOfFirstMatchInString(self as String,
                                options: NSMatchingOptions(0),
                                range: searchRange)
      if resultRange.location == NSNotFound {
        resultRange.location = self.length
        resultRange.length = 0
      }

      let substringRange = NSRange(location: searchRange.location,
                      length: resultRange.location - searchRange.location)
      let substring = self.substringWithRange(substringRange)
      if !substring.isEmpty {
        ret.append(substring)
      }
      searchRange.location = resultRange.location + resultRange.length
      searchRange.length = self.length - searchRange.location
    }

    return ret
  }
}

// 続く

やってる内容は、最初は文字列全体を正規表現の検索対象にして、正規表現にマッチする部分を探し、ヒットしたら、検索の開始地点〜ヒットした部分の直前までを部分文字列として取得して、今度は正規表現の検索対象を今マッチした部分より後ろにする、というのを繰り返しているだけ。
これでちゃんと正規表現を使って文字列の分割が出来るようになった。

プレイヤーの実装

あとは、プレイヤーの実装だけ。

// 続き

public class Human: Player {
  private static let separator = NSRegularExpression(pattern: "\\s*[\\s,]\\s*",
                    options: NSRegularExpressionOptions(0),
                    error: nil)!

  public func select(board: Board) -> Board.Action? {
    println("playable  : \(board.playablePlaces)")
    println("changeable: \(board.changeablePlaces)")

    let stdin = NSFileHandle.fileHandleWithStandardInput()
    let stdout = NSFileHandle.fileHandleWithStandardOutput()

    var action: Board.Action? = nil
    while action == nil {
      stdout.writeString("> ")
      let input = stdin.readString()

      let (command, args) = self.getCommandAndArgs(input)
      if command == "e" {
        break
      }

      action = self.getAction(board, command, args)
      if action == nil {
        println("input 'play row, col', 'change row, col', or 'exit'.")
      }
    }
    return action
  }

  private func getCommandAndArgs(string: String) -> (String, [String]) {
    var components = string.split(Human.separator)
    if components.count == 0 {
      return ("", [])
    }
    let command = (components.removeAtIndex(0) as NSString).substringToIndex(1)
    return (command, components)
  }

  private func getAction(board: Board, _ command: String, _ args: [String]) -> Board.Action? {
    if args.count < 2 {
      return nil
    }

    let row = args[0].toInt() ?? 0
    let col = args[1].toInt() ?? 0
    if (row < Board.ROW_MIN ) || (Board.ROW_MAX < row) ||
        (col < Board.COL_MIN) || (Board.COL_MAX < col) {
      return nil
    }

    switch command {
    case "p":
      if board.isPlayable(row, col) {
        return Board.Action.Play(row, col)
      } else {
        return nil
      }
    case "c":
      if board.isChangeable(row, col) {
        return Board.Action.Change(row, col)
      } else {
        return nil
      }
    default:
      return nil
    }
  }
}

といっても、見ての通り、かなり苦しい実装・・・
オプショナル型がホントうらめしく、フローをキレイに書くことが難しすぎる。

何が困るって、nilが「正常な状態」なこともあるし、「異常な状態」を示すこともあるということ。
「例外処理」の重要なポイントの一つは、正常な出口とは別に異常時の出口を用意しているという点で、これが明確に分かれていることで、フローをキレイに書くことが出来るようになっている。
なのに、Swiftの場合、言語設計者がこの辺りの歴史的な経緯を知らないのかなんなのか、正常な場合も異常な場合も出口は同じで、異常な場合はnilを返しておけばいいだろうとか考えているのがダメ。
nilが返ってきても正常という場合はあるし、それが異常という場合もあるんだよ・・・
(※なお、Swift 2.0では例外処理が機能として追加されてる。なら、オプショナル型は滅んでいいと思う)

まぁ、愚痴はこの辺にして。

これでプレイヤーの実装も出来たので、次のようなコードを用意してコンパイルすれば、実際に遊ぶことが出来る。
(※プレイグラウンドではコンソールから標準入力を得ることは出来ないので、コンパイルが必要)

/* humangame.swift */

let human = Human()
let game = Game(blackPlayer: human, whitePlayer: human)
game.start()

今日はここまで!