Memory · Lesson 15.6

Optional pointer

Source
Tell an optional pointer (*T)? from a pointer to an optional *T?, and use each for its own job.

Pointers and optionals combine in two ways that look almost the same and mean different things. This lesson puts them side by side, gives each its own job, and clears up the difference between null and none.

Where the ? belongs

A ? belongs to the type just before it, as every type suffix does. So in *var int? it attaches to int, and parentheses are needed to put it on the pointer instead:

TypeReads asA value of it is
(*var int)?an optional pointereither an address, or none
*var int?*var (int?), a pointer to an optionalalways an address — of a slot that holds an int?

An optional pointer: maybe an address

Find answers "where is wanted?" with an address the caller can write through — or with none:

func Find(values: var int[..], wanted: int) -> (*var int)? {
    for i in 0..values.length {
        if values[i] == wanted {
            return @values[i];
        }
    }
    return none;
}

A function that returned a bare pointer would use null for "not found", and leave remembering the check to you. One that returns (*var int)? cannot be used until a match or ?? has dealt with none:

match Find(scores[..], 3) {
    found? => {
        *found = 30;
        PrintLine("found 3, changed it to {}", scores[1]);
    },
    none => {
        PrintLine("3 is not there");
    }
}
flowchart LR
    f["Find(scores, 3)"] --> q{"present?"}
    q -- "found? — an address" --> w["*found = 30<br/>writes into scores"]
    q -- "none" --> n["3 is not there"]

Inside the found? arm, found is a plain *var int, and the write lands in the array: scores[1] becomes 30. The second search, for 5, takes the none arm.

A pointer to an optional

The other type is just a pointer — one that reaches an optional living somewhere else, the same way a *var int reaches an int:

func KeepLargest(best: *var int?, value: int) {
    let current = *best ?? value;
    *best = value > current ? value : current;
}

*best is the caller's int?. While it is still none, ?? value makes the first value the largest so far. The caller passes the address of its own optional, which starts out empty:

var best: int? = none;
for score in scores {
    KeepLargest(@best, score);
}

null is not none

null is a pointer value — the address of nowhere. So an optional pointer given null is present: it holds a pointer, which happens to point nowhere.

let wrapped: (*var int)? = null;
PrintLine("null wrapped is present: {}", wrapped is *var int);
You writeThe optional isSafe to reach through?
let p: (*var int)? = none;absentthere is nothing to reach
let p: (*var int)? = null;presentno — it points nowhere
let p: (*var int)? = @score;presentyes

Use none to mean "no pointer". Mixing the two brings back exactly the unchecked null the optional was meant to remove.

The program

The whole lesson is one package in the Examples repository. Its comments explain every step.

Src/Main.rux
// Two types that look almost the same, and mean different things:
//
//     (*var int)?   an optional pointer: either an address, or `none`
//     *var int?     a pointer to an optional: always an address, of a slot that holds an `int?`
//
// A `?` belongs to the type just before it, as every suffix does, so `*var int?` reads as
// `*var (int?)`. Parentheses put the `?` on the pointer instead.
//
// An optional pointer is the honest way to say "maybe an address". A function that returns a
// bare pointer and uses `null` for "not found" leaves the check to memory; one that returns
// `(*var int)?` cannot be used until a `match` or `??` has dealt with `none`.
//
// A pointer to an optional is just a pointer, used to read or replace an optional that lives
// somewhere else, the same way a `*var int` reaches an `int`.
import Io::PrintLine;

// Where is `wanted`? The answer is an address the caller can write through, or `none`.
func Find(values: var int[..], wanted: int) -> (*var int)? {
    for i in 0..values.length {
        if values[i] == wanted {
            return @values[i];
        }
    }
    return none;
}

// Keeps the largest value seen so far in the caller's `int?`, which starts out empty.
func KeepLargest(best: *var int?, value: int) {
    let current = *best ?? value;
    *best = value > current ? value : current;
}

func Main() -> int {
    var scores: int[4] = [7, 3, 9, 3];

    // Absence has to be handled before the address can be used.
    match Find(scores[..], 3) {
        found? => {
            *found = 30;
            PrintLine("found 3, changed it to {}", scores[1]);
        },
        none => {
            PrintLine("3 is not there");
        }
    }
    match Find(scores[..], 5) {
        found? => {
            *found = 50;
        },
        none => {
            PrintLine("5 is not there");
        }
    }

    // Writing through a pointer to an optional.
    var best: int? = none;
    PrintLine("best before: {}", best ?? -1);
    for score in scores {
        KeepLargest(@best, score);
    }
    PrintLine("best after:  {}", best ?? -1);

    // `null` and `none` are different. `null` is a pointer value, so an optional pointer given
    // `null` is present: it holds a pointer, which happens to point nowhere. Use `none` to mean
    // "no pointer".
    let wrapped: (*var int)? = null;
    PrintLine("null wrapped is present: {}", wrapped is *var int);
    return 0;
}

Run it

cd Examples/Memory/OptionalPointer
rux run
found 3, changed it to 30
5 is not there
best before: -1
best after:  30
null wrapped is present: true

Common mistakes

Reaching through an optional pointer without unwrapping it.
*Find(scores[..], 3) = 30; fails with error: operator '*' requires a pointer operand, but found '(*var int)?'. Handle none first, with a match or with ??.
Leaving out the parentheses.
Declare Find as returning *var int? and it promises a pointer to an optional instead. return none; then fails with error: 'none' needs an expected optional type, but found '*var (int?)' — the message spells out how the compiler read the type.
Using null to mean "no pointer".
(*var int)? = null is present, so a match takes the found? arm and the code reaches through null. Write none.

Try it yourself

  1. Give the missing 5 a place to go: let slot = Find(scores[..], 5) ?? @spare; with var spare: int = 0;, then write *slot = 50 and print spare.
  2. Write KeepSmallest and check that it finds 3.
  3. Change Find so that it returns the last match rather than the first, and see which element becomes 30.
  4. Set wrapped to none instead of null. What does the last line print now?

Learn more