読者です 読者をやめる 読者になる 読者になる

yashigani?.days

週刊少年ジャンプについてだらだら書きます

筋の悪いコードレビュー

IMO

たまにコードレビューのしかたを紹介するエントリーを目にすることがある。その中でも反響があるものは実際によくまとまっており、なるほどと感心することもよくある。しかし、中にはコーディングスタイルやイディオム、ちょっとしたテクニックを使うように目を光らせるように助言されているものがある。誤解を恐れずに書くと、それらのトピックは無視できるようなものばかりであり、そのような指摘ばかりつけるのはあまり褒められたものではないなと感じる。これは私自身にも言えることだが、瑣末ことを指摘しすぎてしまうあまり、本質的な議論が霞んでしまうことがままある。レビュイーにとって、指摘が学びになることは確かに存在するのだが、コードレビューの目的はあるべき実装を議論するものであり、老婆心からそれを繰り返すことは必ずしも良いレビューとは言えない。

では、本当に良いコードレビューとはなにか。それは、コーディングスタイルのような事項の指摘ではなく、アプリケーションにとってその実装が好ましいかどうかを議論するものだろう。実際、私がもらった良いレビューを思い返すと、そういうものばかりが思い出される。つまり、良いコードレビューはプロダクトのドメインに深く根ざしているため、おいそれと具体的なケースを公開できることはないはずなのである。

iOSDCにCFPを出しました

昨晩、同僚とiOSDCのCFPソンをしていたら盛り上がって2本応募しました。よかったら投票してください。

ここ半年くらい、はてなブックマークのチームでスクラムマスターをやっているんですが、このチームでは高速にチームビルディング、カイゼン、リリースを並行させることができました。 そこで得た知見について話します。

Dependency Injectionは依存の注入だけでなく、ユニットテストや依存関係の整理にも有効な手法です。 過去関モバで2回LTしているんですが、短い尺だとエッセンスがあまり伝わりづらかったようなので長めの尺でトライします。

同僚のCFPを紹介します

どれもおもしろそうなトークです。気になるものがあれば是非投票してください。

明日は金曜日

IMO

東京へ日帰り出張、疲れた体を新幹線のシートに預ける。「帰りの新幹線ではビールを飲むんだ」チーフエンジニアの教えである。共に帰ることになった同僚と弁当の封を開け、缶ビールのプルタブを引く。東京は何度行っても疲れるけれど、帰りの新幹線の雰囲気は嫌いではない。そこかしらで私たちと同じように弁当を広げ、缶ビールの栓を抜く。車内は小さな幸せと笑顔で溢れている。そんなことを感じながら京都までの2時間半を過ごした。明日は金曜日だ。

人の挨拶を嗤うな

IMO

挨拶がないことや挨拶もできないのかと怒る人がたまにいる。けど、そういう人に限って自分から挨拶しないという経験則があって、挨拶する側じゃなくて挨拶される側の理論だなと耳にするたびにおもう。唯一例外は学校の先生で、彼らは挨拶しろと怒鳴りつけて、実際に挨拶してる。これは信条の話だが、コミュニケーション不全の原因を相手に押しつけるべきではない。挨拶されないのはされない側にされないなりの理由があるわけで、怒るくらい気に入らないなら人を変える前に自分を変えるべき。平たく言うと、挨拶しない側にとって存在感が無い。挨拶されるために自分を変えるのはいかにもコスパが悪いと感じるかもしれないが、気になるあの子に声かけられるために自分を変える、と言いかえるとなんか尊い感じがする。そういう感じ。

挨拶されないことに怒るより、自分から挨拶したほうが生まれる不幸は少ない。とやかく怒らずに普通に挨拶をすればよい。それを踏まえて私がどう振る舞いたいかというと、自分から挨拶したがらない人間に対して大声で挨拶する、そういう人間でありたい。

てんとまる

IMO

Twitterやめたら、インターネットに発信する機会がなくなってさみしいので久しぶりにブログを書くことにした。ちなみに、やめたというのは読んだり投稿したりするのをやめただけで、セルフブランディングを大きく毀損するのでアカウントは残していて、アカウントを削除して黒歴史を封印!みたいな根性めいたものではない。

5、6年前くらいから自分の書く文章では特に縛りがない限り句読点にカンマとピリオドを使っていた。たしかMacを買ったのがきっかけだったとおもう。なぜカンマとピリオドを使っていたかというと、シュッとしていてかっこいいからで、それ以上もそれ以下の理由もなかった。 Macでカンマとピリオドを使うのは簡単で、ことえりなりIMEの設定を変えればよい。しかし、問題なのはiPhoneで、iPhoneはそういう痒いところに手が届く設定が存在しないので、毎度変換するという苦行とポリシーを選ぶかポリシーを棄てるかという選択を迫られる。そこで私がどうしていたかというと、シュッとしているからという理由でカンマとピリオドを使う人間である。当然のごとくポリシーを貫くことを選び、甘んじて苦行を受け入れていた。インスタントメッセージでさえ、句読点を打つたびに変換する。渋さというのはこういったたゆまぬ努力から生まれるものだと自分に言い聞かせていた。

しかし、数週間前にこのポリシーにまったく意味がないことに気づいた。気づきは本当に急におとずれ、カンマとピリオドに変換する作業がまったくばかばかしくおもえてしかたなくなった。あまりにばかばかしいので、一度やめてみるとどうだろう。今まで息の詰まっていたような感覚からスーッとなくなった。これはすごい!とあまりに感動し、気付いたらMacの設定も変えカンマとピリオドをやめてしまった自分がいた。

この話にとくにオチはなくて、ただの日記なんだが、そういうなんかやめにくい習慣や決めごとってよくあるとおもう。やめてみるのはすごく簡単なのでちょっと距離を置いてみるっていうのもよいのかもしれない。冒頭に戻るけど、そういうおもいでTwitterをやめた。やめるのは簡単でiPhoneからクライアントを消すだけで完了した。もともとかじりついていたようなものでも無いので、通勤電車が少し手持ち無沙汰になるだけだった。

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

Swift

この記事ははてなデベロッパーアドベントカレンダー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 です. お楽しみに!