SwiftでLens(すごいgetter/setter)を実装してみた

  • 45
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

Swift Advent Calendar 2015 - Qiitaの20日目の記事です。
遅くなってしまいごめんなさい。

TL;DL

  • 不変性を保ちつつ、ネストしたデータ構造に対するアクセスをLensの合成で扱える

きっかけ

以前、Lens&Prism勉強会 - connpass
に参加した際に興味を持ったので今回Swiftで簡易的な実装をしてみました。
to4iki/Monocle

また、今回間違って説明していることなどありましたらコメント欄で教えていただけると嬉しいです!

Lens

もともとはHaskellのlensパッケージが存在していて、
今回参考にしたのは、それをScalaで実装したMonocleのLensです。
(MonocleはLens以外もTraversal, Optional, Prism, Isoを提供しています)

Java実装: functionaljava/Lens.javaや、
Swift実装: Monocleも既に存在します。
また、SwiftzのLens実装を独立したフレームワークとして切り出したFocusというフレームワークも存在します。

Lensに関しての概念など勉強不足なので、この記事では触れず、
どういった時に使用するのか、威力を発揮するのかを説明してみたいと思います。(すみません、勉強します)

なので、かなり乱暴ですが。。
一旦、すごいgetter/setterとして捉えて頂くとイメージしやすいかもしれません。

LensはJavaなどで言うgetterやsetterを抽象化した概念で、不変性を保ちつつネストしたデータ構造に対するアクセスをLensの合成で表現できるようにしたものになります。
Monocleとかいうのがありまして - 独学大学情報学部

ユースケース

例えば、下記のようなネストしたデータ構造が存在している時、

struct Street {
    let name: String
}
struct Address {
    let street: Street
}
struct Company {
    let address: Address
}
struct Employee {
    let company: Company
}

employee...nameを不変性を保ったまま書き換えたいときにswiftだと、
下記のように変更前のnameを使い、新たにEmployeeを作成する方法が思い浮かびます。

let employee = Employee(company: Company(address: Address(street: Street(name: "street"))))

// street.nameの頭文字を大文字に
Employee(company:
    Company(address:
        Address(street:
            Street(name: employee.company.address.street.name.capitalizedName)
        )
    )
)

少し冗長ですね。。

ちなみにネストしたデータ構造がvar(ミュータブル)な場合は直感的に書けます。

struct Street {
    var name: String
}
struct Address {
    var street: Street
}
struct Company {
    var address: Address
}
struct Employee {
    var company: Company
}

var employee = Employee(company: Company(address: Address(street: Street(name: "street"))))
employee.company.address.street.name = "new street"

只、これだと不変性を担保できないです。

なので、
「不変性を担ちつつ + varのようにすっきりと書く」ためにLensを使用したいと思います。

Lensを使って不変構造にアクセスする

準備

Lens.swift
// Lensの定義
public struct Lens<A, B> {
    private let getter: A -> B
    private let setter: (A, B) -> A

    public init(getter: A -> B, setter: (A, B) -> A) {
        self.getter = getter
        self.setter = setter
    }
}

対象のデータ型に対してLensを指定(データに対するgetter/setter用の関数を設定)

let _name: Lens<Street, String> = Lens(getter: { $0.name }, setter: { Street(name: $1) })
let _street: Lens<Address, Street> = Lens(getter: { $0.street }, setter: { Address(street: $1) })
let _address: Lens<Company, Address> = Lens(getter: { $0.address }, setter: { Company(address: $1) })
let _company: Lens<Employee, Company> = Lens(getter: { $0.company }, setter: { Employee(company: $1) })

参考にしたScalaのMonocleでは、Lensの定義をマクロやアノテーションで自動生成できるぽいので、ここら辺がもう少し楽になると思います。

使い方

Lens.swift
// 基本関数
extension Lens {
    public func get(from: A) -> B {
        return getter(from)
    }

    public func set(from: A, _ to: B) -> A {
        return setter(from, to)
    }

    public func modify(from: A, f: B -> B) -> A {
        return set(from, f(get(from)))
    }
}

// Lensの合成用関数
extension Lens {
    public func compose<C>(other: Lens<B, C>) -> Lens<A, C> {
        return Lens<A, C>(
            getter: { (a: A) -> C in
                other.get(self.get(a))
            },
            setter: { (a: A, c: C) -> A in
                self.set(a, other.set(self.get(a), c))
            }
        )
    }
}

データ型に対するLensの定義が済んだら、
ネストしたデータに対するアクセスをデータ型によるLensの合成で表現します。

let employee = Employee(company: Company(address: Address(street: Street(name: "street"))))

// 合成したLensに対して関数適用
(_company.compose(_address.compose(_street.compose(_name)))).modify(employee) { $0.capitalizedString }
// => Employee(company: Company(address: Address(street: Street(name: "Street"))))

合成関数composeLensのエイリアスとして>>>1が定義されているので、もう少しすっきりと書けます。

(_company >>> _address >>> _street >>> _name).modify(employee) { $0.capitalizedString }
// => Employee(company: Company(address: Address(street: Street(name: "Street"))))

値の取得(get)、書き換え(set)も行えます

// 取得
(_company >>> _address >>> _street >>> _name).get(employee)
// => street

// 書き換え
(_company >>> _address >>> _street >>> _name).set(employee, "new street")
// => Employee(company: Company(address: Address(street: Street(name: "new street"))))

Lensの合成を行うことで、不変性を保ちつつすっきりと書けるようになりました!!

まとめ

今回は簡易的な実装で試してみたといった記事で、
もう少し具体的にiOS開発と絡め、どういった場面で使えるかを探りたいなと思います。
(またの機会に、具体例を)

see also

この投稿は Swift Advent Calendar 201520日目の記事です。