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.
Diese Seite gibt es noch nicht auf Deutsch, deshalb steht sie hier auf Englisch. Zur englischen Seite
?. 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.Kompiliert nichtfn 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.Kompiliert nichtconst 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.