Optional chaining
`?.` is Option.map, or Option.flatMap when the member itself answers an Option, so chaining never nests. `??` is the trait OrElse, which gives a lazy fallback for an absent Option, a failed Result or any type that comes with it.
?. and ?? exist so that reading through several Options in a row does not need a match at every step. Neither
is a new kind of value: both are ordinary method calls. ?? is the one of the two that is a trait, because which
method it calls never depends on anything but the receiver.
Example#
type User {
name: String
manager: User? = None
}
fn findUser(id: Int): User? {
if id == 1 { User "Ada" } else { None }
}
const managerName: String? = findUser(2)?.manager?.name
const name = findUser(1)?.name ?? "anonymous"
print "{managerName} {name}"Syntax#
<option-expression>?.<member> Option.map, or Option.flatMap if <member> itself answers an Option
<expression> ?? <fallback> OrElse.orElse: the value, or <fallback> (evaluated only if it is needed)
trait OrElse<Value> { fn orElse(fallback: lazy Value): Value }Rules#
?.isOption.map.findUser(1)?.nameisfindUser(1).map { _.name }:NonestaysNone,Some(user)becomesSome(user.name).?.becomesOption.flatMapwhen the member itself answers anOption, so chaining never produces a nested one.findUser(2)?.manager?.nameis a plainString?, never aString??, whatevermanagerandnameeach answer.?.is not defined onResult. AResultneedsok()first, because mapping over itsOkside while discardingFailwould throw away the reason for the failure - whichOption.mapon anOptionnever had to begin with.Does not compilefn parse(text: String): Result<Int, String> { text.byteLength() } const length = parse("hi")?.isOk() print length // error: `?.` needs an `Option`, and `Result<Int64, String>` is not one??isOrElse.orElse, andOptionandResultboth come with it.option ?? fallbackandresult ?? fallbackboth answer theValue, never theOptionor theResultitself. It is the trait that decides, not the two types: a type of your own that comeswith OrElse<Value>gets??, and one that does not hears so.Does not compileconst value = 1 ?? 0 print value // error: `Int64` does not implement `OrElse`, so `a ?? b` has no meaning for itThe right side of
??islazy: it is evaluated only when the left side is absent or failed. A fallback that is expensive to compute, or that has a side effect, only runs when it is actually needed.fn parseOr(text: String, fallback: Int): Int { Int.tryFrom(text) ?? fallback } print parseOr("12", 0) print parseOr("nonsense", 0)The right side of
??is checked against theValue, not against the wholeOptionorResult.Int.tryFrom(text) ?? 0needs a plainInton the right, not aSome(0)or anOk(0). TheValueis the argument of the receiver'sOrElse<Value>.Where an
Optionis expected of the whole, the answer of??wraps into it like any other value, and so does a plain value that stands beside aNone-if id == 1 { User("Ada") } else { None }is aUser?without a writtenSome, because a value where anOptionof its type is expected becomesSome(value)(Conversions, rule 8). A?.chain answers anOptionalready, and anOptionnever wraps into another one.?.is a trait for nobody, and that is deliberate. An operator is a trait exactly when it is a method call, and?.is two:mapwhen the member answers a plain value,flatMapwhen it answers anOption. A trait that covered both would have to nameSelf<Output>- the higher-kinded form this language does not have - so?.stays defined onOptionalone.?is the same question with a different answer: it leaves the enclosing function, and no method can do that. Both can be opened later without breaking anything.
What this is not#
?. is not a null-conditional operator that stops the statement. In C# and TypeScript, a?.b?.c short-circuits
the entire expression to undefined/null the moment any link is absent, but the statement around it keeps running
with that value. Here ?. produces an ordinary Option value, nothing more:
type User {
name: String
manager: User? = None
}
fn managerNameOf(user: User): String? {
user.manager?.name
}
print managerNameOf(User("Ada"))type User {
name: String
manager: User? = None
}
fn managerNameOf(user: User): String {
user.manager?.name
}
print managerNameOf(User("Ada"))
// error: Expected `String`, found `Option<String>`The second version tries to hand a String? to a function that promises a plain String; a value wraps into an
Option where one is expected, but nothing ever unwraps one on its own - there is no implicit "or crash" the way ! in
Swift or Kotlin would provide. Write ?? "default", or change the result type to String?.
?? is not the same as || on a Bool, and it is not a truthiness check. The left side has to be an Option or
a Result; a Bool, an empty String or a zero Int is never "falsy" here, so ?? never silently activates for a
value that only looks empty.
Related#
- Result -
?, and why??is defined the same way there. - Option -
map,flatMapandorElsethemselves. - The question mark operator - the other way to leave early instead of falling back.
- Operators are traits - which operator is which trait, and the three that are none.