Should the function be called `map` or `flatMap`?

I'm a little bit confused about the correct naming since the function does not really flatten the types like it's implemented for Optional and it's not a compactMap since the result is not a non-optional type.

import Foundation

public struct Notification<T> {
  ///
  public let date: Date

  ///
  public var value: T

  ///
  public init(date: Date = Date(), value: T) {
    self.date = date
    self.value = value
  }
}

extension Notification {
  ///
  public func map<U>(
    _ transform: (T) throws -> U
  ) rethrows -> Notification<U> {
    return Notification<U>(date: date, value: try transform(value))
  }

  /// Should this be another overload of `map` instead?
  public func flatMap<U>(
    _ transform: (T) throws -> U?
  ) rethrows -> Notification<U>? {
    switch try transform(value) {
    case .some(let newValue):
      return Notification<U>(date: date, value: newValue)
    case .none:
      return nil
    }
  }
}

Your map is a proper map, because it has the signature (Notification<T>, ((T) -> U)) -> Notification<U>.
In general, for any generic type K, map has the signature (K<A>, ((A) -> B)) -> K<B>.

flatmap has a signature of (K<A>, ((A) -> K<B>)) -> K<B>. Since the closure in your "flatMap" isn't returning Notification<U> it's not a flatMap. Calling it "map" would be misleading too, however. I'm not sure if there's a common name for the type of function you propose. It's basically just "transform" composed with "map" (the map on optional, in this case), so:

public func notReallyFlatMap<U>(
    _ transform: (T) throws -> U?
  ) rethrows -> Notification<U>? {
    return transform(value).map { Notification(date: date, value: $0) }
}

That's if you want to use the "established" terms. Of course, you're free to choose whatever terms you like.

5 Likes

I guess transformMap would be better because it's kind of special. And by the way it should be like this or otherwise it won't compile:

return try transform(value).map { Notification<U>(date: date, value: $0) }

Thanks, didn't know the compiler can't infer that.

I don't think that name quite describes what it does. In Swift "transform" is usually used as a parameter name, not in the name of an operation. The "transform" parameter name is used by several methods including map, flatMap and compactMap. It is not indicative of the specific semantics of the operation.

The "special" behavior is that it lifts Optional outside of Notification. If you used map with a transform that returns optional you would get Notification<U>, but since U is an optional type you would have Notification<Optional<Wrapped>>. Your variation will take that same transform but return Optional<Notification<Wrapped>>. I don't know if the FP community has a name for an operation that lifts a functor produced by a transform over the functor on which the operation is provided like this but that is where I would look for prior art.

3 Likes

There is a very general concept that accomplishes this known as "traversal". It completely generalizes the ideas of "flipping" nested containers around. In its most general form it looks like (using Haskell syntax):

(a -> f b) -> t a -> f (t b)

Notice that if you use f = Optional and t = Notification this is precisely your function:

(A -> B?) -> Notifcation<A> -> Notification<B>?

More information can be found here:

http://hackage.haskell.org/package/base-4.11.1.0/docs/Data-Traversable.html
http://hackage.haskell.org/package/lens-4.16.1/docs/Control-Lens-Traversal.html

So if you wanted to go with that precedent, the name would be called traverse. The interesting thing though is that you can use other f type constructors besides Optional and still have something traverse-like:

// f = Array
(A -> [B])          -> Notifcation<A> -> [Notification<B>]

// f = Result
(A -> Result<E, B>) -> Notifcation<A> -> Result<E, Notification<B>>

// f = Future/Promise/Task
(A -> Future<B>)    -> Notifcation<A> -> Future<Notification<B>>

It's kinda hard to see from the fully general form of (a -> f b) -> t a -> f (t b), but the crux of what we are trying to express is how to flip around nested container types: t (f a) -> f (t a). With your types this boils down to transformations:

Notification<A?>        -> Notification<A>?
Notification<[A]>       -> [Notification<A>]
Notification<Future<A>> -> Future<Notification<A>>

It also just happens that this is a really great example of what "higher-kinded types" in Swift could give us. You could essentially implement one single interface and get an implementation for all of these intertwining functions (and so much more) for free. I wrote a bit about this in another thread:

19 Likes

Thanks @mbrandonw! I thought there was probably a name for this. I even knew about Traversable in the context of lists. I'm not sure why it didn't occur to me that it was generalized this way. It's great to see these concrete examples of how it generalizes!

Thank you, I really appreciate that you shared that much detail here. :) I still have one question though: Since in my case I also can map A to B while traversing the container types would you recommend to name the function traverseMap or just traverse?

The mapping is actually part of the traversal, so naming it traverseMap wouldnā€™t really be required. The non-mapping version tends to be called sequence (which is implemented by passing the identity function as the transform function).

1 Like

Yeah, what @Al_Skipp said.

The "wordy" description of traverse is that you are "traversing" into your type Notification, applying a transformation that maps into a well-defined structure (optional, array, result, future, etc.), and then collecting the results into that structure.

In the case you apply the identity transformation { $0 } you see that you are literally swapping the containers Notification<A?> -> Notification<A>?, and that operation is common enough that it is given a name, sequence.

@stephencelis and I have written a few instances of these functions for traversing some of our custom types with some of Swift's standard types. It really cleans up a lot of code. Also, if you want to go to the source of all these ideas there's a pretty approachable paper called "Applicative Programming with Effects" (written 10 years ago!).

2 Likes

Iā€™m just going to come out and say that ā€œtraverseā€ is a terrible name for this operation, and I strongly encourage you to pick a better name that more accurately describes what is happening.

1 Like

If you believe it's terrible perhaps you have something in mind that you think is better. What would you call it?

1 Like

I'm not going to bike-shed over whether "traverse" is a good name or not, but I think it's worth defining as such even if you have a more descriptive alias for your domain that you want to define alongside it.

The traverse function is a term of art that's been around for a long time and has been (and can be) used with a lot of different pairings of container types, including many types you yourself may define. Hiding the name means hiding the abstraction and shared knowledge. This both guards folks from being introduced to a useful, reusable abstraction, and delays folks familiar with the abstraction from knowing that a function is traverse in disguise. This holds true for functions like map and flatMap, too.

3 Likes

Thank you guys for keeping this discussion alive. Even Iā€˜m not familiar with this term in that context Iā€˜m totally fine adopting terms of art in my codebase. As mentioned before the only issue I had is that in my case Iā€˜m not only switching the container types around but also map from A to B which made me think that itā€˜s a combination of traversing containers and mapping the captured inner type, hence traverseMap.

It's precisely the "flip" + "transform" that is traverse. If you are only flipping, then it's sequence. If you wanted to "swifty" it up you could say notification.traverse(with:). It means you want to traverse over notification's values (there's only one, but that's not important), apply a fail-able transformation, and collect the results into an optional.

1 Like

How would you call the parameter of that function? transform or transformation or something completely different? (Naming is hard for non-native English speaker like me. Latter name sounds better to me.)

I'm not great at naming either, and since it's the internal parameter name I don't think it matters too much. transform is probably more grammatically correct than transformation.

This convention seems less than desirable to me. I like the way flatten and flatMap relate to each other. This is much better than having flatten and bind. Is there a compelling reason for the different names or is it just an artifact of FP history?

We shouldn't hesitate to improve on the traditional names when a more consistent approach is identified, especially when it illuminates the underlying concepts and makes the relationship of operators or structures more clear. It seems to me like there is an opportunity to do that here. I am really happy that flatMap caught on and seems to be preferred over bind in most languages today. I think it is more descriptive and is less intimidating / more approachable for most people.

7 Likes

I really love all the FP API in Swift such as map, flatMap and compactMap. However it took a while until I understood the flatMap operator since in most cases I really used compactMap and was confused that in RxSwift flatMap behaved differently on a sequence, but quickly realized that it just flattens the containers (including optional mapping).

The true essence of map and flatMap is not readily apparent in Swift. It can't even be expressed properly as it requires higher-kinded types. It's too bad - the whole flatMap / compactMap confusion could have been avoided if we modeled the underlying concepts clearly from the start.

2 Likes