Skip to content

Instantly share code, notes, and snippets.

@chockenberry
Created July 16, 2024 23:20
Show Gist options
  • Save chockenberry/d6c08e9916442d11a0c69fb01454c063 to your computer and use it in GitHub Desktop.
Save chockenberry/d6c08e9916442d11a0c69fb01454c063 to your computer and use it in GitHub Desktop.
Ugly Swift
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
}
@chockenberry
Copy link
Author

Note that firstBar_TakeOne() only iterates over the collection until it finds a match. The code for firstBar_TakeTwo() is slightly more readable, but must iterate until it reaches the end of the collection.

@ezfe
Copy link

ezfe commented Jul 16, 2024

func firstBar(in names: [Name]) -> Name.Bar? {
    for name in names {
        if case let .bar(bar) = name {
            return bar
        }
    }
    return nil
}

@bewebste
Copy link

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
}

@ezfe
Copy link

ezfe commented Jul 16, 2024

@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

@pyrtsa
Copy link

pyrtsa commented Jul 17, 2024

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.

@karstenBriksoft
Copy link

what would happen if you drop the enum/switch stuff and instead used polymorphism and a common superclass?

@jaredsinclair
Copy link

jaredsinclair commented Jul 17, 2024

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:

  1. Add a barValue computed property to Name
  2. Add an extractFirst(body:) utility method to Collection
  3. 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.

@ZPedro
Copy link

ZPedro commented Jul 17, 2024

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.

@d-ronnqvist
Copy link

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.

@chriskrycho
Copy link

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>.

@ezfe
Copy link

ezfe commented Jul 17, 2024

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.

@pyrtsa
Copy link

pyrtsa commented Jul 17, 2024

Watch out, the performance of that construct may surprise you: https://forums.swift.org/t/adding-firstas-to-sequence/36665/17

@chriskrycho
Copy link

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. 🙃

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment