Idiomatic Swift, zipping and retroactive modeling

I use a lot of Swift at work, and my approach with software design when working in a team is to stick to the language and write code that's as idiomatic as possible. I know that it's not everyone's approach: some for example prefer to "marry" a library that makes them write code in a certain way, but I found that, in the long run, being idiomatic (with some convenience extensions, maybe) is the best bet. Even if I end up adding convenience extension (because of the missing standard library features), I try to follow language conventions as much as possible.

Thus, I try to avoid using repeatedly patterns that are not considered in the standard library. What follows is a couple of examples related to doubts I have about idiomatic Swift, for some common use cases: I'm writing from the position of one that doesn't want to use third-party dependencies for doing these things, and would prefer not to share some internal library or copypaste code everywhere for these simple operations. I'm also trying to be as humble as possible: my intent is not to push specific patterns on people, but to understand if things that I consider common and bother me are niche, thus not worthy of idiomatic concerns, or if there's really some missing guidance here.


For example, a very, very common thing I do is to represent failable states with the Result type, using a domain-specific error type, and produce instances by combining many Result values into a single one: this operation is traditionally called zip, and the standard library has a zip, but only for sequences.

Suppose I have something like this:

extension String: Error {}

enum DomainSpecificError: Error {
  case someError
}

typealias MyResult<A> = Result<A, DomainSpecificError>

struct Location {
  var x: Int
  var y: Int
  var a: Int
}

let r1 = MyResult<Int>.success(42)
let r2 = MyResult<Int>.failure(.someError)
let r3 = MyResult<Int>.success(12)

I want to produce an instance of Location from the 3 results, so it's going to be a Result<Location, DomainSpecificError>: how can I do it? Result.zip would be perfect, but it's not available, so either I make one and copypaste it everywhere, or I try and use some language features. I can come up with 3 alternatives, none great:

/// Concise, but information is lost, and it's retrieved unsafely.
let lr1 = Result {
  try Location(
    x: r1.get(),
    y: r2.get(),
    a: r3.get()
  )
}.mapError { $0 as! DomainSpecificError }

/// Very ugly, confusing and hard to scale if a fourth property is added.
let lr2 = r1
  .flatMap { x in
    r2.map { y in (x, y) }
  }
  .flatMap { (c: (x: Int, y: Int)) in
    r3.map { a in
      Location(x: c.x, y: c.y, a: a)
    }
  }

/// In my opinion the best, most flexible of the bunch, but verbose, heavy on syntax, and requires separate declaration and assignment.
let lr3: MyResult<Location>
switch (r1, r2, r3) {
case let (.success(x), .success(y), .success(a)):
  lr3 = .success(Location(x: x, y: y, a: a))

case let (.failure(e), _, _),
     let (_, .failure(e), _),
     let (_, _, .failure(e)):
  lr3 = .failure(e)
}

What do you think, which is more representative of "idiomatic Swift"?


This thread discusses a way to implement another very, very common thing, that is, producing an identical copy of something, save for some specific properties, by writing only the code needed for changing those properties. @Jens shows some alternatives, along with other posters, and I'm reporting 2 that are polar opposites:

/// free function
func with<T>(_ t: T, _ body: (inout T) throws -> ()) rethrows -> T { 
  var t = t
  try body(&t)
  return t
}

/// empty protocol, with conformance to be declared explicitly
protocol CopyableWithModification {}
extension CopyableWithModification {
    func with(_ modify: (inout Self) throws -> Void) rethrows -> Self {
        var copy = self
        try modify(&copy)
        return copy
    }
}

I tend to use the first option (again, copypasted code from project to project), and the second option seemed to me a little annoying because it requires to add explicit conformance to types that I want to be able to modify that way.

But then I remembered that, in Swift, protocol conformance works exactly like that: I'm always forced to make a type conform to a protocol to add the latter's functionality, even if obvious (e.g. Equatable for obviously equatable types).

The idea of explicitly adding protocol conformance instead of always automatically synthesizing code is rooted in the very nature of Swift, and plays well with the idea of "retroactive modeling" as described in the very first Swift talk at WWDC.

Thus, the more idiomatic solution seems to be the second. But the first is so much smaller, simpler, already available without the "conformance dance" (that we already do for Equatable and things like that) and more composable.

Is creating empty protocols with implemented methods (instead of free generic functions) a very common thing to do in Swift? I might have never truly considered this idea as viable because of the added burden of declaring explicit conformance: did I skipped the lesson where Swift encouraged this?

5 Likes

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

Thanks for the answer, but it goes waaaaay beyond what I'm interested here :smiley:

If you notice, I started the thread you linked, the HKT megadiscussion, and I'm familiar with liftM, but I'd be much happier with a zip like the following:

func zip<SuccessA, SuccessB, Failure>(_ ra: Result<SuccessA, Failure>, _ rb: Result<SuccessB, Failure>) -> Result<(SuccessA, SuccessB), Failure> {
  switch (ra, rb) {
  case let (.success(a), .success(b)):
    return .success((a, b))

  case let (.failure(f), _),
       let (_, .failure(f)):
    return .failure(f)
  }
}

This is a more fundamental operation than liftM, and after calling it, I can always map into my domain-specific type.

My concern is: given that I'd rather not write things like these and carry them around from project to project, and I don't want to import a huge library that diverges from idiomatic Swift, what's the more "Swifty" alternative?

About the second example, I'm really not concerned about "polluting" the global namespace: I consider it a non-problem. A more interesting problem, to me, is the related to discoverability and progressive disclosure: I think Swift tries to encourage the "discover by autocompletion" idea, where I write a . somewhere, and the options (and only the valid options, but AFAIK there's still lots of bugs in this space) appear in autocompletion, and can guide the implementation. Global functions cannot be discovered this way, so I'd prefer to avoid them, if possible, but they are the best in terms of composability, so again I feel stuck between two schools of thought and I don't see much guidance coming from the language itself.

1 Like

Hahaha, I'm sorry, I really didn't notice that because the user picture and the account profile were different :smile:

I don't see why defining a function like this is not "Swifty". Usually, in my consulting practice I just add a function like this to the codebase of a main project directly and forget it, I usually avoid using some of the big "functional progamming" libraries that introduce a lot of this stuff, given that a lot of developers aren't familiar with it enough to write most of the code in a "truly functional" way. Now with introduction of Combine we could expect that more people become acquainted with these patterns.

Speaking of Combine and HKT, notice that Combine's zip on Publisher does exactly the same thing and yet you still have to reimplement zip for Result. Or you could write result1.publisher.zip(result2.publisher) which is probably much uglier. So it's a good question why don't we have zip in the Swift standard library. My answer to that would be "because we don't have HKT and it's too much hassle dragging every variation of these simple functions for every type through the Swift Evolution process" :grin:

I don't have a good answer to your second point, although you mentioning auto-complete makes me think it's more of a convention driven by how our developer tools work, which is probably not a good thing given how buggy Swift developer tools are (and I admire the work of people working on dev tools, I just think we probably don't have enough people working on them to solve all the bugs we currently have). In terms of composability it probably depends more on the use case and conventions within your project. I agree the language and most popular libraries are inconsistent with this, notice how zip in the standard library is a free function, but zip in Combine is a member on Publisher.

1 Like

My humble opinion here is that boilerplate that hides intent and hurts readability is not very "Swifty", and that i would probably write my zip function:

// Clarity at the call site above all
let locationResult = zip(xResult, yResult, aResult).map(Location.init)

Even if zip should be called liftM here doesn't much matter: as long as the intent is clear at the call site, you're good to go:

// Not elegant, but perfectly clear
let locationResult = turn(xResult, yResult, aResult, into: Location.init)

This is also 100% my opinion.

What is interesting are those "missing standard library features" that leave holes that we have to fill.

It's often hard to spot them because we work around their absence every day. It's like breath. This practice easily becomes unconscious. To the point that quoting a few of those omissions is difficult, when we know by experience that there are dozens.

Some of us find those omissions cruel, because we consider them a lack of consistency. Some of us spot the omission but just look for the workaround. Some of us don't spot the omission. Blessed are the poor in spirit.

So one day, fully conscious, we notice that one of those omissions really hurts our code base. How to zip Results properly, damn it!

But now starts the doubt: many of us know pretty well that our job is not "stdlib designer". You need a team full of knowledge, experience, and guts, in order to engrave forever a simple function in the ABI. Our own APIs are often fragile, ill-advised, badly designed, short-sighted, badly named, quadratic, unfit, buggy... not worth entering the stdlib.

On top of that, finding an omission in the stdlib does not mean that we can help fixing it (come up with a proper fix, and then endure the fire of the pitch and review phases).

I thus see no way to do without those little files containing humble convenience functions.

As long as they are clear at the call site, that they improve over time, and that they create discussions, that's really not so bad.

Same experience here. The extra protocol is annoying. But it improves the code base. I don't know how to fix the language in order to get rid of it.

3 Likes

I can’t imagine why you say this. The Result example here is equivalent to zipWith in Haskell. It only requires the power of Applicative, not Monad. (It is also equivalent to zip followed by map.)

I think you misunderstand what HKT would provide. They would not allow us to avoid writing flatMap. What they would do is allow us to write generic code that abstracts over types that provide flatMap. If you look at monad types in Haskell you’ll notice that they all implement >>=. This is the very basis of the monad abstraction.

3 Likes

You're right, I apologise for the mistake, my knowledge of Haskell is quite superficial at this point, I haven't written much code in it for almost a decade. I was going to add the >>= requirement to the Liftable example protocol in my post, but then thought it would be make it harder to read, and I also forgot that >>= actually is flatMap.

Nevertheless, my point was that HKT would allow us to express both Applicative and Monad protocols, which then simplifies a potential implementation for both liftM and zipWith. I hope we're on the same page with regard to that :slightly_smiling_face:

For the first question about the Result: I don't know which of these would be broadly considered idiomatic but I find the logic and control flow in the switch-style easy to quickly understand (in fact, I didn't notice it earlier and was about to write that as a suggestion) compared to the other alternatives, even though it is more verbose in terms of pure character count. The try approach has an as! and I usually avoid as! if I can.

My Swift way also goes with the switch variant, but I am ancient.

1 Like

See my pitch for Compositional Initialization:

This addresses the exact thing you're asking about.

As well, it would offer a novel, albeit non-idiomatic, approach to your first question:

extension Location: PropertyInitializable {
    static var _blank: Location {
        return Self(x: 0, y: 0, a: 0)
    }
}

func process(_ results: MyResult<Int> ...) -> MyResult<Location> {
    var properties = [PartialProperty<Location>]()
    for (i, r) in results.enumerated() {
        switch (i, r) {
        case let (_, .failure(err)):
            return .failure(err) 
        case let (i, .success(n)):
            var path: WritableKeyPath<Location, Int>
            switch i {
            case 0: path = \.x
            case 1: path = \.y
            case 2: path = \.a
            default: fatalError()
            }
            properties += try! [path <- n]
        }
    }
    guard let loc = Location(properties) else {
        return .failure(.someError)
    }
    return .success(loc)
}


let result = process(r1, r2, r3)

In our team we couldn't decide which variant is better, liftM or zip. Everyone has own opinion :slight_smile:
For now, we use function with longer name, but it is clear for everyone:

public func combinedSuccess<A, B, C, D, Error>(of a: Result<A, Error>,
                                               _ b: Result<B, Error>,
                                               _ c: Result<C, Error>,
                                               _ d: Result<D, Error>)
  -> Result<(A, B, C, D), Error> where Error: Swift.Error {
  switch a {
  case .success(let aValue):
    switch b {
    case .success(let bValue):
      switch c {
      case .success(let cValue):
        switch d {
        case .success(let dValue): return .success((aValue, bValue, cValue, dValue))
        case .failure(let error): return .failure(error)
        }
      case .failure(let error): return .failure(error)
      }
    case .failure(let error): return .failure(error)
    }
  case .failure(let error): return .failure(error)
  }
}

There are functions with 3 and 2 arguments respectively.

In our project we use them to combine Results of NetworkResponses, like this:

let combinedResult: Result<(Profile, CartContents), NetworkError> = combinedSuccess(of: profile, cartContents)

It is very common operation in many screens.

Another use case is combining validation Results:

      let result = combinedSuccess(of: model.ctnValidationResult, model.amountValidationResult)

      switch result {
      case let .success(ctn, amount):
        router.routeToApplePay(email: email, phone: phone, amount: amount)
      case .failure(let error):
        showValidationError(error)
      }

As @gwendal.roue said, it is quite hard to make well designed API that will cover wide range of needs.
Though, having such help functions in Standard Library will make code more clear.

Can't help but to chime this in:

switch (a, b, c, d) {
case let (.success(a), .success(b), .success(c), .success(d)):
  return .success((a, b, c, d))
case let (_, _, _, .failure(error)): return .failure(error)
case let (_, _, .failure(error), _): return .failure(error)
case let (_, .failure(error), _, _): return .failure(error)
case let (.failure(error), _, _, _): return .failure(error)
}
2 Likes

Oh, thanks. Compiler became smart enough to understand that such switch is exhaustive.

Pretty sure that at this point, enum, tuples, and any combination thereof is a now supported.

Better yet:

Same difference
switch (a, b, c, d) {
case let (.success(a), .success(b), .success(c), .success(d)):
  return .success((a, b, c, d))
case let (_, _, _, .failure(error)),
     let (_, _, .failure(error), _),
     let (_, .failure(error), _, _),
     let (.failure(error), _, _, _): 
  return .failure(error)
}
5 Likes
Terms of Service

Privacy Policy

Cookie Policy