ペンギン村 Tech Blog

技術をこよなく愛するエンジニア集団が在住するペンギン村から、世界へ役立つ(かもしれない)技術情報を発信する技術系ブログです。某アラレちゃんが済む村とは一切関係ありません。んちゃ!

Swift 4.2 で追加される @dynamicMemberLookup メモ(SE-0195)

全地球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
    }
}

最後の subscriptdynamicMemberLookup になっていますが、これを利用しない場合は以下のようになります。

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 な物語はまだまだ続くのでしょう。

それでは。