Creating lenses with Swift macros (usecase for arbitrary names at the global scope)

Actually, I am, and I've been for a very long time. But I adopt the principles (like purity and higher-order functions), and not necessarily a specific style of programming, based on composing functions with operators (which makes more sense in other languages, that lack features that Swift has though).

To me, this is a pure function

extension User {
  mutating func prependWorkersHomeStreet() {
    for index in workplace.workers.indices {
      workplace.workers[index].home.street = "home \(workplace.workers[index].home.street)"
    }
  }
}

the only difference is that it's written using regular Swift features.

The specific way in which the function is implemented is rather imperative, but that's just because Swift standard library lacks declarative functions for collection manipulation that are also mutating (something like mapInPlace). Those functions are very useful in general, so if I needed them, I would declare them in a generic way, and they would work as compositional tools in my codebase, for example:

extension MutableCollection {
  mutating func modifyEach(_ transform: (inout Element) -> Void) {
    for index in indices {
      transform(&self[index])
    }
  }
}

extension RangeReplaceableCollection {
  mutating func prepend(contentsOf toPrepend: some Collection<Element>) {
    insert(contentsOf: toPrepend, at: startIndex)
  }
}

extension User {
  mutating func prependWorkersHomeStreet() {
    workplace.workers.modifyEach {
      $0.home.street.prepend(contentsOf: "home ")
    }
  }
}

modifyEach still uses indices, but it's hardly "index tracking": it's very small and self contained. Also, if we ever get for inout, which would be a very natural extension of the language, it could be written in a even simpler way, like

extension MutableCollection {
  mutating func modifyEach(_ transform: (inout Element) -> Void) {
    for inout element in self {
      transform(&element)
    }
  }
}

to the point that there would be probably no point anymore in declaring modifyEach in the first place.

Now, having written for years code like this (not identical but very similar, and in the same spirit)

let prependWorkersHomeStreet =
  \User.Lenses.workplace.workers.traverse.home.street %~ { "home " + $0 }

// some function where I'm describing state transitions:
modify(\State.Lenses.user %~ prependWorkersHomeStreet)

I can tell you that, personally, I find my version a lot clearer and more readable, and much easier to both digest for people that are just learning the language, and to introduce in new teams, without lacking in expressive power.

Declarative programming is not really about "what" vs "how" (there's a "what" and a "how" at all levels of abstraction): it's about denotational semantics, which is still achieved by composing (pure) transformations on data structures by using regular Swift tools.

I think the strong distinction that you're making between coding styles is not particularly relevant here: for example, no one should write this code

var s = state
for m in modifications {
  m(&s)
}
someFn(s)

because Swift has already reduce(into:)

someFn(modifications.reduce(into: state) { state, modify in
  modify(&state)
})

I think the imperative Swift code that you're showing is not good, and not the code I would use for a comparison.

mutating functions (named or anonymous) are very convenient to implement, but when used in a context that requires a returned value (for example, the closure passed to map) they end up needing boilerplate (the var m_x = x boilerplate that you've shown). But this can be also solved with another natural extension to the language, the with function, so one can write

workers.map {
  $0.with {
    $0.prependWorkersHomeStreet()
  }
}

which is still compositional and denotational, but more in line, to me, with the rest of the language.

6 Likes