Skip to content
FutureBlitz

    Install

    Null safety and memory, 1 of 5

    Nullable types

    Say where a value can be missing with T?, and let the compiler check every use of it.

    What you’ll build

    An organization chart where each person may have a manager, and a lookup of nicknames that may not be set. Both have values that can be missing, and the program handles each case without ever reading a field of nothing.

    T? holds Null, and T doesn’t

    A type ending in ? can hold Null, and the same type without the ? can’t. A String always holds a String; a String? holds a String or Null. A field of a T? type starts as Null. Local name: String = Null is an error, so a value that can’t be missing never is. The one exception is the raw pointers used with C libraries, which can be Null like C’s.

    a?.b reads b when a holds a value, and is Null when a is Null; a?.M() calls M only when there’s a value. a ?? b gives a’s value, or b when a is Null. Together they read a chain that may stop anywhere, with a fallback at the end.

    chart.fb
    Import std.core
    
    Class Person
        ReadOnly Field Name: String
        Field Manager: Person?
    
        New(name: String, manager: Person? = Null)
            Name = name
            Manager = manager
        End New
    End Class
    
    Local grace = Person("Grace")
    Local ada = Person("Ada", grace)
    Print(ada.Manager?.Name ?? "nobody")
    Print(grace.Manager?.Name ?? "nobody")
    Print(ada.Manager?.Manager?.Name ?? "no one above")
    Output
    Grace
    nobody
    no one above

    A T? can’t go in a string as it is, since Null has no text: "{ada.Manager?.Name}" is an error that says to give a value for Null with ??.

    Checking narrows the type

    After If x != Null, the compiler knows x holds a value, and treats it as a T until the code could change it. An early Return, &&, and || narrow the same way.

    chart.fb
    Import std.core
    Import std.text
    
    Class Person
        ReadOnly Field Name: String
        Field Manager: Person?
    
        New(name: String, manager: Person? = Null)
            Name = name
            Manager = manager
        End New
    End Class
    
    Function ManagerOf(person: Person): String
        Local manager = person.Manager
        If manager == Null Then Return "{person.Name} has no manager"
        Return "{person.Name} reports to {manager.Name}, {manager.Name.Length} letters"
    End Function
    
    Local grace = Person("Grace")
    Print(ManagerOf(Person("Ada", grace)))
    Print(ManagerOf(grace))
    Output
    Ada reports to Grace, 5 letters
    Grace has no manager

    A field isn’t narrowed, since any call could change it, so checking the field itself doesn’t help, and fb says what to do instead:

    chart.fb
    Import std.core
    
    Class Person
        ReadOnly Field Name: String
        Field Manager: Person?
    
        New(name: String)
            Name = name
        End New
    End Class
    
    Function ManagerOf(person: Person): String
        If person.Manager == Null Then Return "{person.Name} has no manager"
        Return "{person.Name} reports to {person.Manager.Name}"
    End Function
    
    Print(ManagerOf(Person("Grace")))
    $ fb run chart.fb
       Compiling chart.fb
    chart.fb:14:39: error: 'person.Manager' may be Null; check a Local copy of it, or use '?.'
       |
    14 |     Return "{person.Name} reports to {person.Manager.Name}"
       |                                       ^^^^^^^^^^^^^^
    error: couldn't build chart.fb because of 1 error

    Copying the field into a Local, as the ManagerOf before this one does, is the fix.

    Expect and OrElse

    Every T? has two Methods. Expect(message) gives the value, and panics with the message when there’s none, for a value the program knows must be there. OrElse(make) gives the value, or calls make for one, only when it’s needed.

    nicknames.fb
    Import std.core
    Import std.collections
    
    Local nicknames = {"Ada": "Countess", "Alan": "Prof"}
    
    Local ada = nicknames.Get("Ada")
    Print(ada.Expect("Ada has a nickname"))
    
    Local grace = nicknames.Get("Grace")
    Print(grace.OrElse(Function() => "no nickname"))
    Output
    Countess
    no nickname

    Expect on a Null stops the program with its message and the line of the call. Use it only where a missing value would be a bug:

    nicknames.fb
    Import std.core
    Import std.collections
    
    Local nicknames = {"Ada": "Countess", "Alan": "Prof"}
    Print(nicknames.Get("Grace").Expect("every user has a nickname"))
    Output
    panic: every user has a nickname at nicknames.fb:5:7
        at top-level code (nicknames.fb:5)

    What you learned

    • A type ending in ? can hold Null, the same type without it can’t, and a field of a T? starts as Null.
    • ?. reads through a value that may be Null, and ?? gives a fallback.
    • A check like If x != Null narrows x to its type without the ?.
    • A field isn’t narrowed; copy one into a Local and check that.
    • Expect gives the value or panics, and OrElse makes a fallback only when it’s needed.

    Report an issue with this page or with FutureBlitz on GitHub.