Your post highlights two different but connected issues in my opinion:
- In my understanding of the
Result
example, this corresponds more toliftM
thanzip
. 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 HaskellliftM
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 )
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
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
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.
- With your second example I would prefer a free function myself, especially because the
CopyableWithModification
protocol doesn't have any requirements andwith
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 singleflatMap
in the global namespace instead of the current situation where everyflatMap
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