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(©)
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?