kex docs Standard Library 0.4.0-alpha kex.run ↗

Optionalprelude

type Optional<X>

An optional value: Just(x) carries a value, None says there is none.

X? is shorthand for Optional<X>, and is the spelling you will normally write. Kex has no null: anything that might not produce a value returns an Optional instead, so the compiler makes you say what happens when it is empty. Most of the time that is a single .or(default) at the end of a chain.

let names = ["ada", "grace"]
names.first.or("nobody")        # => "ada"
names.at(9).or("nobody")        # => "nobody"
names.first.map(~upperCase)        # => Just("ADA")

Pattern matching handles the cases that need more than a default:

match config.get("port") do
  Just(port) => IO.printLine("listening on ${port}")
  None       => IO.printLine("no port configured")
end

Variants

  • Just(X)
  • None

type Result<X, E>

The outcome of an operation that can fail: Ok(x) on success, Error(e) on failure with a reason.

Use Result over Optional when the *reason* for failure matters to the caller. Parsing is the standard example: "12x".to(Integer) answers None, while Integer.parse("12x") answers an Error that says where it stopped.

Integer.parse("42").or(0)     # => 42
Integer.parse("4x").or(0)     # => 0

match Integer.parse(input) do
  Ok(n)    => IO.printLine("got ${n}")
  Error(e) => IO.printError("bad number: ${e}")
end

Variants

  • Ok(X)
  • Error(E)

type Either<L, R>

One of two values, of possibly different types: Left(l) or Right(r).

Unlike Result, neither side means failure: Either is for a value that is legitimately one of two shapes.

type Id = Either<Integer, String>

let describe(id: Id) -> String do
  match id do
    Left(n)  => "numeric id ${n}"
    Right(s) => "slug id ${s}"
  end
end

Variants

  • Left(L)
  • Right(R)

trait Optionable

Marker trait for Optional. Constrain a generic parameter with it when a function accepts any optional value.

trait Resultable

Marker trait for Result. Constrain a generic parameter with it when a function accepts any result value.

trait Eitherable

Marker trait for Either. Constrain a generic parameter with it when a function accepts any either value.

make Optional<X> implements Optionable

set?

Returns true when a value is present.

set? : Bool

Returns: Bool

Examples
Just(42).set?   # => true
None.set?       # => false

Guarding on presence

let cached = lookup(key)
if cached.set?
  IO.printLine("hit")
end

none?

Returns true when there is no value. The opposite of set?.

none? : Bool

Returns: Bool

Examples
None.none?       # => true
Just(42).none?   # => false

Reporting a missing entry

if config.get("host").none?
  IO.printError("host is required")
end

or

Returns the wrapped value, or default when there is none.

This is the usual way an optional leaves the optional world: put .or at the end of a chain and the rest of your code works with a plain value.

or : X -> X
Parameters
default X
the value to use when None

Returns: X — the wrapped value, or default

Examples
Just(42).or(0)   # => 42
None.or(0)       # => 0

Closing a chain of lookups

["a", "b"].first.or("?")            # => "a"
[].first.or("?")                    # => "?"
"hello".indexOf('z').or(-1)         # => -1

map

Applies f to the wrapped value, keeping the result wrapped. None is returned unchanged, so f never sees a missing value.

map : (X -> Y) -> Y?
Parameters
f X -> Y
applied to the value when present

Returns: Y?Just(f(x)), or None

Examples
Just(2).map { |x| x * 3 }   # => Just(6)
None.map { |x| x * 3 }      # => None

Transforming before supplying a default

["ada"].first.map(~upperCase).or("ANON")   # => "ADA"
[].first.map(~upperCase).or("ANON")        # => "ANON"

flatMap

Applies f, which itself returns an optional, and flattens the result.

Use it instead of map when the step can also fail: map would give you a doubly wrapped Just(Just(x)), flatMap gives a single layer. A None anywhere in the chain short-circuits the rest.

flatMap : (X -> Y?) -> Y?
Parameters
f X -> Y?
the next optional-returning step

Returns: Y? — the result of f, or None

Examples
Just(4).flatMap { |x| x > 0 then Just(x * 2) else None }   # => Just(8)
Just(-4).flatMap { |x| x > 0 then Just(x * 2) else None }  # => None
None.flatMap { |x| Just(x * 2) }                           # => None

Chaining lookups that may each come up empty

users.get(id)
  .flatMap { |user| user.address }
  .flatMap { |address| address.postcode }
  .or("unknown")

make Result<X, E> implements Resultable

ok?

Returns true when the result is Ok.

ok? : Bool

Returns: Bool

Examples
Ok(42).ok?         # => true
Error("!").ok?     # => false

Counting successes

inputs.map(~Integer.parse).count(~ok?)

error?

Returns true when the result is Error. The opposite of ok?.

error? : Bool

Returns: Bool

Examples
Error("oops").error?   # => true
Ok(42).error?          # => false

or

Returns the Ok value, or default when the result is an Error.

The error payload is discarded. Match on the result instead when you need to report why it failed.

or : X -> X
Parameters
default X
the value to use on failure

Returns: X — the Ok value, or default

Examples
Ok(42).or(0)       # => 42
Error("!").or(0)   # => 0

Parsing with a fallback

Integer.parse(input).or(8080)

map

Applies f to the Ok value. An Error passes through untouched, so a chain of map calls describes the success path only.

map : (X -> Y) -> Result<Y, E>
Parameters
f X -> Y
applied to the value on success

Returns: Result<Y, E>Ok(f(x)), or the original Error

Examples
Ok(2).map { |x| x * 3 }        # => Ok(6)
Error("oops").map { |x| x }    # => Error("oops")

Converting a parsed value

Integer.parse("21").map { |n| n * 2 }   # => Ok(42)

flatMap

Applies f, which itself returns a Result, and flattens the result.

The step-by-step form of map for stages that can fail on their own. The first Error ends the chain and is the answer.

flatMap : (X -> Result<Y, E>) -> Result<Y, E>
Parameters
f X -> Result<Y, E>
the next fallible step

Returns: Result<Y, E> — the result of f, or the original Error

Examples
Ok(4).flatMap { |x| x > 0 then Ok(x * 2) else Error("neg") }    # => Ok(8)
Ok(-4).flatMap { |x| x > 0 then Ok(x * 2) else Error("neg") }   # => Error("neg")

A pipeline where each stage may fail

Integer.parse(raw)
  .flatMap { |n| n > 0 then Ok(n) else Error("must be positive") }
  .flatMap { |n| n < 65536 then Ok(n) else Error("out of range") }

optional

Converts the result to an Optional, dropping the error payload.

Useful when a caller only needs to know whether there is a value, and everything downstream already speaks Optional.

optional : X?

Returns: X?Just of the Ok value, or None

Examples
Ok(42).optional          # => Just(42)
Error("oops").optional   # => None

Keeping only the values that parsed

["1", "x", "3"]
  .map { |s| Integer.parse(s).optional }
  .filter(~set?)
  .map { |o| o.or(0) }    # => [1, 3]

function or

Returns the value unchanged.

The catch-all clause of or: a value that is neither an Optional nor a Result has already succeeded, so there is nothing to fall back to. This is what lets .or(default) be written after a call whose return type may later stop being optional, without the call site changing.

or(value, _)
Parameters
value A
any plain value

Returns: A — the same value

Examples
42.or(0)        # => 42
"text".or("")   # => "text"

function to

Converts value to the type t, or None if it cannot be represented.

t is a runtime type value: write the type name itself: String, Integer, Float, List. Conversion to String goes through the Showable protocol, so it works for every value.

The result is an Optional, not a Result, and deliberately so: the reason a conversion failed is usually implied by the types alone. When the reason carries information (where a parse gave up and on what) reach for Integer.parse or Float.parse, which answer with Result<_, ParseError>. So: to for every day, parse when the failure needs handling.

to(value, String)
Parameters
t Type
the target type

Returns: A? — the converted value, or None

Examples

Numbers and strings

42.to(String)        # => Just("42")
"42".to(Integer)     # => Just(42)
"4x".to(Integer)     # => None
"3.5".to(Float)      # => Just(3.5)

Closing the conversion immediately

let port = env.get("PORT").flatMap { |s| s.to(Integer) }.or(8080)

make Either<L, R> implements Eitherable