Idiomatic Swift, zipping and retroactive modeling

Your post highlights two different but connected issues in my opinion:

  1. In my understanding of the Result example, this corresponds more to liftM than zip. I'll give an example of it in Haskell, and I'll explain in a moment why I use Haskell instead of Swift here. In Haskell liftM functions have these type signatures:
liftM2 :: Monad m => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM3 :: Monad m => (a1 -> a2 -> a3 -> r) -> m a1 -> m a2 -> m a3 -> m r
liftM4 :: Monad m => (a1 -> a2 -> a3 -> a4 -> r) -> m a1 -> m a2 -> m a3 -> m a4 -> m r 

(Read :: as Swift's : here and Monad m as Swift's <M: Monad> generic constraint)

The documentation string for it may sound a bit cryptic...

Promote a function to a monad, scanning the monadic arguments from left to right. For example,

...but its purpose is easy to see with a few examples:

liftM2 (+) [0,1] [0,2] = [0,2,1,3]
liftM2 (+) (Just 1) Nothing = Nothing

(Just is equivalent to Swift's some and Nothing is equivalent to nil)

This is a generic (in Haskell's lingo "polymorphic") function. Basically it does the same thing you want for Result if I understand you correctly.

(As a sidenote, Haskell maybe could benefit from what we call "variadic generics" to avoid the proliferation of functions for each number of arguments here :stuck_out_tongue_winking_eye:)

Note that liftM family of functions is generic over the type of a "container" (called Monad here), so the same liftM2 function definition will work for any "container" that defines a "conformance" to Monad, be it an array, a set, a result type or whatever. (I put "conformance" in quotes here because in Haskell it is called "a type class instance" where a "type class" is roughly what we call a "protocol" in Swift).

This feature is called "higher-kinded types" and is not available in Swift, unfortunately. I personally don't see a compelling answer for why it's not on the roadmap. Thus we're doomed to reinvent the wheel reimplement the same flatMap function (or the aforementioned liftM function for that matter) over and over again for Result, Publisher, collection types etc, even though all reimplementations of it work in the same way and we're basically copy-pasting it, substituting the container type for every case. The flatMap function in Swift is generic, but not generic enough :slightly_smiling_face:

One argument against higher-kinded types feature in Swift was that it's "too complex". Although one could question how multiple copies of the same algorithm are less complex than a single implementation we'd have with higher-kinded types in Swift. Let's imagine how it would look, I'll call it Liftable to make it sound more "approachable" than Monad :smile:

protocol Liftable {
 // a few requirements here I'll leave out, 
 // mostly would be a translation of Haskell's `Monad` type class
}

extension Liftable {
  func liftM2<T1, T2, R>(
    _ first: Self<T1>, 
    _ second: Self<T2>, 
    lifter: (T1, T2) -> R
  ) -> Self<R> {
    // generic implementation for any `Liftable` type
  }

extension Result: Liftable {}
extension Array: Liftable {}
extension Publisher: Liftable {}
// and so on...

What Swift doesn't allow here is the Self<T> construct, you can probably find some alternative syntax in numerous pitches of higher-kinded types here on Swift Forums.

  1. With your second example I would prefer a free function myself, especially because the CopyableWithModification protocol doesn't have any requirements and with function, in principle, can be applied to any type, no generic constraints needed. The downside of free functions is that they can pollute the global namespace, especially if you have many of them that doing the same thing but requiring different generic constraints. This is actually a corollary of the point "1" above, if Swift supported higher-kinded types from the start, wouldn't we be fine with a single flatMap in the global namespace instead of the current situation where every flatMap variation is namespaced as a member on its corresponding type? I don't know, maybe it's still a matter of preference and even if higher-kinded types are ever added to Swift it would probably be too late to change the convention.

Hope this answers your questions, but I'd be happy to clarify whatever sounds too convoluted in my post :slightly_smiling_face:

2 Likes