-
-
Save chockenberry/d6c08e9916442d11a0c69fb01454c063 to your computer and use it in GitHub Desktop.
import UIKit | |
enum Name { | |
case foo(Foo) | |
case bar(Bar) | |
struct Foo { | |
let name: String | |
} | |
struct Bar { | |
let count: Int | |
} | |
} | |
let names = [Name.foo(Name.Foo(name: "hello")), Name.bar(Name.Bar(count: 123)), Name.bar(Name.Bar(count: 999))] | |
// The goal: to get the first Name.Bar item from names, and more importantly, to make the code readable. | |
func firstBar_TakeOne(in names: [Name]) -> Name.Bar? { | |
if case let .bar(bar) = names.first(where: { name in | |
if case .bar(_) = name { | |
return true | |
} | |
return false | |
}) { | |
return bar | |
} | |
return nil | |
} | |
func firstBar_TakeTwo(in names: [Name]) -> Name.Bar? { | |
let initialResult: Name.Bar? = nil | |
let result = names.reduce(into: initialResult) { result, name in | |
guard result == nil else { return } | |
if case let .bar(bar) = name { | |
result = bar | |
} | |
} | |
return result | |
} | |
if let bar = firstBar_TakeOne(in: names) { | |
bar.count | |
} | |
if let bar = firstBar_TakeTwo(in: names) { | |
bar.count | |
} |
func firstBar(in names: [Name]) -> Name.Bar? {
for name in names {
if case let .bar(bar) = name {
return bar
}
}
return nil
}
This doesn't generalize, but I'd probably do something like this:
enum Name {
...
var asBar: Bar? {
switch self {
case .bar(let bar): return bar
default: return nil
}
}
...
}
func firstBar(in names:[Name]) -> Name.Bar? {
return names.first(where: { $0.asBar != nil })?.asBar
}
@bewebste That's a great way to abstract the complexity, you can write it this way, but unfortunately might be less efficient depending on how it compiles.
return names.compactMap(\.asBar).first
With Algorithms
, you could say names.firstNonNil { ... }
to get short-circuited evaluation cleanly, and with some more macro trickery from Point-Free's CasePathMacros
, the whole thing would reduce down to names.firstNonNil(\.bar)
.
It's a real shame most of what's in Algorithms
isn't part of the standard library already.
what would happen if you drop the enum/switch stuff and instead used polymorphism and a common superclass?
I have often found that the key to readable code is to employ tactics like (A) add computed properties to model types that make code more expressive and (B) write reusable extension methods on Swift Standard Library types that you could argue should exist already in well-known forms.
In the following:
- Add a
barValue
computed property to Name - Add an
extractFirst(body:)
utility method to Collection - Reimplement your call site to use both together for maximum readability.
extension Name {
/// This uses compile-time exhaustive switch guarantees that help future
/// maintainers (including you, next week) to find places that need to be
/// edited as new cases are added (like, any other case that has a Bar).
var barValue: Bar? {
switch self {
case .foo: nil
case .bar(let bar): bar
// case .somethingWithABar(_, let bar, _): bar
}
}
}
extension Collection {
/// This is widely reusable code now.
func extractFirst<T>(body: (Element) -> T?) -> T? {
// Using a for-in with an early return limits the number
// of times that `body()` gets invoked to the minimum,
// in case future consumers have expensive bodies:
for element in self {
if let match = body(element) {
return match
}
}
return nil
}
/// This is widely reusable code now, too.
func extractFirst<T>(_ keypath: KeyPath<Element, T?>) -> T? {
extractFirst { $0[keyPath: keypath] }
}
}
// Now you can do this in a single line of code, which in the end does not
// even justify having defined a `firstBar(in: names)` domain-specific
// function in the first place.
//
// The call site reads, aloud in English:
//
// "Names, extract first bar value." which is pretty close to self-documenting.
if let bar = names.extractFirst(\.barValue) {
bar.count
}
The extension Collection
methods are best defined in a reusable location, the kind of in-house package that ends up getting imported by every project because of the gaps it patches in the standard library.
I would personally go with:
if let bar = names.extractFirst({ // from answer above
guard case let .bar(barVal) = $0 else { // fold all other cases into:
return nil;
}
return barVal;
}) {
bar.count;
}
because I assume struct Name.Bar's role is nailed down enough; if it isn't, what Jared Sinclair said.
Building on the for-loop solution from @ezfe: you can make the variable binding in the loop condition:
func firstBar(in names: [Name]) -> Name.Bar? {
for case .bar(let bar) in names {
return bar
}
return nil
}
This will iterate over names
until it finds the first .bar(_)
value and return its associated value (bound to bar
in the loop condition). If names
doesn't contain any .bar(_)
values it will return nil.
I would tend to reach for something like this:
let firstBar = names
.lazy
.compactMap({
switch $0 {
case let .bar(bar): bar
default: nil
}
})
.first;
The combination of .lazy
and .first
should mean you only iterate as far through the collection as you need to, and the result is an Optional<Name.Bar>
.
If you use the asBar
or barValue
helper values others have contributed above, you could then write it as:
return names.lazy.compactMap(\.asBar).first
and still get nearly optimal performance. A regular for loop will probably be marginally faster for small collections though but not by much.
Watch out, the performance of that construct may surprise you: https://forums.swift.org/t/adding-firstas-to-sequence/36665/17
Ahhhh, overloads fun. 🙃 The first(where: { _ in true })
hack is terrible, too 😂 but if abstracted over and provided with a very good explanatory comment, I guess I could live with it. 🙃
Note that
firstBar_TakeOne()
only iterates over the collection until it finds a match. The code forfirstBar_TakeTwo()
is slightly more readable, but must iterate until it reaches the end of the collection.