Adding `defaultValue` method to Optional

This is a quick pitch without a formal proposal yet. We can always flesh that out later depending on the feedback.

I'm wondering if the Swift community would like to add such a convenience method to Optional or discard the idea.

I personally do not have many strong motivation cases and therefore encourage you to share if your code base contains a similar extension.

My use-case is a continuous chain of transformation methods.

// before
let result = something
  .foo
  .bar
  .flatMap(...)
return result ?? "default"

// after
return something
  .foo
  .bar
  .flatMap(...)
  .defaultValue("default")

The implementation which would wrap around ?? operator.

extension Optional {
  public func defaultValue(
    _ value: @autoclosure () throws -> Wrapped
  ) rethrows -> Wrapped {
    try self ?? value()
  }
}

What do you folks think? Please share your feedback and if you already have something similar to this in your codebase.


In the future we might be able to use this with key-path method literals.

arrayWithOptionalStrings.map(\.defaultValue("default"))
4 Likes

I’m neutral. I sympathize with everything that makes left-to-right chains flow naturally though. But my coding style already have a lot of operators in the chains, so I don’t really mind inlining an ?? without a method wrapper.

return something
  .foo
  .bar
  .flatMap(...)
  ?? "default"
  |> Type.init
5 Likes

My sense is that this is something that needs to be discussed on a more holistic level.

There have been multiple pitches lately (and in the past—recall all the discussions about prefix !) with this motivation of chaining, addressing some specific operation or other.

I think it is unsustainable to make up new names duplicating each operator, initializer, or other functionality that can’t be spelled as a member function, one by one, thread by thread. The project needs to declare either that enabling this motivation is a design goal, and then we should provide a systematic way to address this goal, or it is not.

18 Likes

I think I'd still prefer to add optional at the end. Most of them are more-or-less transformations anyway, so I slightly benefit from short-circuiting. Thought I do see merit if part of the chaining has side-effect.

return something
  .foo
  .failable?
  .flatMap(...) 
  ?? "default"
4 Likes

I can't speak for others, but I don't think I would make much use of this. I find ?? to already be a very clear, concise way of handling default values, and I tend to like the fact that in Swift optionals are largely handled through operators, as compared to something like Rust where you have a lot of more verbose unwrapping functions.

In my opinion using operators for this gives a clear visual delineation that you are working with optionals which makes code easy to parse at a glance.

12 Likes

FWIW, I use butIfNone and butThrowIfNone extensions on Optional to handle default value and error throwing cases inspired by John Sundell's article here: https://www.swiftbysundell.com/articles/extending-optionals-in-swift/. I'm not sure if they are appropriate for the standard library, but they read in an English-like way to me.

extension Optional {
    func butIfNone(_ defaultValue: @autoclosure () -> Wrapped) -> Wrapped {
        self ?? defaultValue()
    }

    func butThrowIfNone(
        _ errorExpression: @autoclosure () -> Error
    ) throws -> Wrapped {
        guard let value = self else {
            throw errorExpression()
        }

        return value
    }
}

optionalString.butIfNone("message to show instead")
optionalString.butThrowIfNone(AppError.NoValueForThing)

I not convinced we need this in the standard library, but for anyone who wants it in their own code I would recommend the spelling “or”:

let x = currentGreeter.or(Greeter()).preferredGreeting(for: guest).or("Hello").reversed()

But the ?? version is fine too:

let x = ((currentGreeter ?? Greeter()).preferredGreeting(for: guest) ?? "Hello").reversed()

Realistically in my own code, I would break this kind of thing up into multiple lines:

let greeter = currentGreeter ?? Greeter()
let greeting = greeter.preferredGreeting(for: guest) ?? "Hello"
let x = greeting.reversed()
2 Likes

I made a forum post here a few months ago, which proposes a subscript on Optional instead of a method: Subscript on Optional with default value it didn’t get much traction then, but I’m glad this topic is being raised again.

1 Like

I've been struggling with this for as long as I used Swift :smiley: To me, "chainability" is an essential part or readable, maintainable code, but the "optional chaining" sugar that Swift offers doesn't work in all use cases, unfortunately. The problem is mostly related to chains where there's an Optional somewhere, but not in the last call.

Consider the following code:

struct Foo {
  var bar: Bar
}

struct Bar {
  var baz: Baz?
}

struct Baz {
  var value: String
}

let foo = Foo(bar: Bar(baz: Baz(value: "howdy")))
let base: Int? = foo.bar.baz?.value.count /// we would like to default `.value` to `"yello"`

Instead of defaulting base to "yello".count, we would like to default Baz.value to `"yello".

There are several options to tackle the problem, and none seems optimal to me. I usually define a method on Optional like the one proposed by @DevAndArtist, but I call it get(or:), that is:

extension Optional {
  func get(or defaultValue: @autoclosure () -> Wrapped) -> Wrapped {
    self ?? defaultValue()
  }
}

To consider all options, let's also define a |> "left function application" operator.

precedencegroup LeftFunctionApplicationPrecedence {
  associativity: left
  higherThan: AssignmentPrecedence
  lowerThan: TernaryPrecedence
}

infix operator |> : LeftFunctionApplicationPrecedence
func |> <A, B> (_ lhs: A, _ rhs: (A) -> B) -> B {
  rhs(lhs)
}

Now, I can think of these options:

let option1 = foo.bar.baz?.value.get(or: "yello").count /// doesn't work
let option2 = (foo.bar.baz?.value ?? "yello").count /// works but requires parenthese to be chained
let option3 = foo.bar.baz.get(or: Baz(value: "yello")).value.count /// works, but it's awkward
let option4 = foo.bar.baz.map { $0.value }.get(or: "yello").count /// works, but requires explicit `map`
let option5 = Optional(foo).flatMap { $0.bar.baz?.value }.get(or: "yello").count /// works, but it's verbose
let option6 = foo.bar.baz?.value ?? "yello" |> { $0.count } /// works, but requires extra operator and precedence rules

In my code I tend to use both option2, because it's not that bad and it's idiomatic, and option4, in which I simply renounce the syntactic sugar of optional chaining and use the actual methods on Optional (I'd suggest this option to those who want full chainability). option6 considers a different approach that's actually very good in terms of chainability, but requires more work up front and can be harder to understand to some (also, uses a custom operator, and personally I tend to prefer methods).

Wait why would option1 not work? It‘s the same as option2. Am I missing something *scratching head*?

Oh I see, optional chaining is swallowing everything on the right hand side. An alternative to this would kinda like option2, but it only works well in case of readability if it‘s written in a single line.

let option1_1 = (foo.bar.baz?.value).get(or: "yello").count

Typo in option4, there no .count.


In Swift 5.2 option4 would be more pleasant to read.

let option4 = foo.bar.baz.map(\.value).get(or: "yello").count

I corrected the typo, thanks :wink:

This has some parallels with protobuf v3, where all values are optional and are sent with a default when they're missing, rather than be made required.

I think get should be reserved to parallel Result.get and turn an Optional into a throwing statement. There are better names anyway.

1 Like