Линзы или геттеры и сеттеры в функциональном свифте

В идеальном функциональном мире состояние объекта не может изменяться после создания, а каждая функция работает только с теми данными, которые она получает, не имея никакой информации о существовании всего, что есть в програме, кроме других функций. И поскольку одна из самых мощных функций Свифта является возможность писать код в функциональном стиле, то почему бы не разобраться ради интереса как можно работать с не изменяемыми обьектами? Главная идея в том, что поскольку данные не изменяемые, то надо создавать копию этих данных с нужными нам изменениями при помощи пары геттеров и сеттеров, которые называются линзами.

Для начала напишем две структуры:

struct User {
    let name: String
    let age: Int
}
struct Corp{
    let name: String
    let user: User
}

let john = User(name: "John", age: 18)
let corp = Corp(name: "Google", user: john)

Эти структуры полностью immutable, то есть их состояние после создания никак изменить нельзя. Теперь напишем сеттер и геттер для поля name в структуре User, геттер просто возвращает имя, а сеттер создает нового юзера с новым именем:

extension User{
    func getName() -> String {
        return self.name
    }
    func setName(name: String) -> User {
        return User(name: name, age: age)
    }
}

То же самое сделаем для структуры Corp, проверяем:

john.getName() // => "John"
john.setName("John M").getName() // => "John M"
john.getName() // => "John"
corp.setUser(corp.getUser().setName("John M")).getUser().getName() // => "John M"

Теперь введем новую абстракцию – линзу, которая позволит вынести повторяемый код сеттеров и геттеров:

struct Lens <A, B> {
    let get: A -> B
    let set: (B, A) -> A
}

Создадим линзу для имени юзера:

let userNameLens = Lens<User, String>(
    get: { user in user.name },
    set: { (name, user) in User(name:name, age: user.age) }
)
userNameLens(john) // => "John"
userNameLens("Johg 2", corp.getUser()).name // => "John 2"

Линза для структуры Corp будет возвращать пользователя и создавать новую структуру с заданым пользователем.

let corpUserLens = Lens<Corp, User>(
    get: { corp in corp.user },
    set: { user, corp in Corp(name: corp.name, user: user) }
)

corpUserLens.set(
    userNameLens.set(
        "Test User",
        corpUserLens.get(corp)
    ),
    corp
)

Теперь напишем функцию для композиции линз, что-бы можно было легко получить доступ напрямую к имени пользователя структуры Corp. Функция get – это результат get’ов двух линз, set чуть сложнее, на примере композицию двух линз corpUserLens и userNameLens происходит следующее: сначала corpUserLens получит пользователя из Corp, затем userNameLens создаст нового пользователя с новым именем и еще раз линза corpUserLens создаст новую корпорацию с новым пользователем.

func compose<A, B, C>(lhs: Lens<A, B>, _ rhs: Lens<B, C>) -> Lens<A, C> {
    return Lens<A, C> (
        get: { a in rhs.get(lhs.get(a)) },
        set: { (c, a) in lhs.set(rhs.set(c, lhs.get(a)), a) }
    )
}
let composedLens = compose(corpUserLens, userNameLens)

composedLens.get(corp)
composedLens.set("test", corp).getUser().getName()

Для удобства и большей наглядности напишем функцию-оператор для композиции линз, так как из выражения composedLens.set(“test”, corp) не понятно, что в конечном счете будет использована линза изменения имени у пользователя.

func * <A, B, C> (lhs: Lens<A, B>, rhs: Lens<B, C>) -> Lens<A, C> {
    return compose(lhs, rhs)
}

Теперь все намного лучше:

(corpUserLens * userNameLens).get(corp)
(corpUserLens * userNameLens).set("User", corp)

Добавим еще немного синтаксического сахара, заменим set() на оператор:

infix operator ~> { associativity left precedence 100 }
func ~> <A, B> (lhs: Lens<A, B>, rhs: B) -> A -> A {
    return { a in lhs.set(rhs, a) }
}

(userNameLens ~> "Test name")(john)

Для комбинирования линз добавим еще один оператор:

infix operator |> { associativity left precedence 80 }
func |> <A, B> (x: A, f: A -> B) -> B {
    return f(x)
}
func |> <A, B, C> (f: A -> B, g: B -> C) -> A -> C {
    return { g(f($0)) }
}

john |> userNameLens ~> "New Name"

В итоге получится удобная конструкция:

corpUserLens * userNameLens ~> "test name"
|> corpUserLens * ageUserLens ~> 26
|> someOtherLens * someOtherLens2 ~> String()
Share
Send
2016   Lens   Swift
Your comment
won’t be published

HTML will not work

Ctrl + Enter
Popular