Swift実践入門読書勉強会 #7を開催しました
Swift実践入門読書勉強会 #7をOrigami株式会社で行いました。
https://read-swift.connpass.com/event/56756/
2月から開始したこの勉強会も、今回で最後になりました。
Swift実践入門読書勉強会は
工藤さん、田中さんと玉城さん、私佐藤で運営とファシリテーターをしながら、Swift実践入門を一章ずつ読んていく勉強会です。
最後ということで、なんと著者の石川さんもいらっしゃっいました。
テーマは13章の「エラー処理」です。
石川さんも参加!
ファシリテーターは田中さん!
Optional型によるエラー処理
書き方
処理の結果をOptional型で表し、値が存在すれば成功、存在しなければ失敗とみなす。
struct User {
let id: Int
let name: String
let email: String
}
func findUser(byID id: Int) -> User? {
let users = [
User(id: 1,
name: "Yusei Nishiyama",
email: "nishiyama@example.com"),
User(id: 2,
name: "Yosuke Ishikawa",
email: "ishikawa@example.com"),
]
for user in users {
if user.id == id {
return user
}
}
return nil
}
let id = 0
if let user = findUser(byID: id) {
print("Name: \(user.name)")
} else {
print("Error: User not found")
}
使い所
エラーの内容を問わないときに使える方法です。
勉強会内で石川さん直々にお話していただいたことは、例えばサーバーからのjsonファイルからenumのインスタンスを初期化する時。
失敗したとすれば、jsonはenumの構造のキーを持っていなかったということ。そういう時は、失敗したかどうかを見ればいいのでOptional型によるエラー処理が有効とのこと。
勉強会の議論
どんな時に使う?
— SatoTakeshi (@hatakenokakashi) May 24, 2017
->石川さん:enumを初期化したいとき、jsonからenumを初期化できない時。初期化できないのはjsonがマッチしてないということだけわかればいい
失敗する理由が一つの場合は簡潔に記述できる失敗可能イニシャライザを使ったりすることがある。
— ダンボー田中 (@ktanaka117) May 24, 2017
もしも理由が複数あって、詳細を知りたい場合は他の方法でエラーを表現する#read_swift
Result<T, Error>型によるエラー処理
成功したらT型、失敗したらError型が返る様な方を定義するやり方。
Swiftの言語で実装されたものではないが、デファクトスタンダード。
HaskelやRustに同様なエラー処理があり、それをSwiftに適応したものとのこと。
ライブラリーにもなっている。
https://github.com/antitypical/Result
書き方
enum Result<T, Error> {
case success(T)
case failure(Error)
}
enum DatabaseError:Error {
case entryNotFound
case duplicatedEntry
case invalidEntry(reason: String)
}
struct User {
let id: Int
let name: String
let email: String
}
func findUser(byID id: Int) -> Result<User, DatabaseError> {
let users = [
User(id: 1,
name: "Yusei Nishiyama",
email: "nishiyama@example.com"),
User(id: 2,
name: "Yosuke Ishikawa",
email: "ishikawa@example.com"),
]
for user in users {
if user.id == id {
return .success(user)
}
}
return .failure(.entryNotFound)
}
let id = 0
let result = findUser(byID: id)
switch result {
case let .success(name):
print(".success: \(name)")
case let .failure(error):
switch error {
case .entryNotFound:
print(".failure: .entryNotFound")
case .duplicatedEntry:
print(".failure: .duplicatedEntry")
case .invalidEntry(let reason):
print(".failure: .invalidEntry(\(reason))")
}
}
使い所
Result<T, Error>型の特徴は、エラーの内容がわかり、それが網羅されていること。
以下のときに使うと良い
- エラーの網羅性がコンパイルで保証する
- 何が起きるのかが開発者が分かっている
- do-chatchだと何が起こるのかを自分で調べなきゃいけなくなる。網羅性があまりない
- 逆に不明なエラーがある場合はunknownなどのエラーを作り、そこに放り込む
- 非同期処理で使える
- do-chatch文は同期処理で使える。
勉強会での議論
Result 型のお話。 https://t.co/ExvuNzhVuu #swift #read_swift pic.twitter.com/9i7gQ3vOSo
— 熊谷 友宏 (@es_kumagai) May 24, 2017
石川さん自ら誤字を発見
— SatoTakeshi (@hatakenokakashi) May 24, 2017
DatabaseErrorはErrorを準拠しないとだめっぽい。#read_swift
Result型の使い所。
— SatoTakeshi (@hatakenokakashi) May 24, 2017
エラーの詳細を提供したい時#read_swift
Result で、エラーの種類を網羅できるの、良いポイントですよね。 #read_swift
— 熊谷 友宏 (@es_kumagai) May 24, 2017
Result型とSwitchでErrorを扱う網羅性はとても強力。その網羅性のみでなく、特定のError型に対してどういうErrorが発生しうるかというのが一目瞭然になるのがまた素晴らしい。 #read_swift
— ダンボー田中 (@ktanaka117) May 24, 2017
コードの該当箇所を読むだけでどんなエラーが発生するか網羅的に理解できるのは色々確認の手間が減ってよいというお話がありました。Result<T,Error>型を利用したエラーハンドリング。#read_swift
— Yuta Hoshino (@hsylife) May 24, 2017
私が石川さんに質問
「〇〇ページのように、switch文がネストしてしまうことがある。これどうにかならないのか。こうするしかないのか」
すると石川さんは
Result型でswitch文をネストしないようにするコードを書く石川さん
書かれたコードはこんな感じ
Result<User, ~~Error>
let r1 = findUser(byID: 123)
let r2 = r1.flatMap{ localRart (fromEmail: $0.email)
//r2はResult<String, ~~Error)になる
元ネタはSwiftでResult<T, Error>
型のライブラリーantitypical/Resultの以下のコード
https://github.com/antitypical/Result/blob/master/Result/ResultProtocol.swift
Result<T, Error>
型にflatMapメソッドを定義し、サクセスした時はその値を含めた値に変換する方法を取ればswitch文をネストしなくても良くなるとのことでした。
ただし、このときのErrorは同じ型でないといけないそうです。
このとき、r1とr2のResult型のErrorは同じ型でなければいけないという制約があるとのこと #read_swift https://t.co/66apJ643X6
— ダンボー田中 (@ktanaka117) May 24, 2017
do-catch文によるエラー処理
書き方
throw文によるエラーが発生する可能性のある処理をdo節で記述し、catch節にエラー処理を記述します。
enum SomeError: Error {
case error1
case error2(reason: String)
}
do {
throw SomeError.error2(reason: "何かがおかしいようです")
} catch SomeError.error1 {
print("error1")
} catch SomeError.error2(let reason) {
print("error2: \(reason)")
} catch {
print("Unknown error: \(error)")
}
使い所
do-catch文の特徴は必ずcatch文を書かなければいけないことがSwiftの言語仕様上決まっていることです。
また、catchできるエラーはResult<T, Error>型と異なり型情報がなくなっています。
(Errorプロトコルを実装した何かということだけしかわからない)
このことから、do-catch文は「どんなエラーが来るか実装時には分からないが、必ず復帰させる処理をしたいとき」にdo-catch文は有効だという話になりました。
勉強会での議論
catchには型情報がなくなっている。この例ではcatch SomeError.error1はcatchの後の型はError型の何かが来るとしか不明 https://t.co/YJYsCoVype #swift #read_swift pic.twitter.com/Lm1twj3Wq8
— SatoTakeshi (@hatakenokakashi) May 24, 2017
do-catchのthrowは型情報を保持しない。これに対して、Result<T,Error>型による場合は、型情報が保持される。だからenumのcaseに100%限定できる。#read_swift
— Yuta Hoshino (@hsylife) May 24, 2017
Playgroundだと最後のcatchがなくてもコンパイルエラーにならないが、アプリの環境だとコンパイルエラーになるよ。 https://t.co/5gsjatmt2H #swift #read_swift pic.twitter.com/Bme8qMcbNC
— ダンボー田中 (@ktanaka117) May 24, 2017
do-catch:なんかエラーが起こり得る時に使える
— SatoTakeshi (@hatakenokakashi) May 24, 2017
->必ずcatchは書いてエラー復帰をさせる
Result型:どんなエラーが起こるのかがわかっている#read_swift
Error型はエラーごとに定義しましょう#read_swift
— SatoTakeshi (@hatakenokakashi) May 24, 2017
いわゆる default catch なしの場合、ちゃんと Xcode でエラーになること、確認できました。 https://t.co/9GeDrIUWps #swift #read_swift pic.twitter.com/7sT5ACukLx
— 熊谷 友宏 (@es_kumagai) May 24, 2017
あ、Playground でも func 内で使う do-try-catch なら、普通のプロジェクトと同じエラーが出てくれますね。きっと Playground のグローバルスコープが暗黙的に do-catch されてる予感がする。 #read_swift
— 熊谷 友宏 (@es_kumagai) May 24, 2017
rethrowsについて
勉強会の議論
rethrowsという存在を知らんかった #read_swift
— ダンボー田中 (@ktanaka117) May 24, 2017
rethrowsキーワードをつけると、引数にとったクロージャが発生させるエラー「のみ」を取り扱うことになる。throwsキーワードでも同じことはできるが、throwsだった場合は、元々の関数の中で別なエラーを発生させることもできる? #read_swift
— ダンボー田中 (@ktanaka117) May 24, 2017
『rethrows は、渡されたクロージャーの中だけでしかエラーが発生しないことを保証できる』『エラーの際は、独自のエラーにも差し替えられる』つまり、エラーの発生箇所を限定し、かつ、必要に応じてエラー情報をより事実に即したものに差し替えられる。 #read_swift
— 熊谷 友宏 (@es_kumagai) May 24, 2017
rethrowsは引数のクロージャーが発生させるエラーにエラーを限定する#read_swift
— SatoTakeshi (@hatakenokakashi) May 24, 2017
違った。これにおいて、throwingClosure()は引数からきているのだからおk。それ以外のところからのthrowはダメよってことね https://t.co/HeICjnCmpX #swift #read_swift pic.twitter.com/CrpirLg2I1
— ダンボー田中 (@ktanaka117) May 24, 2017
rethrowsをつけた関数で引数のクロージャー以外でエラーを呼ぶとだめ#read_swift
— SatoTakeshi (@hatakenokakashi) May 24, 2017
rethrowsはクロージャーを引数で受ける時に使う#read_swift
— SatoTakeshi (@hatakenokakashi) May 24, 2017
rethrowsは汎用的なものを書くときに登場したりする。map, filter, reduceなどには実装されていて、みんな知らないうちに恩恵に預かっていたりする #read_swift
— ダンボー田中 (@ktanaka117) May 24, 2017
.@_ishkawa 「rethrowsを自分から出会いに行くことは、まず訪れない」 #read_swift
— Yutaro (@yutailang0119) May 24, 2017
deferについて
勉強会の議論
俺、deferってほぼ書いた事ないよ。 #read_swift
— ワニ@tmk (@alligator_tama) May 24, 2017
defer は、その式が記述されているスコープを抜けた『直後』? 直前? ちょっと良いこと思いついた( ̄ー ̄) #read_swift
— 熊谷 友宏 (@es_kumagai) May 24, 2017
なるほど?deferってそう使うのか。全然使ったことなかった #read_swift
— ダンボー田中 (@ktanaka117) May 24, 2017
あー、なるほどエラー処理の時に使えば良いのか。納得した。 #read_swift
— ワニ@tmk (@alligator_tama) May 24, 2017
おわりに
7回に渡り、最後まで続けられて本当に嬉しく思います。
この勉強会のいいところは、Swiftの文法書き方をただ覚えるのではなく、「これはなぜそうなっているのか」「どういう場面で使われるのか」を議論できるところだと思います。
iOSエンジニアの堤さんもこの勉強会で使用したSwift実践入門でこんな書評を書いていました。
Swiftの各機能が「なぜ」存在し「いつ」使うべきかを解説した技術書 - Swift実践入門
プログラムの文法や書き方を「おまじない」で済まさず、どんな理論で動いているのでどういう場面で使うべきなのかを話せたのはとても有意義な時間でした。
参加者からの発言も多く取り入れて「こういう時使っている」や「ここがわからん」と、どんどん議論を深めて行きました。
改めまして、Swift実践入門読書勉強会をやろうと誘ってくれた、工藤さん、田中さん、会場をいつもお借り頂いてる玉城さん、参加していただいた皆さんにお礼を致します。
ありがとうございました。
次回
GoogleがKotlinを正式なAndroidプログラム言語にしました。
Google、KotlinをAndroidアプリ開発言語に選定――I/O会場から大喝采
時代はKotlinです。
なのでKotlin勉強会を同じメンバーで始めます。
勉強会グループ
Kotlin読書勉強会
第一回目イベント
Kotlinスタートブック読書勉強会 #1
Kotlinを覚えてAndroidアプリを作ろう。
これからもよろしくお願いします!