関数プログラミング実践入門 メモ - 第4章 評価戦略
関数プログラミング実践入門の4章「評価戦略」を読んだので簡単にメモ。
遅延評価(lazy evaluation)
- 実際に使うまで計算しないという計算順序の規則
- たらい回し関数(竹内関数)は、積極評価だと実行に時間がかかり、遅延評価だとすぐ完了する関数(の例)
- メジャーなプログラミング言語の殆どは「積極評価」。正確評価(strict evaluation)や先行評価、厳密評価とも。
- 遅延評価では「無限」を定義できる(実際に使われるまでは評価されないので)
- 人間にとって自然な(無限な)「数列」の定義と、その数列から値を取り出す、というのを分離できる。
評価戦略(evaluation strategy)
- 評価(evaluation)を行うときの計算順の決め方
- ラムダ計算では「簡約(reduction)」という変換操作を行うことで評価を進める。
(\x -> 変数xを含むかもしれない式A) 式B
を式A中の変数xをすべて式Bに置き換えた式
にするような変換規則。
- 式中のどの部分も簡約できない式を「正規形(normal form)」と呼ぶ。
- ラムダ計算が基礎となっている関数型言語において、評価戦略とは「簡約を行う順序の決め方」といえる。
積極評価(eagar evaluation)
最左最外簡約(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はランタイム上の処理コストは殆どかからない。
- これは単純なreturn処理としてコンパイラが扱うため。
- 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のセレクタや動的ランタイムが使用できる。
これらの類似は、他の言語のイディオムを採用できることを意味する。
- 適例として、Objective-Cのプロジェクトの機能の殆どはSwiftに移植できる。
- JavaやC#のデザインパターンをSwiftで実装する本はすぐに出版された。
- ニューウェーブとして、Monadのチュートリアル的なブログ記事が生まれた。
しかし、(これらの類似は同様に)フラストレーションももたらした。
- なぜ、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)
まずassert
でarguments
の要素数が1以上であることを確認している。
前述したが、引数無しで$ carthage
と打った場合でもcarthage
が格納されるはずなのでこれは正当なassert。
そして次に「コマンド名」と「コマンド引数」に分離すべく、arguments
をvar
で宣言しなおしている。
(ここで同じ名前で宣言しなおしているのは賛否ありそうだが、あえて新しい名前をつけるシチュエーションでもなかろう)
そして最初の引数であるはずの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
とりあえずイニシャライザを覗いてみると、、、
verb
とfunction
は、単にCommandTypeの値を転記しているだけのようだ。
verb = command.verb function = command.function
run
run
はArgumentParser
を受け取って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(()) }
これでコマンド引数のパースから、前回のVersionCommand
のrun()
までつながった。
usage
usage
も同じようにクロージャが格納されている。
usage = { () -> CommandantError<ClientError>? in return C.Options.evaluate(.Usage).error }
CommandType
に紐づくOptions
で.Usage
を評価させて、.error
を返しているらしい。
難しく見えるけれど、要はオプション引数のusage(使い方)を返す処理のようだ。
また新しいテーマに入ってしまうので深追いはやめておくが、参考までにHelpCommand
のevaluate
は以下のようになっている。
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.println
のcarthage
はモジュール名を指しているらしい。
つまり「モジュール名.関数」という形でアクセスできるというわけだ。(常識だったりする?)
最初はどこかにインスタンスが定義されているものと思ったが、そもそも実体は関数であるわけで。
元の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
準備
$ 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はシンプルなパッケージマネージャだと聞いていたので、依存ライブラリは少ないと思っていたのがところがどっこい。間接的な依存ライブラリも多いが、それらも含めると結構な量である。
Commandant
コマンドライン引数のパース用ライブラリらしい。 後述のJSONパーサ「Argo」をインスパイヤしているとのこと。
Curry
カリー化用の関数を提供している。 たぶん今回の中で一番シンプルなライブラリ。(だって実質1ファイルだもの)
ReactiveCocoa
いわずとしれたリアクティブプログラミング用のライブラリ。
Quick
Nimble
Quickと一緒に使えるマッチャー。
ReactiveTask
シェル実行用のライブラリらしい。 名前からしてReactiveCocoa(あるいはRx系のライブラリの考え方)をインスパイヤしているのは間違いないだろう。
Argo
関数型のJSONパーサ。 どうやらApplicativeFunctorの考え方がベースになってそう。
PrettyColors
ターミナルの色付け用。
Result
HaskellでいうところのEitherの++的な?
Tentacle
GitHub API用のライブラリ。 ちなみに翻訳して知ったのだが「触手」という意味の単語らしい。 (触手って・・・もう少しネーミングどうにかならなかったのか。。)
まとめ
やり始めたけど思ったより大変そう・・・。 はてさて、2日目以降の記事は書けるのか?
CookieClicker v2.0 - クッキーを自動クリック
風邪を引いて体調悪いので、自動クリックに対応してみた。
これでよりたくさんのクッキーを焼けるぞ!やったね!!
追記(2016/06/26):
久しぶりに実行したらAddEventでエラーになっていたので修正。