[Swift]Dynamic Member LookupからSwift5.1で追加されたKeyPath Member Lookupまで

Swift4.2 のDynamic Member Lookup。Proposal の Author が Chris Lattner ということで、重要な機能だということはわかっていたのですが、きちんと理解せずに過ごしていました。

すると 5.0 ではそれの拡張のDynamic Callableが追加され 5.1 では KeyPath に対して@dynamicMemberLookup できるKeyPath Member Lookupが追加されることがわかり、さらに 5.1 で追加される様々な言語機能は SwiftUI などの新しいフレームワークでふんだんに使われているのでこの辺りを疎かにするとこれから支障が出るかもしれないと思いました。

そこで今回の記事では Swift4.2 の新機能として紹介されたDynamic Member Lookupを、5.1 で追加されるKeyPath Member Lookupを見ていきたいと思います。

Dynamic Member Lookup の概要

コンパイル時に存在しないプロパティに対して.シンタックスを使用してアクセス出来ます。実行時にメンバーが存在するはずという前提でコーディングします。 これによって Python のような他の言語との相互運用性をサポートできます。Google で Swift for Tensorflow に携わっている Chris Lattner が Author として Proposal を出しています。

サンプルコード

enum KeySwitch {
    case linear
    case tactile
    case clicky
}

@dynamicMemberLookup class MechanicalKeyboard {
    let keySwitch: KeySwitch
    let name: String
    let numberOfKeys: Int

    init(keySwitch: KeySwitch, name: String, number: Int) {
        self.keySwitch = keySwitch
        self.name = name
        self.numberOfKeys = number
    }

    subscript(dynamicMember key: String) -> Any {
        switch key {
        case "switch":
            return keySwitch
        case "type":
            return "This keyboarwd is \(name)"
        case "numbers":
            return numberOfKeys
        default:
            return ""
        }
    }
}

let keyboard = MechanicalKeyboard(keySwitch: .tactile, name: "kbd67", number: 67)
print(keyboard.switch)
print(keyboard.type)
print(keyboard.numbers)

例がわかりにくいのはご容赦ください。@dynamicMemberLookup は'subscript(dynamicMember:)というメソッドの実装を必要とします。上記のサンプルコードでは 3 つの定義していないプロパティにアクセス出来るようになっています。

このsubscript(dynamicMember:)というメソッドでプロパティ名をそのまま返すだけのものに書き換えてみます。

@dynamicMemberLookup class MechanicalKeyboard {
    // 略

    subscript(dynamicMember key: String) -> Any {
        // そのまま受け取ったStringを返すだけ
        return key
    }
}


let keyboard = MechanicalKeyboard(keySwitch: .tactile, name: "kbd67", number: 67)
// ドットでアクセスしたプロパティ名がそのまま返る
print(keyboard.switch) // switch
print(keyboard.type) // type
print(keyboard.numbers) // numbers

できました。かなり柔軟な実装が出来る言語仕様ですね。

Dynamic Member Lookup の使い所

@dynamicMemberLookup 属性は基本的にプロパティ名の有効性をチェックしないようにコンパイラに指示します。その点で Swift の優秀なコンパイラの恩恵にあずかることができないのを承知で Dynamic Member Lookup を使用する必要があります。

つまり Dynamic Member Lookup を使用すると実行時に正しく動いているかわかるという意味では動的型付け言語と同じような状態になります。

強力な静的型付け言語のメリットを一部享受しない選択をしつつもこの Dynamic Member Lookup を使用する用途として、Proposal では他言語との相互運用、他には JSON などのデータ構造へのアクセスが紹介されています。

他言語との相互運用

今まで他言語を Swift から扱うときは以下のように書いていたのを

// import pickle
let pickle = Python.get(member: "import")("pickle")

// file = open(filename)
let file = Python.get(member: "open")(filename)

@dynamicMemberLookup を用いると

// import pickle
let pickle = Python.import("pickle")

// file = open(filename)
let file = Python.open(filename)

のように書くことができます。といっても当面の Swift から他言語の資産を利用することになるメインのユースケースはSwift for TensorFlowだと思います。

この Proposal で達成したい主な用途とはいえ普段のアプリケーションの開発において意識することは今の所ないので Proposal のサンプルコードを引用するのみで次に進みます。

JSON のデータ構造へのアクセス

@dynamicMemberLookup を使わないと以下のようなコードではjson[0]?["first"]?.numberValueのようにアクセスをすることになります。

import Foundation
enum JSON {
    case number(Double)
    case string(String)
    case array([JSON])
    case dictionary([String: JSON])
}

extension JSON {
    var numberValue: Double? {
        guard case .number(let n) = self else {
            return nil
        }
        return n
    }

    var stringValue: String? {
        guard case .string(let s) = self else {
            return nil
        }
        return s
    }

    subscript(index: Int) -> JSON? {
        guard case .array(let arr) = self,
            arr.indices.contains(index) else
        {
            return nil
        }
        return arr[index]
    }

    subscript(key: String) -> JSON? {
        guard case .dictionary(let dict) = self else {
            return nil
        }
        return dict[key]
    }
}


let json = JSON.array([
    JSON.dictionary(
        ["first": JSON.number(1),
         "second": JSON.string("2")
        ])
    ])
json[0]?["first"]?.numberValue // 1

SwiftyJSONっぽい書き方ですね。

※参考: SwiftyJson

if let JSONObject = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [[String: Any]],
    let username = (JSONObject[0]["user"] as? [String: Any])?["name"] as? String {
        // There's our username
}

このコードに@dynamicMemberLoocUp を追加して subscrpt(dynamicMember:)を実装してみます。

import Foundation

@dynamicMemberLookup // 追加
enum JSON {
// 略

    subscript(dynamicMember key: String) -> JSON? {
        guard case .dictionary(let dict) = self else {
            return nil
        }
        return dict[key]
    }
}

extension JSON {
// 略
}

let json = JSON.array([
    JSON.dictionary(
        ["first": JSON.number(1),
         "second": JSON.string("2")
        ])
    ])

このように実装すると.シンタックスで値にアクセス出来ます。

json[0]?.first?.numberValue // 1

subscript でアクセスしていくよりはスッキリしていて気持ち良いですね。

Dynamic Member Lookup combined with KeyPath in Swift

Swift4.2 で追加された Dynamic Member Lookup ですが、これにより実行時にプロパティを決定することが可能になりました。先述の通りこれにより動的型付け言語の資産を利用すやすくなったのですが、Swift5.1 から keyPath でも Dynamic Member Lookup の恩恵を得てKey Path Member Lookupという機能が追加されます。

Proposal は以下です。

swift-evolution/0252-keypath-dynamic-member-lookup.md at master · apple/swift-evolution

Key Path Member Lookup

@dynamicMemberLookup属性の機能を KeyPath で活用して、より強力な型の動的メンバーの検索を可能にします。 そして、KeyPath は、参照先プロパティの読み書きに使用できるプロパティの動的表現を提供します。

任意の KeyPath を dynamicMemberLookup 出来るようになったことにより、.シンタックスで型安全に subscript で値にアクセス出来るようになります。

@dynamicMemberLookupattribute を追加した場合、5.0 までは以下のような警告が表示されます。

@dynamicMemberLookup attribute requires 'xxx' to have a 'subscript(dynamicMember:)' method with an 'ExpressibleByStringLiteral' parameter

Swift5.1 では以下のような警告に変わります。

@dynamicMemberLookup attribute requires ‘xxx’ to have a ‘subscript(dynamicMember:)’ method that accepts either ‘ExpressibleByStringLiteral’ or a keypath

Proposal のコードを変更して最低限 KeyPath Member Lookup が動作するコードにしてみます。

@dynamicMemberLookup struct Lens<T> {
    var value: T
    subscript<U>(dynamicMember keyPath: WritableKeyPath<T, U>) -> U {
        return value[keyPath: keyPath]
    }
}

struct Point {
    var x, y: Double
}

struct Rectangle {
    var topLeft, bottomRight: Point
}

let lensRectangle = Lens(value: Rectangle(topLeft: Point(x: 10, y: 10), bottomRight: Point(x: 20, y: 20)))
print(lensRectangle.bottomRight) // Point(x: 20, y: 20) LensのプロパティかのようにPointのtopLeftプロパティにアクセス
print(lensRectangle.topLeft) // Point(x: 10, y: 10)

このように subscript で指定した型に適合する KeyPath であれば\.の記法を使わずに.シンタックスでアクセス出来ます。まるでLensが topLeft や bottomRight プロパティを持っているように見えます。

まとめ

ここまでDynamic Member Lookup や後半で紹介した Swift5.1 で追加される Key Path Member Lookup を見ていきましたが、アプリケーションの開発で頻繁に使う言語機能ではなさそうだなという印象を持ちました。

オブジェクトが持っている単純なプロパティにアクセスしているように見せるところなどは、ライブラリで内部の実装を意識させたくない時とかに使われたりするのだろうか、などと考えながら Proposal を読んでいました。

今回学んだことを活かしてAppleが提供する新しいフレームワークのコードが少しでも読みやすくなればなと思います。

まだまだ理解できていない部分や知らないことが多々あるかと思います。なにかお気づきだったり、ご意見がある際はコメントなどからご指摘いただけるとありがたいです。

次の記事はDynamic Callableの予定です。まだちゃんと読めてないので終わったら実際にコードを書いてみてその後記事にするか考えます。