My Favorite Things - Coding or die.

とある技術者の経験記録、的な。

関数プログラミング実践入門 メモ - 第4章 評価戦略

関数プログラミング実践入門の4章「評価戦略」を読んだので簡単にメモ。

遅延評価(lazy evaluation)

  • 実際に使うまで計算しないという計算順序の規則
  • たらい回し関数(竹内関数)は、積極評価だと実行に時間がかかり、遅延評価だとすぐ完了する関数(の例)
  • メジャーなプログラミング言語の殆どは「積極評価」。正確評価(strict evaluation)や先行評価、厳密評価とも。
  • 遅延評価では「無限」を定義できる(実際に使われるまでは評価されないので)
  • 人間にとって自然な(無限な)「数列」の定義と、その数列から値を取り出す、というのを分離できる

評価戦略(evaluation strategy)

  • 評価(evaluation)を行うときの計算順の決め方
  • ラムダ計算では「簡約(reduction)」という変換操作を行うことで評価を進める。
    • (\x -> 変数xを含むかもしれない式A) 式B
    • 式A中の変数xをすべて式Bに置き換えた式にするような変換規則。
  • 式中のどの部分も簡約できない式を「正規形(normal form)」と呼ぶ。
  • ラムダ計算が基礎となっている関数型言語において、評価戦略とは「簡約を行う順序の決め方」といえる。

積極評価(eagar evaluation)

  • 関数型言語でも積極評価を採用している言語は多い。
    • Haskell使いでも”Haskellが積極評価だったら良かったのに”という意見を述べる人も。

最左最外簡約(leftmost-outermost reduction)

  • 外側にあるものから
  • 左側にあるものから
    • を、優先的に簡約していく順序。
  • 評価戦略としては「遅延評価」になる
  • 最左最外簡約で停止しないラムダ式であれば、他のどのような簡約順を選んでも停止しない。(正規順序)

弱冠頭正規形(WHNF) - weak head normal form

  • これ以上適用する値がない関数
  • 式の先頭にコンストラクタが出た状態の値
  • というところまで評価する、Haskellでの評価戦略。
  • isJust (Just (1 + 2))という式では、(1 + 2)の計算は実行されずにTrueが得られる。(中身の値は関係ないので)

サンク(thunk)

  • 「評価が行われないまま放置されている計算予定」オブジェクト
  • 他の言語では「ラムダ式(関数)」を利用することで、同じようなことを実現できる。(関数を呼び出した時に、はじめて計算)
  • サンクに予定されていた計算を発生させて値を得ることを「サンクを潰す」と表現する。
  • Haskellでは「グラフ簡約(graph reduction)という仕組みで、同じ式が複数回登場する場合に最初の1回だけ評価される。

積極評価 vs 遅延評価

  • 「積極評価」の方が、現在の計算機アーキテクチャと相性が良い。
  • 「遅延評価」では、
    • 必要のない計算を本当に行わないことで計算量を低減できる。(が、実際には稀)
    • 「計算の定義」と「その実行」を区別できるため、モジュラリティが高い。(数列定義と取り出し操作など)

評価の制御

  • seq(seq :: a -> b -> b)という関数を利用することで、サンクを潰せる。
  • 1つ目の値を「WHNFまで評価してから」2つめの値になる関数
  • let xs = map (+1) [0, 1, 2] in xs seq xs ++ xs では、++する段階でxsが「WHNF」まで簡約されている

パフォーマンスチューニング

他の言語とチューニング方法は一緒。

  • パフォーマンスを計測する
  • 問題箇所を潰す
  • 積極評価の言語でも、ラムダ式などを利用することで「関数呼び出しを行うことで評価される」仕組みを作れる。

Advanced Swift メモ - 8. Error Handling

8. Error Handling

  • Swiftにはいくつものエラーハンドリングの仕組みがある。
    • Optional、はシンプルだがエラー情報は返せない。
    • Assertion、はバグを早期発見するために利用できる。
    • 例外、はOptionalと違い詳細なエラー情報を持てる。
  • CollectionTypeのfirst/lastなどはOptionalだが、失敗の理由が「配列が空」しかないので適切。
  • ネットワークエラーなどは、失敗の理由を知りたいこともあるはずなのでErrorTypeが良い。

The Result Type - P.248

enum Result<A> {
    case Failure(ErrorType)
    case Success(A)
}
  • Swiftのエラーハンドリングは、殆どResultTypeと同じように実装されている。
  • throwsで宣言された関数を呼び出した時は、キャッチするか伝播させる必要がある。
  • try-catchは他の言語と似ているが、Swiftはランタイム上の処理コストは殆どかからない。
  • ResultTypeとの大きな違いは、エラーが型として定義されていないこと。
    • 厳格な型情報が欲しければResultTypeを使うと良い。
    • でも標準のtry-catchを使ったほうがシンプルではあるかも。

Errors and Objective-C Interface

  • NSErrorポインタを受けるようなObjective-Cの関数はthrowsに変換される。
    • エラーポインタの引数は除外され
    • 戻り値で成否を返すようなものはVoidになる。
- (NSString *)contentsOfFile(NSString*)fileName error:(NSError **)error;
func contentsOfFile(fileName: String) throws

Errors and Function Parameters - P.253

  • すべての要素が条件を満たすか判定するメソッドallは以下のように実装できる。
extension SequenceType {
    func all(@noescape check: Generator.Element -> Bool) -> Bool {
        for el in self {
            guard check(el) else { return false }
        }
        return true
    }
}
  • しかし上記のバージョンではthrows宣言された関数を渡すことが出来ない。
func isEven1(x: Int) -> Bool {
    return x % 2 == 0
}
func isEven2(x: Int) throws -> Bool {
    return x % 2 == 0
}

(1..<10).all(isEven1) // OK
(1..<10).all(isEven2) // Compile error
  • そこでmapなどの標準メソッドは以下のようにrethrowsで宣言されている。
extension SequenceType {
    func all(@noescape check: Generator.Element throws -> Bool) rethrows -> Bool {
        for el in self {
            guard try check(el) else { return false }
        }
        return true
    }
}

try! (1..<10).all(isEven2) // OK

Cleaning Up Using defer - P.255

guard let database = openDatabase(...) else { return }
defer { closeDatabase(database) }
guard let connection = openConnection(database) else { return }
defer { closeConnection(connection) }
guard let result = runQuery(connection, ...) else { return }
  • deferは他の言語のfinallyと似ているが少し異なる点もある。
  • try/doブロックに繋げる必要はなく、どこでも書くことが出来る。
  • deferを同一ブロックで複数宣言した場合、逆順に実行される(スタックのように)
  • セグメンテーションフォルトあるいはfatal errorなどの場合は実行されない(プログラムはクラッシュする)

Errors and Optionals - P.256

if let contents = try? parseFile("Hello.md") {
    print(contents)
}
  • エラーがthrowされなかった場合だけ処理したいときはtry?が使える。
  • Optionalがnilだった場合に、代わりの例外をthrowするような関数は以下のように書ける。
func optional<A>(value: A?, onError e: ErrorType) throws -> A {
    guard let x = value else { throw e }
    return x
}
let int = try optional(Int("42"), onError: ReadIntError.CouldNotRead)
  • try?キーワードは、エラーを無視するべきではないというSwiftの哲学とは矛盾しているようにも見える。
  • しかし、try?は明示的に書く必要がある。
  • あなたがエラーの詳細な内容について興味がないときには、これは便利である。

Chaining Errors - P.257

  • 例外がthrowされる可能性のあるメソッド呼び出しがチェーンしても、ifなどのようにネストしたりしない。
  • 例外が発生した場合は、catchにコントロールが移動して処理される。
func checkFilesAndFetchProcessID(filenames: [String]) -> Int {
    do {
        try filenames.all(checkFile)
        let contents = try contentsOfFile("Pidfile")
        return try optional(Int(contents),
            onError: ReadIntError.CouldNotRead)
    } catch {
        return 42
    }
}
  • これをResultで実装することも簡単に出来る。
  • optionalのように成功の場合は中身を取り出して適用し、失敗の場合は何もしない。
  • そうしたコードはとてもエレガントになる。
func checkFilesAndFetchProcessID(filenames: [String]) -> Int {
    return filenames
        .all(checkFile)
        .flatMap { _ in contentsOfFile("Pidfile") }
        .flatMap { contents in
            Int(contents).map(Result.Success) ?? .Failer(ReadIntError.CouldNotRead)
        }
}

Higher-Order Functions and Errors - P.258

  • この本を執筆している段階では、Swiftのエラーはコールバック関数に向いていない。
  • 単純なケースでは、コールバックの引数をoptionalにすることで解決できる。
    • nilが入っていた場合は失敗をみなす、というように。
func compute(callback: Int -> ()) // 結果をIntで返す
func compute(callback: Int? -> ()) // 失敗した場合はnil
  • 詳細なエラー情報を返すために、最初は以下のようにしてthrows宣言しようと思うかもしれない。
func compute(callback: Int throws -> ())
  • しかし、この宣言は完全な誤りで、型ではなく関数に修飾するthrowsの使い方としてはNG。
  • この宣言をResultで書きなおした場合どうなるかやってみると、間違いであることに気づく。
func compute(callback: Int -> Result<()>)
  • 我々が欲しいのは、以下のようにコールバック関数の引数がResultであるものだ。
func compute(callback: Result<Int> -> ())
  • throwsを使った、現在ではあまり分かりやすくない書き方もある。
func compute(f: (() throws -> Int) -> ())
  • これを利用する側はさらに複雑になる。
compute { (theResult: () throws -> Int) in
    do {
        let result = try theResult()
        print(result)
    } catch {
        print("An error happend: \(error)")
    }
}
  • なのでResultは非同期エラーハンドリングの選択としては良い。
  • しかし、同期関数でthrowsをすでに使用している場合、Resultを使用している箇所との食い違いで、APIが使いづらく鳴るかもしれない。
  • あなたが多くの非同期関数を書いている場合、トレードオフとしてResultを使う価値はある。
  • しかし、一つのコールバックしかないようであれば、先ほどのネストした関数を利用するのも良い選択肢だ。

Conclusion - P.260

  • AppleがSwift 2.0にエラーハンドリングを導入した時、多くのイケてないことが発生した。(訳間違ってるかも)
  • Swift 1.xでは、殆どResult型がエラー処理に使われていた。
  • 実際、型付けされていないthrowsは、嬉しいような、そうでもないような気持ちがあった。(意訳)
  • throwsの利点として、型シグネチャの宣言がシンプルであるという点がある。
  • 例えば、複数のエラーが発生する可能性のある関数の場合、以下のようになっていたかもしれない。
func checkFilesAndFetchProcessID(filenames: [String])
    throws ReadFileError, CheckFileError, MiscellaneousError -> Int // この書き方はしんどい
  • しかし、これは大きな欠点も残した。
    • どのエラーが発生したか明示できず、余分なボイラープレートコードを必要とした。
    • さらに、throwsが関数のみに作用するため、不必要な複雑さを生むことになった。(非同期コールバックのように)
  • Swiftが80%に最適化された、実用的な言語であると考えると、この欠点はシンプルな振る舞いから外れる。

    • 組み込みのエラーハンドリングを利用することで、関数を結果に包んだり、不必要な複雑さをもたらす。
    • そしてあなたが思っているように、我々は曖昧なエッジケースについてここでは言及していない。(非同期コールバックのみ)
  • もし特別なエラー情報が欲しい場合、Result型(エラーをジェネリック型で表現したもの)を使用することが出来る。

  • しかし、これは他の複雑さをあなたのコードに持ち込むことにもなる。
  • あなたが作っているものによっては、この複雑さを持ち込む価値はあるかもしれない。

  • このようにコード中の期待しない動作をハンドリングする仕組みは多くある。

    • 継続できないのであればfatalErrorまたはAssertion
    • エラーの種類に興味がないか、1種類に限定できるのであればOptional
    • エラーの種類が必要か、追加情報が欲しい場合、Swift標準の例外か、Result
    • 関数を引数に受け取る関数を宣言するときは、rethrowsを使うことで、throws宣言された関数も受け取れるようになる
    • そしてdeferは標準のエラーを扱うときにとても便利である
    • deferを使ったコードはスコープを抜けるときにクリーンアップとして必ず実行される(throwsあるいはreturnなど)

Advanced Swift メモ - 2. Introduction (1)

Advanced Swift を読んでのメモ。 www.objc.io

英語レベルは低いので間違っている箇所は普通にあると思います。

Swiftはあなたが選択したプログラミング言語と似ているように見える。

  • 低レベルなビット操作(とパフォーマンスチューニングができる点)は、C言語に似ている。
    ただし、たくさんの未定義動作の落とし穴は除外されている。
  • mapやfilterなどの軽量な接尾クロージャは、Rubyプログラマにお馴染みのもの。
  • SwiftのジェネリクスC++のテンプレートに似ている。
    ただし、型制約は使用時ではなく宣言時に正しいことが保証される。
  • 柔軟な高階関数演算子オーバーロードは、HaskellやF#のようなコードを書けることを意味する。
  • @objcキーワードを使用すれば、Objective-Cセレクタや動的ランタイムが使用できる。

これらの類似は、他の言語のイディオムを採用できることを意味する。

しかし、(これらの類似は同様に)フラストレーションももたらした。

  • なぜ、Protocol Extensions の関連型は、Javaのインターフェースのように使えないのか。
  • なぜ、配列は私達の期待通りに共有されないのか。
  • なぜ、Fanctor(ファンクター)が書けないのか。
  • 時々ある答えとして、Swiftではまだ実装されていないというものがある。
  • (ここから先は自信が無いので後で)

Swiftは多くのプログラミング言語の複合体になっている。

  • しかし、上手に複雑さを隠している。
  • あなたがSwiftでアプリを開発し始める時に、ジェネリクスオーバーロード、静的ディスパッチと動的ディスパッチの違いを理解している必要はない。
  • たぶんあなたは、Cで書かれたライブラリを呼び出したり、自前のコレクション型を書くことは無いだろう。
  • しかし、しばらくして、最終的には以下の様なことを知る必要がでてくる。
    • コードのパフォーマンスを改善したり、
    • よりエレガントに表現したり、
    • 特定の事を実現するために

こうやって日本語に落としてみると、翻訳という作業がいかに大変なのかに気づく。

つづく?

Carthageコードリーディング::3日目

さて3日目。

前回は「VersionCommand」まわりの実装を見た。 今回は実際にコマンドを実行している周りを見ていく。

エントリポイント(復習)

1日目でも触れたが、エントリポイントであるmain.swiftの以下でhelpコマンドをデフォルトに実行している。

registry.main(defaultVerb: helpCommand.verb) { error in
    fputs(error.description + "\n", stderr)
}

CommandRegistry::main

mainメソッドの中身を見ると、以下のようになっている。

@noreturn public func main(defaultVerb defaultVerb: String, errorHandler: ClientError -> ()) {
        main(arguments: Process.arguments, defaultVerb: defaultVerb, errorHandler: errorHandler)
}

標準のProcess.argumentsを使ってコマンドライン引数を取得した上で、別のメソッドに転送している。

ちなみにProcess.arguments[String]を返し、最初の要素はコマンド自身の名前となる。 つまりcarthage helpとした場合は、["carthage", "help"]という結果が得られるはずである。

転送先のメソッドを見てみると・・・ちょっと長い。

@noreturn public func main(arguments arguments: [String], defaultVerb: String, errorHandler: ClientError -> ()) {
  assert(arguments.count >= 1)

  var arguments = arguments

  // Extract the executable name.
  let executableName = arguments.remove(at: 0)

  let verb = arguments.first ?? defaultVerb
  if arguments.count > 0 {
    // Remove the command name.
    arguments.remove(at: 0)
  }

  switch runCommand(verb, arguments: arguments) {
  case .Success?:
    exit(EXIT_SUCCESS)

  case let .Failure(error)?:
    switch error {
    case let .UsageError(description):
      fputs(description + "\n", stderr)

    case let .CommandError(error):
      errorHandler(error)
    }

    exit(EXIT_FAILURE)

  case nil:
    if let subcommandExecuted = executeSubcommandIfExists(executableName, verb: verb, arguments: arguments) {
      exit(subcommandExecuted)
    }

    fputs("Unrecognized command: '\(verb)'. See `\(executableName) help`.\n", stderr)
    exit(EXIT_FAILURE)
  }
}

端折ってもいいけれど一応順番に見ていく。 (今後、繰り返しになりそうであれば割愛していく感じで)

コマンド名と引数の取得

assert(arguments.count >= 1)

var arguments = arguments

// Extract the executable name.
let executableName = arguments.remove(at: 0)

まずassertargumentsの要素数が1以上であることを確認している。 前述したが、引数無しで$ carthageと打った場合でもcarthageが格納されるはずなのでこれは正当なassert。

そして次に「コマンド名」と「コマンド引数」に分離すべく、argumentsvarで宣言しなおしている。 (ここで同じ名前で宣言しなおしているのは賛否ありそうだが、あえて新しい名前をつけるシチュエーションでもなかろう)

そして最初の引数であるはずのcarthageを除外して、executableNameに入れている。 (最初、これは使わないから_でいいかと思ったのだが、あとで使っているようだ)

let verb = arguments.first ?? defaultVerb
if arguments.count > 0 {
  // Remove the command name.
  arguments.remove(at: 0)
}

ここでverbにコマンド名、あるいはデフォルトであたえたhelpCommand.verb(すなわちhelp)が入る。

つまり$ carthage versionであればversion$ carthageであればhelpがデフォルトコマンドとして格納されるわけだ。

そしてargumentsの最初の要素を削って、コマンドの本来の引数としている。 すなわち$ carthage update --platform macであれば、["--platform", "mac"]となるはずだ。

実行

次に実際にコマンドを実行しているようだ。

switch runCommand(verb, arguments: arguments) {
case .Success?:
  exit(EXIT_SUCCESS)

case let .Failure(error)?:
  switch error {
  case let .UsageError(description):
    fputs(description + "\n", stderr)

  case let .CommandError(error):
    errorHandler(error)
  }

  exit(EXIT_FAILURE)

case nil:
  if let subcommandExecuted = executeSubcommandIfExists(executableName, verb: verb, arguments: arguments) {
    exit(subcommandExecuted)
  }

  fputs("Unrecognized command: '\(verb)'. See `\(executableName) help`.\n", stderr)
  exit(EXIT_FAILURE)
}

runCommand(verb, arguments: arguments)メソッドで先ほど解決した、コマンド名と引数を渡している。 そしてその結果(成功 or 失敗 or nil?)で処理結果を分離している。

そのまま読んでみると、以下のようになるだろうか。

  • 成功:プロセス終了
  • 失敗(引数の指定が正しくない):usage(正しい使い方)を出力して終了
  • 失敗(コマンド失敗):登録されたエラーハンドラを実行
  • nil:サブコマンドがあれば実行し、なければエラーとする

nilのケースが良くわからなかった。 これは「Commandantモジュール」の中なので、Carthageでは使用していない部分の可能性もある。

追記: 直後に分かったが登録されていないコマンドを与えた場合にnilのケースになるようだ。

$ carthage foo
$ Unrecognized command: 'foo'. See `carthage help`.

CommandRegistry::runCommand

runCommandの中身に飛ぶと、以下のようになっている。

public func runCommand(verb: String, arguments: [String]) -> Result<(), CommandantError<ClientError>>? {
  return self[verb]?.run(ArgumentParser(arguments))
}

self[verb]でsubscriptを呼び出して、CommandWrapperを取得している。

public subscript(verb: String) -> CommandWrapper<ClientError>? {
        return commandsByVerb[verb]
}

commandsByVerbは以下のように宣言されており、registerメソッドを呼び出した際に更新されている。

private var commandsByVerb: [String: CommandWrapper<ClientError>] = [:]

public func register<C: CommandType where C.ClientError == ClientError, C.Options.ClientError == ClientError>(command: C) {
  commandsByVerb[command.verb] = CommandWrapper(command)
}

つまりcommandsByVerbはコマンド名をキーに、CommandoTypeのラッパーを値として格納するDictionaryだ。 これによってコマンド名に対応するCommandWrapper(とやら)を取得して、.run(ArgumentParser(arguments))を呼び出している。

ArgumentParserは大体想像するとおりのこと(つまり引数文字列のパース)をやっていると思うので、とりあえず放置する。

CommandWrapper

まずコード全体を見てみる。

public struct CommandWrapper<ClientError: ClientErrorType> {
    public let verb: String
    public let function: String

    public let run: ArgumentParser -> Result<(), CommandantError<ClientError>>

    public let usage: () -> CommandantError<ClientError>?

    /// Creates a command that wraps another.
    private init<C: CommandType where C.ClientError == ClientError, C.Options.ClientError == ClientError>(_ command: C) {
        verb = command.verb
        function = command.function
        run = { (arguments: ArgumentParser) -> Result<(), CommandantError<ClientError>> in
            let options = C.Options.evaluate(.Arguments(arguments))

            if let remainingArguments = arguments.remainingArguments {
                return .Failure(unrecognizedArgumentsError(remainingArguments))
            }

            switch options {
            case let .Success(options):
                return command
                    .run(options)
                    .mapError(CommandantError.CommandError)

            case let .Failure(error):
                return .Failure(error)
            }
        }
        usage = { () -> CommandantError<ClientError>? in
            return C.Options.evaluate(.Usage).error
        }
    }
}

これもちょっとややこしそうだ。

verb、function

とりあえずイニシャライザを覗いてみると、、、 verbfunctionは、単にCommandTypeの値を転記しているだけのようだ。

verb = command.verb
function = command.function

run

runArgumentParserを受け取ってResultを返すクロージャが格納されている。

run = { (arguments: ArgumentParser) -> Result<(), CommandantError<ClientError>> in
  let options = C.Options.evaluate(.Arguments(arguments))

  if let remainingArguments = arguments.remainingArguments {
    return .Failure(unrecognizedArgumentsError(remainingArguments))
  }

  switch options {
  case let .Success(options):
    return command
      .run(options)
      .mapError(CommandantError.CommandError)

  case let .Failure(error):
    return .Failure(error)
  }
}

個人的にはクロージャではなくて普通にメソッドでも良いと感じたのだが、何かメリットがあるのだろうか?

とりあえずオプション引数のパースを試みて、成功したらcommand.run(options)で実行し、 失敗した場合は.Failureで失敗を返しているようだ。(まぁその辺の詳細はおいおい)

command.run(options)で実行される具体例は、前回見た以下となる。

public func run(options: NoOptions<CarthageError>) -> Result<(), CarthageError> {
    let versionString = NSBundle(identifier: CarthageKitBundleIdentifier)?.objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
    carthage.println(versionString)
    return .Success(())
}

これでコマンド引数のパースから、前回のVersionCommandrun()までつながった。

usage

usageも同じようにクロージャが格納されている。

usage = { () -> CommandantError<ClientError>? in
  return C.Options.evaluate(.Usage).error
}

CommandTypeに紐づくOptions.Usageを評価させて、.errorを返しているらしい。 難しく見えるけれど、要はオプション引数のusage(使い方)を返す処理のようだ。

また新しいテーマに入ってしまうので深追いはやめておくが、参考までにHelpCommandevaluateは以下のようになっている。

public static func evaluate(m: CommandMode) -> Result<HelpOptions, CommandantError<ClientError>> {
  return create
    <*> m <| Argument(defaultValue: "", usage: "the command to display help for")
}

まとめ

とりあえず今日はここまで! なんかCarthageというよりも依存ライブラリに対するコードリーディングになっている気もするが・・・。

続く・・・のかもしれない。

Carthageコードリーディング::2日目

今回は一番簡単であろう「version」コマンドを見ていく。

VersionCommand

登録

前回も触れたが、main.swiftでCommandRegistryに各コマンドを登録している。

// main.swift
let registry = CommandRegistry<CarthageError>()
registry.register(VersionCommand())

「version」コマンドは「VersionCommand」という構造体を生成して登録している。

VersionCommandの定義

実際の定義を見てみると、以下のようになっている。

public struct VersionCommand: CommandType {
    public let verb = "version"
    public let function = "Display the current version of Carthage"

    public func run(options: NoOptions<CarthageError>) -> Result<(), CarthageError> {
        let versionString = NSBundle(identifier: CarthageKitBundleIdentifier)?.objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
        carthage.println(versionString)
        return .Success(())
    }
}

「CommandType」というプロトコルを実装しているシンプルな構造体になっている。

見たままだが、定義されている定数とメソッドは以下のようになっている。

  • verb・・・コマンド名
  • function・・・コマンドのusage
  • run()・・・実際に実行する処理

「verb」と「function」で定義された文字列は「carthage help」で確認できる。

$ carthage help
Available commands:
...
   version           Display the current version of Carthage

「run()」の処理結果はもちろん「carthage version」で確認できる。

$ carthage version
0.15.2

これでとりあえず「VersionCommand」の処理内容は全て確認できたはずである。

run()の中身

run()の中身をもう一度見てみる。

public func run(options: NoOptions<CarthageError>) -> Result<(), CarthageError> {
  let versionString = NSBundle(identifier: CarthageKitBundleIdentifier)?.objectForInfoDictionaryKey("CFBundleShortVersionString") as! String
  carthage.println(versionString)
  return .Success(())
}

最初はバージョン番号の取得だが、よく使われる(と私が思っている)NSBundle.mainBundle()は使われていない。

CarthageKitBundleIdentifierの定義を見てみる。

/// Carthage’s bundle identifier.
public let CarthageKitBundleIdentifier = NSBundle(forClass: Project.self).bundleIdentifier!

Projectクラスが所属するBundleのbundleIdentifierを取得するようになっているようだ。 正確な意図がわからなかったが、Frameworkなどでプロジェクトを分割した時でも正しいplistが参照できるようにするためだろうか?

ちなみに強制アンラップ(!)や強制キャスト(as!)が使用されているのには興味深い。

プロジェクトの開発ルールによっては一部を除外してほぼ禁止されることも多いが、必ず成功するという確信が得られるケースでは利用しているようだ。 (これは個人的にも賛成だ)

次はバージョン番号を出力しているコードであるが、carthage.printlnなるメソッドが利用されている。

carthage.println(versionString)

そしてどうやら定義はExtensions.swiftに記述された以下関数らしい。

/// A thread-safe version of Swift's standard println().
internal func println<T>(object: T) {
    dispatch_async(outputQueue) {
        Swift.print(object)
    }
}

標準のprint()がスレッドセーフでないので、スレッドセーフなバージョンを用意しているようだ。 (コメントに書いてあるまんまですね)

そして私は初めて知ったのだが、carthage.printlncarthageはモジュール名を指しているらしい。 つまり「モジュール名.関数」という形でアクセスできるというわけだ。(常識だったりする?)

最初はどこかにインスタンスが定義されているものと思ったが、そもそも実体は関数であるわけで。

元のrun()メソッドに戻り、最後は結果を返している。

return .Success(())

これは「Result」モジュールの「Result」が利用されているらしい。

/// An enum representing either a failure with an explanatory error, or a success with a result value.
public enum Result<T, Error: ResultErrorType>: ResultType, CustomStringConvertible, CustomDebugStringConvertible {
    case Success(T)
    case Failure(Error)

Haskell使いには「Either」といったほうが早いだろうか? つまり今回のケースでは処理としては「成功」で、その中身は「空タプル」というわけだ。

CommandTypeプロトコル

これで一応はrun()の中身が読み終わったので、CommandTypeプロトコルを見てみる。

/// Represents a subcommand that can be executed with its own set of arguments.
public protocol CommandType {

    /// The command's options type.
    associatedtype Options: OptionsType

    associatedtype ClientError: ClientErrorType = Options.ClientError

    /// The action that users should specify to use this subcommand (e.g.,
    /// `help`).
    var verb: String { get }

    /// A human-readable, high-level description of what this command is used
    /// for.
    var function: String { get }

    /// Runs this subcommand with the given options.
    func run(options: Options) -> Result<(), ClientError>
}

ここまでくれば目新しいものは無い。 VersionCommandで実装されていた定数verbとfunction、run()がプロトコルとして定義されている。

つまりすべてのCarthageコマンドは、このCommandTypeプロトコルを実装することになる。

もし自分で新しいコマンドを作りたい場合(がもしあったとしてだが)、 このプロトコルを実装したクラスあるいは構造体を作ってCommandRegistryに登録しておけばいいわけだ。 (なんかオブジェクト指向の授業みたいになっちゃったけど、まぁそれはさておき)

ちなみにCommandTypeは「Commandant」モジュールに含まれている。 これは(おそらく)Carthageのコマンド引数のパースや実行の仕組みが「Commandant」モジュールで実現されていることを示唆する。

まとめ

今回は一番簡単であろう「version」コマンドがどのように処理されているのかを見てみた。 単純な実装ではあったが、コマンド実行の仕組み、ちょっとした工夫が見られたような気がする。

はてさて、こんなペースで続けていてコードリーディングが終わるのか不安である。 次回に続く・・・のか?

文章中の表現が統一できていない箇所が多いですけれど、そのうち揃えていきますのでご容赦を。

Carthageコードリーディング::1日目

Swift力を上げるために、Swiftで書かれたパッケージマネージャ「Carthage」のコードリーディングを始めてみる。 github.com

準備

まずはGitHubリポジトリをclone。

$ git clone https://github.com/Carthage/Carthage.git

次に依存ライブラリをインストール。

$ carthage update --platform mac

関係ないけど、自身の依存関係管理に自身(つまりCarthage)を使ってるのって夢があるよね。 GitやSVNといったVCSほどの感動はないけれど。

エントリポイント

どうやらエントリポイントは「main.swift」らしい。 https://github.com/Carthage/Carthage/blob/master/Source/carthage/main.swift

まずはCarthageを動作させるために必須なバージョンのgitがインストールされているかチェックしている模様。

guard ensureGitVersion().first()?.value == true else {
    fputs("Carthage requires git \(CarthageRequiredGitVersion) or later.\n", stderr)
    exit(EXIT_FAILURE)
}

どうやらCarthageは、2.3.0以上のgitでなければ動作しないようだ。(READMEに書いてあるだろうけれど)

/// The git version Carthage requires at least.
public let CarthageRequiredGitVersion = "2.3.0"

次にcarthageの実行パスを環境変数に追加している。

if let carthagePath = NSBundle.mainBundle().executablePath {
    setenv("CARTHAGE_PATH", carthagePath, 0)
}

別プロセスで参照することがあるからだろうか? 今のところ必要性が分かっていない。

そしてここからが本質的なところとなるが、 carthageのコマンドに対応したものが、CommandRegistryに追加していっている。

let registry = CommandRegistry<CarthageError>()
registry.register(ArchiveCommand())
registry.register(BootstrapCommand())
...
registry.register(VersionCommand())

そして、helpコマンドをデフォルトとして実際に実行している。

let helpCommand = HelpCommand(registry: registry)
registry.register(helpCommand)

registry.main(defaultVerb: helpCommand.verb) { error in
    fputs(error.description + "\n", stderr)
}

ここでいうデフォルトとはつまり単にcarthageと引数を与えずに実行した場合という意味だ。 実際に、carthageとだけ打つとhelpコマンドが実行されるのが分かる。

$ carthage
Available commands:

   archive           Archives built frameworks into a zip that Carthage can use
   bootstrap         Check out and build the project's dependencies
   build             Build the project's dependencies
   checkout          Check out the project's dependencies
   copy-frameworks   In a Run Script build phase, copies each framework specified by a SCRIPT_INPUT_FILE environment variable into the built app bundle
   fetch             Clones or fetches a Git repository ahead of time
   help              Display general or command-specific help
   update            Update and rebuild the project's dependencies
   version           Display the current version of Carthage

エントリポイントとしてはこんなところ。 CommandRegistryがいい味を出している。

依存ライブラリ

Carthageはシンプルなパッケージマネージャだと聞いていたので、依存ライブラリは少ないと思っていたのがところがどっこい。間接的な依存ライブラリも多いが、それらも含めると結構な量である。

f:id:yu_dotnet2004:20160627000205p:plain

Commandant

コマンドライン引数のパース用ライブラリらしい。 後述のJSONパーサ「Argo」をインスパイヤしているとのこと。

Curry

カリー化用の関数を提供している。 たぶん今回の中で一番シンプルなライブラリ。(だって実質1ファイルだもの)

ReactiveCocoa

いわずとしれたリアクティブプログラミング用のライブラリ。

Quick

こちらもいわずとしれたBDDなテスティングフレームワーク

Nimble

Quickと一緒に使えるマッチャー。

ReactiveTask

シェル実行用のライブラリらしい。 名前からしてReactiveCocoa(あるいはRx系のライブラリの考え方)をインスパイヤしているのは間違いないだろう。

Argo

関数型のJSONパーサ。 どうやらApplicativeFunctorの考え方がベースになってそう。

PrettyColors

ターミナルの色付け用。

Result

HaskellでいうところのEitherの++的な?

Tentacle

GitHub API用のライブラリ。 ちなみに翻訳して知ったのだが「触手」という意味の単語らしい。 (触手って・・・もう少しネーミングどうにかならなかったのか。。)

まとめ

やり始めたけど思ったより大変そう・・・。 はてさて、2日目以降の記事は書けるのか?

CookieClicker v2.0 - クッキーを自動クリック

風邪を引いて体調悪いので、自動クリックに対応してみた。

これでよりたくさんのクッキーを焼けるぞ!やったね!!

追記(2016/06/26):
久しぶりに実行したらAddEventでエラーになっていたので修正。