The opinionated Tao of Optional Mappage

Given these two functions:

  • p(x) -> Void executes side effects in its body.
  • f(x) -> y produces a transformed value

Prefer:

  • if let x = x { p(x) } to x.map({ p(x) }) when executing a function for its side effects.
  • return x.map({ f(x) }) when returning a transformed value based on an optional.

You may think of map as inherently functional but x.map({ p(x) }) does not warn on unused result. In this case, the return type is Void. If  you assign the value, the compiler warns of the unexpected ()? return type.

I’ve often thought that forEach would be a better approach for procedural calls than map when working with optionals. This is based on the often disputed notion that optionals are like tiny collections with either zero or one members.

This is not how optionals are conceived, and the swift team has mentioned so on several occasions. Dmitri Gribenko wrote:

My argument is that map on collections and on optionals are two completely different things.  They unify on a level that does not exist in Swift (higher-kinded types).  We could name Optional.map() differently, and that would be a separate decision from renaming Collection.map(), but we used the name `map` primarily because of precedent, not because there is a HKT notion of these two operations
being the same…

I can’t say that it is not valid to think about an Optional as a tiny collection, but as implemented in Swift, .map() does objectively behave differently…Optional is a result of a computation that can fail.  .map() allows you to chain more things onto a computation if it succeeded, and do nothing if it failed already.

Until such things are resolved, you might consider replacing procedural calls on optionals with a custom forEach implementation:

extension Optional {
    public func forEach(perform action: (Wrapped) -> Void) {
        self.map(action)
    }
}

let x: String? = "Hello"
x.forEach { print($0) }

I think it better presents the notion that you’re not returning a value from mapping but executing a procedure for its side effects on any wrapped value within an optional.

What do you think?

11 Comments

  • I wouldn’t overload ‘forEach’ as that would be misleading when digging through an unfamiliar codebase. It might imply at first that x is a collection, rather than an optional, even more so than map does.

    Perhaps, something closer to the spirit of ‘x.doIfNotNil { print($0) }’ or ‘x.perform { print($0) }’. I’m terrible at naming.

    • Apply maybe? But again that’s highly correlated with collections

      • I assume that `do` is reserved. But perhaps `doBlock`?

        `perform`?

        • How about `then`

          x.then { print($0) }

        • I think `do` is a good name as it fits nicely for collections and optionals (and other monads). Smalltalk uses it for collections.

          Using it in Swift is actually possible. You just need backticks at the declaration site (not at the use site!):


          extension Optional {
          public func `do`(perform action: (Wrapped) -> Void) {
          self.map(action)
          }
          }

          let x: String? = "Hello"
          x.do { print($0) }

  • Not really answering the question, but can’t you write `x.map(p)` and `x.map(f)` and `x.map(print)` instead of using an explicit closure?

    • You can with `p` and `f` but for print you need

      func printit<T>(_ x: T) { print(x) }
      x.map(printit)
  • How about renaming `Optional.map()` as `Optional.maybeDo()`? This would be more inline with `Optional`’s resemblance to Haskell’s `maybe`.

  • I disagree with the quote from Dmitri. Just that the level of unification (HKT) cannot currently expressed in Swift cleanly does not mean that optionals and collections (and other monads) are not unified on a conceptual level. They are and this should be reflected as good as possible in Swift and not be hidden artificially.

  • How about `ifSome` since Optional has cases .some and .none:

    extension Optional {
    public func ifSome(perform action: (Wrapped) throws -> Void) rethrows {
    try self.map(action)
    }
    }

    let x: String? = "Hello"
    x.ifSome { print($0) }

    • I see `some` as an implementation detail, so I’d prefer not to reference it. This morning, I updated my utilities to use `do` as Thorsten suggested

Join the Discussion

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>