yashiganiの英傑になるまで死ねない日記

週末はマスターバイクでハイラルを走り回ります

Promiseを学ぶためにSwiftでPromiseを実装してみた話

この記事ははてなデベロッパーアドベントカレンダー2015の16日目の記事です.昨日は id:motemenエンジニア寿司を支える技術 - Hatena Developer Blog でした.


こんにちは.id:yashigani_w です. はてなでは定期的に開発合宿があり,好きな開発言語を試したり,普段仕事では一緒にならないメンバーとサービス開発をすることができます. 今年の合宿で私が所属したチームでは,node.jsとtypescriptを使い,Webサービスを開発しました. 私は普段iOSアプリ開発を担当しているので,あまりサーバサイドの実装をすることはありませんし,JavaScriptもあまり得意ではないのですが,開発合宿の機会を使って新たな技術に挑戦してみました.

合宿を前に技術的に不安を抱えていた私は,あるチームメンバーに「事前になにを学んでおけばいいか」と訪ねたところ,「Promiseをよく使うから理解しておいてくれ」と助言されました. Promiseについて調べはじめると「これはSwiftで実装するとおもしろそうだぞ」と感じ,学習がてらSwiftで実装してみることにしました.

Promiseとは

実装するためには,まずPromiseについて理解を深める必要があります. そもそもPromiseとはなんなのでしょうか? Promiseは,非同期処理を内包したオブジェクトで,それに対して処理を追加していくことができるのが特徴です. 今回の元ネタとなるJavaScriptでは,Promiseを使ってこのようなコードを書くことができます.

// http://azu.github.io/promises-book/#how-to-write-promiseより抜粋

function getURL(URL) {
    return new Promise(function (resolve, reject) {
        var req = new XMLHttpRequest();
        req.open('GET', URL, true);
        req.onload = function () {
            if (req.status === 200) {
                resolve(req.responseText);
            } else {
                reject(new Error(req.statusText));
            }
        };
        req.onerror = function () {
            reject(new Error(req.statusText));
        };
        req.send();
    });
}
// 実行例
var URL = "http://httpbin.org/get";
getURL(URL).then(function onFulfilled(value){
    console.log(value);
}).catch(function onRejected(error){
    console.error(error);
});

非同期処理がうまく隠蔽されているうえに,結果に対する操作を逐次実行のように加える事ができます.

このPromiseですが,Promise - JavaScript | MDN を参照すると,意外とそれ自体のAPIはさほど多くありません. 非同期処理を隠蔽すること,状態と値を持つこと,thencatchで別のPromiseを作って返すことの3つを抑えれば実装できそうです. 今回はこのドキュメントを参照しつつ実装してみました.

非同期処理を隠蔽する

Swiftで非同期処理するならGCDを使います. (SwiftOSS化に伴い,コアライブラリとして公開されるのもあって,安心して使えます) といっても,そのままinitに与えられたclosureを非同期実行するだけです.

public init(_ executor: (T -> Void, ErrorType -> Void) -> Void) {
  dispatch_async(queue, {
    executor(self.onFulfilled, self.onRejected)
  })
}

onFulfilledonRejectedは後述するPromiseの状態を変更するためのメソッドです.

状態と値

Promiseには Pending/fulfilled/rejected の3つの状態があります. 加えて,fulfilledrejected の状態では非同期処理の結果によって得られた値かエラーを持っています. それぞれSwiftではenumを使って表現するのがよいでしょう. 値については任意の型を使えるようにしたいので,ジェネリクスenumを組み合わせてこのように表現することにしました.

// 状態
enum State {
  case Pending
  case Fulfilled
  case Rejected
}

// 結果の値かエラー
enum Result<T> {
  case Undefined
  case Value(T)
  case Error(ErrorType)
}

そしてこれらを持つPromiseを定義します.

public final class Promise<T> {
  internal private(set) var state: State = .Pending
  internal private(set) var result: Result<T> = .Undefined

  public init(_ executor: (T -> Void, ErrorType -> Void) -> Void) {
    dispatch_async(queue, {
      executor(self.onFulfilled, self.onRejected)
    })
  }

  private func onFulfilled(value: T) {
    if case .Pending = state {
      result = .Value(value)
      state = .Fulfilled
    }
  }

  private func onRejected(error: ErrorType) {
    if case .Pending = state {
      result = .Error(error)
      state = .Rejected
    }
  }

}

onFulfilledonRejectedでは複数回実行されないように.Pendingのときのみ状態を変更するようにしています. また,外部から状態を変更されると困る部分に関してはprivateでアクセス制御をしておきます. これで,T型の値を持つPromise<T>が定義できました.

thencatch

thencatchはPromiseに処理を追加し,新しいPromiseを返します. catchthen(null, onRejcted)のショートハンドとみなすことができますので,実装自体はthenのみを考えれば事足ります.

元のPromiseの非同期処理が終了している場合(fulfilled または rejected であるとき)は,即時で与えられた関数を実行して新しいPromiseを返すことができますが,そうでない場合(pending の状態)では,非同期処理の完了を待つ必要があります. そこで,元のPromiseの処理の完了を待つために,プロパティに非同期処理の完了とともに実行されるclosureを追加します.

public final class Promise<T> {
  internal private(set) var state: State = .Pending {
    didSet {
      if case .Pending = oldValue {
        switch (state, result) {
        case (.Fulfilled, .Value(let value)):
          resolve?(value)
        case (.Rejected, .Error(let error)):
          reject?(error)
        default: ()
        }
      }
    }
  }
  private var resolve: (T -> Void)?
  private var reject: (ErrorType -> Void)?

  // 省略...

  private func onFulfilled(value: T) {
    if case .Pending = state {
      result = .Value(value)
      state = .Fulfilled
    }
  }

  private func onRejected(error: ErrorType) {
    if case .Pending = state {
        result = .Error(error)
        state = .Rejected
    }
  }

}

state.Pendingから変化したときに実行します. thenでこれらのプロパティにclosureをセットすれば非同期実行を待つことができます.

また,今回はPromiseをPromise<T>としましたが,thenの操作はPromise<T>からPromise<U>に写す操作だととらえることができますので,thenはこのように定義します.

func then<U>(onFulfilled: T -> U, _ onRejected: (ErrorType -> Void)?) -> Promise<U> {
  return Promise<U> { _resolve, _reject in
    switch (self.state, self.result) {
    case (.Pending, _):
      let resolve = self.resolve
      self.resolve = {
        resolve?($0)
        _resolve(onFulfilled($0))
      }
      let reject = self.reject
      self.reject = {
        reject?($0)
        _reject($0)
        onRejected?($0)
      }
    case (.Fulfilled, .Value(let value)):
      _resolve(onFulfilled(value))
    case (.Rejected, .Error(let error)):
      _reject(error)
      onRejected?(error)
    default:
      assertionFailure()
    }
  }
}

元のPromiseの状態によって処理を分岐しています.

thenができたので,これを使ってcatchが実装できます.

public func `catch`(onRejected: ErrorType -> Void) -> Promise<T> {
  return then({ $0 }, onRejected)
}

ここではthenの第一引数に引数をそのまま返すclosureを渡すのがポイントです.

これで基本的な機能は実装できました.

その他のAPIを実装する

ES6のPromiseには他にもいくつかのAPIがありますが,基本的にいままで実装したものを組み合わせれば実装することができます. 例えば,Promise.resolveではこのように非同期処理の実行を待ちます.

public static func resolve(value: T) -> Promise<T> {
    let semaphore = dispatch_semaphore_create(0)
    let promise = Promise { resolve, _ in
        resolve(value)
        dispatch_semaphore_signal(semaphore)
    }
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
    return promise
}

Swiftっぽく味付けをする

ここまではなるべくES6のPromiseに則った実装をしましたが,せっかくSwiftで実装しているので少しSwiftっぽさを効かせたいと思います.

thenオーバーロードしてtrailing closureを使えるようにする

ES6ではthenメソッドp.then(onFulfilled, onRejected)のように定義されていますが,Swiftではオーバーロードし1引数のものと2引数のものを分けたほうがよいでしょう.

// 2引数then
public func then<U>(onFulfilled: T -> U, _ onRejected: ErrorType -> Void) -> Promise<U> {
}

// 1引数then
public func then<U>(onFulfilled: T -> U) -> Promise<U> {
}

Swiftではデフォルト引数を与えることでメソッド呼び出し時の引数を省略できます. しかし,2引数のonRejectedをOptionalにしデフォルト引数にnilを与えるスタイルだと,trailing closure(引数の最後のclosureはメソッド呼び出しの外に書くことができる記法)が使えません.

// デフォルト引数の場合trailing cosureが使えない
Promise.resolve(1)
       .then { $0 * 2 }
       .then { $0 + 1 }

このようなパターンではデフォルト引数ではなく,オーバーロードを使うほうが良いといえます.

autoclosureを使って便利イニシャライザを追加する

JSONのパース結果など,エラーを発生させる関数の結果をそのままPromiseに包みたくなることがあるかもしれません. その都度,

let p = Promise { onFulfilled, onRejected in
  do {
    let obj = try NSJSONSerialization.JSONObjectWithData(data, options: [])
    onFulfilled(obj)
  } catch {
    onRejected(error)
  }
}

のようにエラーハンドリングを書くのは面倒です. そこで,autoclosureを使ってこのようなイニシャライザを追加し,エラーハンドリングを隠蔽すると便利です.

public convenience init(@autoclosure(escaping) _ executor: () throws -> T) {
  self.init { resolve, reject in
    do {
      let v = try executor()
      resolve(v)
    } catch {
      reject(error)
    }
  }
}

autoclosureな引数は,自動でclosureに包まれます. escapingはこのclosureは即時に実行されるという意味で,この関数が外の値をキャプチャしないことを示します. この便利イニシャライザによって,元のコードはこのような形でPromiseに包むことができるようになります.

let p = Promise(try NSJSONSerialization.JSONObjectWithData(data, options: []))

autoclosure便利ですね.

まとめ

新しい概念を学習するために実際に実装してみるのは学習効果がとても高く,なにより楽しむことができました. 当初APIばかりを読んで実装していたのですが,型が無いとその本質を捉えるのが難しく,インターフェースをすばやく理解するために型は有益な情報なんだと再認識する機会にもなりました.

最後に申し訳程度にSwiftっぽく味付けをしてみましたが,catch予約語なので使うときにエスケープが必要なのであまり好ましくありません. APIについては一考の余地がありそうです.

今回紹介したPromiseは以下のリポジトリで公開しています. テストコード以上の利用例はありませんので常用に耐えうるかまではわかりません. また,Promiseの仕様は promises-aplus/promises-spec · GitHub で策定されていますが,今回の実装はこちらの全てを網羅しているわけではないことに注意してください. CocoaPodsやCarthageなどのパッケージマネージャでもインストールできますので,興味があれば使ってみてください.

後日談

合宿では主にasync/awaitを使ったので,Promiseを直接使うことはほぼありませんでした.

参考資料


明日のアドベントカレンダーid:wtatsuru です. お楽しみに!

UITableViewでレイアウトの違うセルをどう扱うか #関モバ

毎月恒例の関モバ #8に参加しました.

kanmoba.connpass.com

今回は,UITableView複数のレイアウトを持つセルをどう扱うかというトークをしました.

speakerdeck.com

骨子としては,UITableView複数のレイアウトを持つセルを実装するとき,いくつかの方法がありますがコードでレイアウトをいじるとAuto Layoutの指定が複雑になるので,レイアウトのパターンごとにxibを分けるといいというものです.

複数のxibを使うことについて少し補足

UITableViewにおいてxibを増やす場合,xibの登録やセルのリユースなどセルを使うための決め事が複雑になることが問題になります. セルを複数のView Controllerで使いまわしたい場合,それぞれで同じ実装をすることが強いられます. そこで,このトークでは実装パターンとしてEntryCellRepresentableのようなprotocolを使うことを紹介しました. 紹介したものは単純で以下のような実装です. ※ この例ではEntryCellに対して複数のxib(EntryCell.xibとEntryCellWithImage.xibの2つ)が存在し,それぞれでレイアウトを定義することを前提としています(画像があるパターンと無いパターンです).

protocol EntryCellRepresentable {
  var tableView: UITableView! { get }
  func registerEntryCell()
  func dequeueEntryCell(entry: Entry, forIndexPath indexPath: NSIndexPath) -> EntryCell
}

extension EntryCellRepresentable {
  func registerEntryCell() {
    ["EntryCell", "EntryCellWithImage"].forEach {
      tableView.registerNib(UINib(nibName: $0, bundle: nil), forCellReuseIdentifier: $0)
    }
  }

  func dequeueEntryCell(entry: Entry, forIndexPath indexPath: NSIndexPath) -> EntryCell {
    let reuseIdentifier = entry.hasImage ? "EntryCellWithImage" : "EntryCell"
    let cell = tableView.dequeueCellReusableCellWithIdentifier(reuseIdentifier, forIndexPath: indexPath) as! EntryCell
    cell.entry = entry
    return cell
  }
}

EntryCellRepresentableEntryCellを使ううえでの規約(全てのパターンのxibを漏れなくregisterNibする,dequeueCellReusableCellWithIdentifierし,モデルであるEntryをセットする)を隠蔽します. UITableViewを使うとき,多くはUITableViewControllerのサブクラスを使うことでしょう. UITableViewControllerはプロパティにvar tableView: UITableView!を持っていますので,EntryCellRepresentableを指定するだけでprotocolに適合させることができます. したがって,EntryCellを表示するclass EntriesViewController: UITableViewControllerはこのように実装できます.

class EntriesViewController: UITableViewController, EntryCellRepresentable {
  override func viewDidLoad() {
    super.viewDidLoad()
    registerEntryCell()
  }

  override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let entry = entries[indexPath.row]
    return dequeueEntryCell(entry, forIndexPath: indexPath)
  }
}

UITableViewを自分で追加する場合はvar tableView: UITableView!を実装するだけです. このような工夫によって,使いまわしやすさを維持しつつxibを分けることが可能です.

unwrapして値を棄てるときを少し小洒落たかんじに

たまにunwrapするけど,必要なのはその有無でそのままそれを棄てることがあります. こういうときです.

if let a = some {
    print("some is .Some")
}

このままだとXcodeに,aを使ってないから_に変えろって怒られたりしてゲンナリ………オシャレ感ゼロです. そんなとき,パターンマッチを使うとちょっと小洒落たかんじになります.

if case .Some = some {
    print("some is .Some")
}

同様に,nilかどうかを確かめたいときもsome == nilよりパターンマッチを使うほうが小洒落てます.

if case .None = some {
    print("some is .None")
}

最近Swiftを始めた同僚がif caseなんてあるんですねって言ってたときに,「あんま使わねーな」とおもってたんですけど,それから意識してみると小洒落たコードが書けました.

追記

次の日また別の同僚と話していたら,「SwiftってOptionalの存在を極力隠しているからnilとの比較のほうがよくない?」って言われました. たしかに,Optionalの正体を知っていると別になんともないコードなんですが,そうでないと読み解くのが難しいかもしれません. 別段パターンマッチが必要な場面ではありませんしnilとの比較のほうが凡庸なので,普段はそっちを使ったほうがいいなとおもいました. オシャクソ野郎は使え. こちらからは以上です.