Combining ? with throwing when there's no reasonable default

If we want to say, "use this or if it's nil, that default", we can write:

let a = maybeA ?? A()

However, saying "use this or if it's nil, throw" is more verbose (if descriptive):

guard let a = maybeA else {
	throw SomeError()
}

One might imagine writing

let a = maybeA ?? throw SomeError()

but this doesn't work: throw X is a statement, not an expression, so it doesn't parse. As @Nevin notes, we can make the right-hand side an explicit closure evaluation:

let a = try maybeA() ?? { throw SomeError() }()

This reads a little arcane, and the try feels both redundant and oddly placed.

Further, @Chris_Lattner3 comments:

That said, making that change wouldn't fix your code above, because ?? requires the left and right side to be the same base types, and Never is not an A.

This is a little odd -- arguably, Never is a natural candidate for a bottom type; see here -- but prevents us from just definining throw X to be an expression of type Never.

What are other ways to express the above neatly?


For reference, this is valid Kotlin:

fun foo(a: String?) : Int {
    return a?.toIntOrNull() ?: throw RuntimeException()
}

Kotlin's Elvis operator is similar to ??, but (I think) throw X is an expression of type Nothing here, which is the bottom type in Kotlin. So this type-checks (and even drives data flow analysis).

2 Likes

I guess the type checker could give special treatment to throw X expressions (which can't be confused with any other expression due to the keyword, I think) and make it attain any type "on demand". An implicit bottom type, if you will.

Doesn't sound like the value obtained justifies that kind of cost (new special cases in grammar and type checker), though.

If this would go into stdlib I donβ€˜t think it would require any magic or compiler support other than an overload that you can define on your own already.

infix func ?? <T>(lhs: T?, rhs: @autoclosure () -> Error) throws -> T

// usage
let a = try optional ?? SomeError()
1 Like

This is possible through a simple extension:

infix operator ?!: NilCoalescingPrecedence

/// Unwrap or throw operator
public func ?! <T>(lhs: T?, rhs: @autoclosure () -> Error) throws -> T {
    if let val = lhs { return val }
    throw rhs()
}

Use it with:

let x = try optional ?! MyError()
3 Likes

You can also just do unwrapped ?? fatalError("This should never be nil"), however throw would be nicer.

2 Likes

If you change Error to Never then yes, otherwise we would need to wait until Never becomes the bottom type in Swift. That was proposed once by Erica Sudan but was rejected, I donβ€˜t remember the reasons though.

Edit: Ah now that Never conforms to Error, this is also possible with a single overload yes, good catch.

You can make a throwError function:

func throwError<T>(_ e: Error) throws -> T {
  throw(e)
}

And then use it:

let y = try x ?? throwError(SomeError())

β€’ β€’ β€’

You can also do something similar with fatalError:

func crash<πŸ’₯>() -> πŸ’₯ { fatalError() }
3 Likes

I did actually originally have throw as an bottom expression, but I got grief over it and didn't care to push the issue. It seems like a reasonable thing to do.

I don't think we'd want to make arbitrary uninhabited types act like bottom in the type system; that would almost certainly do nasty things to the type-checker.

6 Likes

I like this variant. The try seems to be in the wrong place, but there isn't a good one for it, anyway.

So basically, if throw was a function instead of a keyword, it would already work? That may be an angle, but would probably break too much.

(Using untypable but descriptive characters for type parameters, nice!)

Try is in the correct place actually since the expression really is not different from:

let a = try (optional ?? SomeError())

Yes and no. While correct, it doesn't make much visual sense: the idea is that the rhs throws, but since we faked it by making ?? throw, the try has to go in front.
In @Nevin's proposal, we could write (I think)

let y = x ?? (try throwError(SomeError()))

That is, the try would be where something can actually be thrown.

But then, a "native" implementation shouldn't require try at all, just like a throw X statement doesn't require one; it's already explicit that and where we can throw.

I don't understand what you're trying to solve. If you cannot unwrap the value it's ?? that has to throw and you will need to handle the error in the same scope.

let y = x ?? (try throwError(SomeError()))

Does not make sense since you cannot assign anything to y if you cannot unwrap the value and it will only throw inside ?? anyway.

That means you want one the two possible overloads:

func ?? <T>(lhs: T?, rhs: @autoclosure () -> Error) throws -> T
func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Void) rethrows -> T

In case of the second overload we would need to teach the compiler to interpret throw Error as it would return Void, but even then you still would require try.

func ?? <T>(lhs: T?, rhs: @autoclosure () throws -> Void) rethrows -> T {
  if let result = lhs { return result }
  try rhs()
  fatalError("Cannot be reached")
}

let optional = Int?.none
struct SomeError: Error {}

let y = try (optional ?? throw SomeError())

I think the friction (in my mind) comes from Swift having a rather colorful mix of built-in syntax and functions. Here, throw is a keyword (in a statement) but ?? is a function (that takes expressions) so they don't combine well. If they were both from the same category, natural solutions would exist.

1 Like

throw doesn't technically need to be builtin syntax β€” there does have to be a primitive, but that could be hidden within a standard function the same way that it is in, say, SmallTalk β€” but that's quite common across languages. The limitation here is really just that it's a statement and not an expression. I think we'd be happy to consider a proposal to generalize it to be an expression; I can't guarantee that it'd be accepted, but I for one expect I'd support it.

6 Likes

Allowing specifically Never to act like bottom seems tractable, though, and is a direction we suggested going forward in our resolution for rejecting the !! operator. That combined with making throw into an expression sounds good to me.

12 Likes

It's still quite problematic even if there's only one blessed bottom type. The issue is that it adds complexity to what you can do with unresolved type variables; normally if Ο„ < T and T is concretely known to have no subtypes then you can deduce Ο„ = T, but that's not true if you have a bottom type.

5 Likes

Is there a way to treat it similarly to optional-promotion, whereby Never can be statically promoted to T as required?

It's not obvious to me that it makes things any worse than they are now, since for an unresolved type variable there already isn't any guarantee that there are no subtypes, because of subclassing, existentials, T < T?, and so on.

T was meant to be a concrete type head there. There are many concrete types that have no subtypes and for which we could otherwise immediately reach that conclusion.