全地球100億人のSwifterな皆さん、コンバトラーっ! tobi462 でーっす!
え、いつもとノリが違う?まぁ、そういう日もあるんじゃないでしょうか。
そんなわけで、前回・前々回と続き Swift 4.2 の記事です。
今回は、Swift 作者であるラトナーさんの Proposal (SE-0195) のようですよ。
Tl;Dr
@dynamicMemberLookup struct AnimeCharacter { subscript(dynamicMember member: String) -> String { let properties = ["name": "阿良々木月火", "music": "白金ディスコ"] return properties[member, default: ""] } } character.name // => "阿良々木月火" character.music // => "白金ディスコ"
dynamic?
Swift では Objective-C と異なり、コンパイル時に多くの静的チェックが行われます。
それはメソッド呼び出しやプロパティアクセスについても同様で、コンパイル時に存在しないシンボルへアクセスするようなコードはコンパイルエラーになります。
struct Person { } let alice = Person() alice.name // コンパイルエラー
Swift などの静的型付けに慣れ親しんだ方はこれが極めて自然な挙動に見えるかもしれません。実際、コンパイル時にチェックされることでタイポなどのミスを事前に検出できるという大きなメリットがあります。
しかし、Swift から Python などの動的型付けを呼び出したい、つまり相互運用性を考えた時はどうでしょうか?
以下は Proposal から抜粋したコードです。
// import pickle let pickle = Python.get(member: "import")("pickle") // file = open(filename) let file = Python.get(member: "open")(filename) // blob = file.read() let blob = file.get(member: "read")()
これは Swift 4.1 においても正当なコードですが明らかに冗長に感じます。
すなわち以下のように書けたらスマートではないでしょうか?
// import pickle let pickle = Python.import("pickle") // file = open(filename) let file = Python.open(filename) // blob = file.read() let blob = file.read()
というのが、この Proposal の発端のようです。
@dynamicMemberLookup
それを実現するために @dynamicMemberLookup
というアノテーションが導入されました。
class や struct 、 enum に付けられるもので、あわせて subscript(dynamicMember :xxx)
の実装が必要になります(実装しないとコンパイルエラー)。
@dynamicMemberLookup struct AnimeCharacter { subscript(dynamicMember member: String) -> String { let properties = ["name": "阿良々木月火", "music": "白金ディスコ"] return properties[member, default: ""] } }
上記は String
を受け取る subscript(dynamicMember)
を実装していますが、これは以下のように呼び出すことが出来ます。
let character = Character character.name // => "阿良々木月火" character.music // => "白金ディスコ"
このように事前に定義されていないシンボルを呼び出せているように見えるのが、 dynamicMemberLookup
のメリットといえるでしょう。
あまり Swifty に感じない人も多いかもしれませんが、これは以下のコードのシンタックスシュガーと思えば良いでしょう。
let character = Character character[dynamicMember: "name"] // => "阿良々木月火" character[dynamicMember: "music"] // => "白金ディスコ"
こう考えると、Dictionary などへのアクセスと同様に感じられるのではないでしょうか。
メソッド呼び出し風の書き方
さて、メンバーへのアクセス風の構文は分かりましたが、メソッド呼び出し風の書き方をしたい場合はどうなるのでしょうか?
それには以下のようにクロージャを返す subscript(dynamicMember: xxx)
を宣言します。
@dynamicMemberLookup struct Person { subscript(dynamicMember member: String) -> (_ input: String) -> Void { return { print("Hello, \($0)") } } }
これは以下のように呼び出せます。
let person = Person() person.hello("Goodbye") // => "Hello, Goodbye"
少し分かりづらいかもしれませんが、.hello
でクロージャを取得し、それに対して (“Goodbye”)
で呼び出しを行っている感じです。関数型プログラミングに通じている方は「カリー化」されている関数に対する呼び出しと考えると分かりやすいかもしれません。(これがカリー化という訳ではないのですが)
使いどころ?
さて、ここまで @dynamicMemberLookup
について見てきましたが、果たしてどういったところに使えるのでしょうか?
お気づきだと思いますが、この機能が有効に適用できる箇所は限られています。
前述したようにコンパイル時にシンボルがあることを検出できるのが静的型付け言語のメリットです。
例えば、すべてのプロパティ・メソッドアクセスをこの機能で置き換えることは可能ですが、それはもはや動的型付け言語と同様「実行しなければ正しく動くかわからない」状態になるでしょう。
1つの例としては前述したPythonなどの動的型付け言語との相互運用性です。
// import pickle let pickle = Python.import("pickle") // file = open(filename) let file = Python.open(filename) // blob = file.read() let blob = file.read()
このようにドット記法で呼び出せるのはとてもスマートだと感じます。
もう1つの例として Proposal では JSON のデータ構造へのアクセスが挙げられています。
@dynamicMemberLookup enum JSON { case intValue(Int) case stringValue(String) case arrayValue(Array<JSON>) case dictionaryValue(Dictionary<String, JSON>) var stringValue: String? { if case .stringValue(let str) = self { return str } return nil } subscript(index: Int) -> JSON? { if case .arrayValue(let arr) = self { return index < arr.count ? arr[index] : nil } return nil } subscript(key: String) -> JSON? { if case .dictionaryValue(let dict) = self { return dict[key] } return nil } subscript(dynamicMember member: String) -> JSON? { if case .dictionaryValue(let dict) = self { return dict[member] } return nil } }
最後の subscript
が dynamicMemberLookup
になっていますが、これを利用しない場合は以下のようになります。
let json = JSON.stringValue("Example") json[0]?["name"]?["first"]?.stringValue
これは SwiftyJSON/SwiftyJSON ともよく似た見た目かと思います。
これが以下のように書けるようになります。
json[0]?.name?.first?.stringValue
Subscriptのアクセスに[]
が不要となっているので、とてもスッキリした見た目になっています。
ご利用は計画的に
このように事前にすべてを定義できないけれども、 .
を使ってアクセスしたほうがスッキリするケースで @dynamicMemberLookup
を活用できるかと思います。
逆に言えば、それ以外のケースでは利用は慎重になるべきでしょう。
例えば 事前にすべてを定義できない
と聞くと、 UserDefaults
のラッパーとして利用することを考えることも出来るかもしれません。
@dynamicMemberLookup struct UserDefaultsWrapper { let userDefaults: UserDefaults subscript(dynamicMember member: String) -> String? { return userDefaults.string(forKey: member) } } let wrapper = UserDefaultsWrapper(userDefaults: UserDefaults.standard) let userName = wrapper.userName
しかし、これは明らかにアンチパターンです。
なぜなら userName
とタイプすべきところを userNeme
とタイポしてもコンパイラは何の警告も出さないためです。
さらに .
アクセスされているので、一見するとこのコードはコンパイル時に静的に解決されているように見えてしまいます。
このように呼び出し時のタイプ量を減らすことを目的に利用するのは明らかに間違いでしょう。
おわり
というわけで、今回は @dynamicMemberLookup
の紹介というかメモでした。
実のところ、私は最初にこの機能を知った時に Swift らしくないと感じました。静的にシンボルを解決するというSwiftの思想と相反すると感じた為です。
しかし、「Swifty な呼び出しを実現するためのシンタックスシュガー」と捉え直すと、まぁ使い方さえ間違えなければトレードオフとしてはありかな、という意見に落ち着きました(なんだか上から目線?)。
わたしの知らない Swift な物語はまだまだ続くのでしょう。
それでは。