Swiftの静的型付けの特性がシリアライズされたオブジェクトのデコードを難しくしています。 代替案としてカリー化のような特性を用いるのはおもしろい試みです。この try! Swift NYC の講演では、 Anat Gilboa がSwiftの関数型の側面を利用して、JSONのパースを楽しく、エキサイティングなものにしてくれます!
イントロダクション (0:00)
JSONのパースについてお話します。元々この講演のインスピレーションを得たのは、チームメイトの一人がネットワーク層をObjective-CからSwiftに移行するという案を提案したことでした。その案には2つのサードパーティ製ライブラリの利用が含まれていました。Argo と Result です。JSONのデコードと、それぞれのエラーハンドリングを行うためです。JSONをデコードする経験があまりありませんでしたが、いくつかのREADMEやXcodeのプロジェクトをチェックすることにしたのです。
提案 (0:41)
Argo は、安全な方法でJSONからモデルを抽出するJSONデコードライブラリです。 Swiftの型システムを使用して、必要なデータフィールドを明示的に検証し、成功か失敗かを教えてくれます。
カリー化がコードで何をしているかよくわかっていなかったので、チェックしてみることにしました。「カリー化」という単語に馴染みのない方は、必要な引数よりも少ない引数の関数を与え、残った引数をとる関数を返すアクションのことだと思ってください。なので、Userオブジェクトは下記のようになります。
struct User {
let id: Int
let name: String
let age: Int
}
static func create(id: Int)(name: String)(age: Int) -> User {
return User(id: id, name: name, age: age)
}
引数はそれぞれのカッコにより分けられています。
Result はValueとErrorを、それぞれSuccessとFailureにラップできるマイクロフレームワークです。ここに例がありますが、JSONのエラーがあり、ErrorType
のenum
で、2つのケースがあります。それから与えられたKeyを使って、JSONのDictionaryからStringの取得を試みている関数があります。
Result<Value, Error>
typealias JSONObject = [String:AnyObject]
enum JSONError : ErrorType {
case NoSuchKey(String)
case TypeMismatch
}
func stringForKey(json: JSONObject, key: String) -> Result<String, JSONError> {
guard let value = json[key] else {
return .Failure(.NoSuchKey(key))
}
if let value = value as? String {
return .Success(value)
}
else {
return .Failure(.TypeMismatch)
}
}
AnyObjectを返すのではなく、キーに対するStringの値と、エラー詳細のあるエラー型を含んだResultを返しています。キーに対応する値がないときには、guard let
によって失敗させたり、値のアンラップに成功したり、型の不一致によって失敗させることができます。これらはエラー型から示される2つのエラーに過ぎません。
なぜSwift 2で導入されたthrowを使わずにこのようなことをするのか、疑問に思っていました。
SwiftでJSONをパースする (2:45)
SwiftでJSONをパースする際に起こる問題は何でしょうか?Swiftは静的型付け言語なので、型のある変数にオブジェクトを投げることができず、Objective-Cでやっていたように、コンパイラは型が宣言されたものであると信じています。今コンパイラはランタイムエラーが起こらないことを確認しています。これはすばらしいです。ですが、やることがもっとあることを意味しています。
なので、こんなユーザーモデルがあるとします。
struct User {
let id: Int
let name: String
let age: Int
}
Swift 2以前では、こんな風になっていたでしょう。
let user: User?
if let user = json["user"] as? [String: String] {
if let id = user["id"] {
if let name = user["name"] {
if let age = user["age"] {
user = User(id, name: name, age: age)
}
}
}
}
基本的なモデルオブジェクトを見ると、ユーザオブジェクトを作成するために if let
のネストが連続しています。プロパティが多くなって、かなり煩雑で、冗長的です。そして美しくありません。
また、すべてのプロパティを取得しないとどうなりますか?何も起こりません。もっと良い方法があるのでしょうか? Swift2では、guard文が導入されて、ちょっとよくなりました。
let user: User?
guard let user = json["user"] as? [String: String] {
if let id = user["id"],
name = user["name"],
age = user["age"] {
user = User(id, name: name, age: age)
}
} else {
return nil
}
これらの条件を満たしたときのみ、Userオブジェクトがつくられます。しかし繰り返しますが、エラーハンドリング部分ではあまり進んでいません。
do-catch
はどうでしょうか? do-catch
では、do
ブロックでエラーを処理でき、エラーが発生したかどうかを判断するために catch
で句にマッチします。これは順番に複数のtryに使えますが、do-catch
を実行している関数がthrowしていることを確認する必要もあります。
このことから何がわかるでしょうか?同じ問題を他の人がどのように解決しようとしているかを知りたかったので、GitHubで人気のあるライブラリを分析することにしました。プロジェクトの中を見て、基礎を成している実装が何であったかを知りたいと思っていました。APIの単純さについては心配していませんでした。なぜなら、単に外部向けのものだからです。実際にJSONをどのように解析しているのでしょうか?
議題 (5:02)
いくつかの議題があります。例を通してみてみましょう。
import SwiftyJSON
struct User {
let id: Int
let name: String
let age: Int
}
let json = JSON(["id":2378, "name":"Jack", "age": 23])
if let name = json["name"].string {
// Do a thing
} else {
print(json["name"].error)
}
SwiftyJSON でデコードする例です。nameを得るためにif-let
を用いており、.string
というシンタックスで値を取り出しています。とても簡単ですね。美しいです。コードブロックの中でユーザオブジェクトのnameというプロパティを初期化できますし、elseでエラーを表示しています。
public let ErrorUnsupportedType: Int = 999
public let ErrorIndexOutOfBounds: Int = 900
public let ErrorWrongType: Int = 901
public let ErrorNotExist: Int = 500
public let ErrorInvalidJSON: Int = 490
SwiftyJSON.swift
ファイルの先頭に独自のエラーが定義されています。多くのケースに対応しているわけではありませんが、型の不一致、間違ったエラー、存在していない、不正なJSONなど、実際のエラーに対応しています。enumで定義されていないのは面白いと思いました。なぜそのようになっているのかはわかりませんが、基本的にはただのエラーコードということです。
import Mapper
struct User {
...
}
extension User : Mappable {
init(map: Mapper) throws {
try id = map.from("id")
try name = map.from("name")
age = map.optionalFrom("age")
}
}
Mapper はLyftの人々が作成したJSONデシリアライズライブラリです。オブジェクトにthrowするイニシャライザがあり、値をセットします。内部的には、Mapper.swift
にfrom
というフィールド関数があり、JSONを得ます。これがフィールドが空かどうかをチェックして、値をセットします。フィールドが空なら、MapperErrorをthrowします。
public enum MapperError: ErrorType {
case ConvertibleError(value: AnyObject?, type: Any.Type)
case CustomError(field: String?, message: String)
case InvalidRawValueError(field: String, value: Any, type: Any.Type)
case MissingFieldError(field: String)
case TypeMismatchError(field: String, value: AnyObject, type: Any.Type)
}
enumです。ここで初めて登場しました。これらはMapperError.swift
ファイルに宣言されており、throwしてenumでそれぞれのErrorTypeから値を抽出できるので、かっこいいと思いました。
import Freddy
struct User {
...
}
extension User: JSONDecodable {
public init(json value: JSON) throws {
id = try value.int("id")
name = try value.string("name")
age = try value.int("age")
}
}
Freddy も有名なライブラリです。オブジェクトのextensionを作り、JSONDecodableプロトコルに適合させます。これはJSONインスタンスからオブジェクトのインスタンスを作ります。ほとんどのプリミティブ型を処理し、init
で実装されているすべてのextensionを持っています。Stringやint、bool,あなたが命名したものを含みます。内部のJSON initからのthrowのエラーは、変換可能な値ではなく、かなり説明的なメッセージを吐き出します。
public enum Error: ErrorType {
case IndexOutOfBounds(index: Swift.Int)
case KeyNotFound(key: Swift.String)
case UnexpectedSubscript(type: JSONPathType.Type)
case ValueNotConvertible(value: JSON, to: Any.Type)
}
またです。ErrorTypeがenumになっています。これをdo-catch
で行うことが推奨されています。なのでモデルのinitでなくてもいいのです。モデルのinitからこれを行わない場合は、initのextensionがあります。
もうひとつライブラリがあります。Decodableです。
import Decodable
struct User {
...
}
extension User: Decodable {
static func decode(j: Any) throws -> User {
return try User(
id: j => "id",
name: j => "name",
age: j => "age"
)
}
}
Decodableプロトコル、decodeを実装し、Userオブジェクトのextensionを再度作成します。Decodable.swift
に宣言されたプロトコルがあるからです。さっき見たように、プリミティブ型のextensionにdecode
を追加しています。
public enum DecodingError: ErrorProtocol, Equatable {
case typeMismatch(expected: Any.Type, actual: Any.Type, Metadata)
case missingKey(String, Metadata)
case rawRepresentableInitializationError(rawValue: Any, Metadata)
case other(ErrorProtocol, Metadata)
}
抽出する人へすてきなことがあります。何を期待して、何をthrowするのかです。これはREADMEにありました。型にあるそれぞれのenumで、どのように実装するのかを見るのはかなり面白そうだと思いました。
発見したこと(9:00)
この発見したことリストを一部ご紹介したいと思います。これらのライブラリ間の相違点や類似点に気付かれましたら幸いです。
一般的に、throwするプロトコルがあります。これは、インスタンスのイニシャライザと指定されたJSONオブジェクトの上にあり、JSONの値から派生した何らかのエラーをthrowします。DecodableSyllable、JSONDecodable、JSONConvertibleという名前になっていると思います。
もう1つ興味深いのは、オブジェクトのプロパティをマップするさまざまな方法です。値を実際にどのようにデコードしていますか?カスタム演算子をオーバーライドするライブラリがあります。それは演算子を過負荷にするでしょう。他のライブラリはSwiftyJSONのように、その型の添字を作成するだけです。演算子をオーバーロードするライブラリは、マップされたオブジェクト上のプロパティを抽出していることがわかりました。最初から圧倒的に見えるかもしれませんが、ドキュメントでは、概要と、オプショナル、非オプショナル、配列、辞書のために何をしていたのかをよく説明されていました。ライブラリで多くの演算子を見ましたが、狂ってました。日々の仕事で頻繁に使う演算子に負荷をかけることはありません。だから、人々が何を思いついたかを見るのは面白かったです。
添字 JSONについて最初に学んだとき、辞書に入れ子になっているアイテムのように見えて、値を抽出するためにオブジェクトにインデックスを付けることが望ましいと思いました。とても直感的でした。しかし、私がJSONのデコードを始めたとき、それを行うことはできず、SwiftyJSONは、シームレスに行っており、本当にきれいだと思いました。彼らは基本的な実装のカスタム添え字をサポートしています。
エラー処理 どのような場合でも、何らかの形で結果を処理する必要があります。 1つの方法は、Resultプロトコルを実装し、成功または失敗に伴ってthrowされたエラー型をラップすることです。他のライブラリは、 do
でアンラップして、大文字と小文字を区別したカスタムエラーをcatchすることを推奨しています。これは、ほとんどのライブラリが、これまでみたように、それぞれの型に対してこれらのカスタムエラーを持ち、独自のエラー処理を実装するのに役立ちます。しかし、いくつかの安全性を追加したい場合は、別の失敗または成功でその列挙型のエラー型で定義されているエラーをラップすることができます。次に、これまで見てきたライブラリのいくつかのエラー型を見てみましょう。
まとめ (12:03)
結局のところ私は多くのライブラリが好きでしたが、チームは解決策を選ぶための特定の基準を持っていました。
主な違いの1つは、型チェックとそれぞれのエラー処理です。どのような選択をしても、ランタイムエラーを防ぐ方法でJSONを受け取るのに、コンパイラが役立つ型安全なソリューションがあることを確認するソリューションが必要になります。SuccessとFailureに対するFailureのケースをラップしたResultと同じように、宣言的なエラー処理を追加することもできます。
ジェネリックが使えるものであることを望んでいるでしょう。多くのライブラリが、プリミティブ型の場合、intとboolとstringをデコードすることを見ました。これは、ジェネリック関数を持つのと同様です。他の人は自分自身のint、extension、initsを持っていて、私も興味深いと思っていました。
JSONをデコードするための宣言的で型安全な方法を探しているなら、ArgoとResultを使うのが良い選択かもしれません。 Argoの使用例を次に示します。
import Argo
import Curry
import Result
struct User {
let id: Int
let name: String
let age: Int?
}
extension User: Decodable {
static func decode(j: JSON) -> Decoded<User> {
return curry(User.init)
<^> j <| "id"
<*> j <| "name"
<*> j <|? "age"
}
}
デコードを行うDecodableプロトコルに適合したUser
オブジェクトが得られました。それをJSONに渡すと、curryになります。繰り返しますが、最初にみたカリー化と同じではありません。Swift 3では廃止予定です。Argoのカリー化の基礎となる実装がそうでないことを確認してください。
id
、name
、 age
を抽出することができます。これらのプロパティを取得しなければ失敗し、それぞれint、string、intにマップされます。
このトピックを提供してくれたコミュニティメンバーに感謝します。私があまり精通していたものではなかったので、たくさんの疑問がありました。質問を返してくれたり、プルリクエストを承認してくれたチームメンバーに感謝します。JSONデコードライブラリにコントリビュートしてくださった皆様、ありがとうございます。
About the content
2016年9月のtry! Swift NYCの講演です。映像はRealmによって撮影・録音され、主催者の許可を得て公開しています。