Packages

option

Represent a value that may or may not be there with some and none, and transform it with map, andThen and okOr.

import "std:option"

An option is either some(value) or none(). It answers "is there a value?" in the type itself, and gives you methods to work with the answer.

Most of the time you don't need it. OSL already has nullable types: string? is a string or null, and ?? supplies a default. Use an option when null is itself a legitimate value and "absent" has to be told apart from "present, and null". The classic case is a lookup that can return a stored null.

some and none are built in, and std:option provides them.

some and none#

main.osl
import "std:option"

auto present = some(5)
auto absent = none()

log present.isSome() present.isNone()
log absent.isSome() absent.isNone()
log present.unwrap()
Click the code to edit it

Getting the value out#

unwrap() gives the value and stops the program when the option is none. unwrapOr(fallback) is the safe form. expect(message) is unwrap with your message, to say what you were relying on:

main.osl
import "std:option"

auto absent = none()
log absent.unwrapOr("fallback")
log try(absent.unwrap()).unwrapErr().toStr()
log try(absent.expect("no user selected")).unwrapErr().message
Click the code to edit it

Returning an option#

A search function should say "not found" in its type. Return option<int>, and callers have to handle both cases:

main.osl
import "std:option"

def indexOf(string[] items, string target) option<int> (
  for i, item in items (
    if item == target ( return some(i) )
  )
  return none()
)

string[] names = ["ada", "bo", "cy"]
log indexOf(names, "bo").unwrap()
log indexOf(names, "zed").isNone()
log indexOf(names, "zed").unwrapOr(0)
Click the code to edit it

Transforming with map#

map(fn) changes the value inside a some and leaves a none alone, so you can write the happy path once:

main.osl
import "std:option"

auto price = some(20)
auto missing = none()

log price.map(p -> p * 2).unwrap()
log missing.map(p -> p * 2).isNone()
log price.map(p -> "$" ++ p).unwrap()
Click the code to edit it

Chaining with andThen#

When the function itself returns an option, use andThen so you don't end up with an option inside an option. A failure at any step turns the rest into no-ops:

main.osl
import "std:option"

object users = {ada: {manager: "bo"}, bo: {manager: "cy"}, cy: {}}

def managerOf(string name) option<string> (
  object user = users[name] ?? {}
  string? boss = user.manager
  if boss == null ( return none() )
  return some(boss)
)

log managerOf("ada").unwrap()
log managerOf("ada").andThen(b -> managerOf(b)).unwrap()
log managerOf("ada").andThen(b -> managerOf(b)).andThen(b -> managerOf(b)).isNone()
Click the code to edit it

Providing alternatives#

orElse(fn) runs on none and returns another option, to try a second source. unwrapOrElse(fn) returns a plain fallback, computed only when needed:

main.osl
import "std:option"

auto cached = none()
auto fresh = cached.orElse(def() -> some("from the network"))
log fresh.unwrap()

log cached.unwrapOrElse(def() -> "computed fallback")
Click the code to edit it

Turning an option into a result#

okOr(error) converts: a some becomes an ok, and a none becomes an err with the error you give. This is the bridge to result when absence is a failure the caller must handle:

main.osl
import "std:option"
import "std:result"

def findUser(string name) result<string, error> (
  option<string> found = none()
  if name == "ada" ( found = some("Ada Lovelace") )
  return found.okOr(error("NotFoundError", "no user " ++ name))
)

log findUser("ada").unwrap()
log findUser("zed").unwrapErr().toStr()
Click the code to edit it

Present but null#

This is the reason to choose an option over a nullable. some(null) is a value that exists and happens to be null, which is different from none:

main.osl
import "std:option"

auto stored = some(null)
auto never = none()

log stored.isSome()
log never.isSome()
log stored.unwrap() == null
log never.unwrapOr("default")
log stored.unwrapOr("default") == null
Click the code to edit it

With a plain string? the first and last lines can't be told apart, because null would both mean "nothing" and "stored null".

Options and JSON#

When you turn an option into JSON, some(x) becomes x and none() becomes null:

main.osl
import "std:option"
import "std:json"

log json.stringify([some("a"), none(), some(5)])
Click the code to edit it

When to use a nullable instead#

For everyday "maybe a value" with no need for the some-versus-null distinction, a nullable type is shorter:

main.osl
string? nickname = null
log nickname ?? "no nickname"
Click the code to edit it

Choose T? by default. Choose option<T> when null can be real data, or you want the map and andThen methods.

Quick check

A settings lookup can store null on purpose, meaning "the user cleared this". Which return type lets the caller tell "cleared" from "never set"?

Your turn

First even number

Finish firstEven so it returns some of the first even number in the array, or none() when there isn't one.

main.osl
Ctrl Enter runs
Expected output
8
-1

API reference#

0 members, generated from the std:option source of OSL 9.3.0.