Examples

Values that may be missing, errors, and functions as values

The whole of examples/sensor.zeno, and what zeno run prints for it.

sensor.zeno
# sensor.zeno: values that may be missing, errors, and functions as values.
#
#     zeno run examples/sensor.zeno      run it with the interpreter
#     zeno build examples/sensor.zeno    compile it to sensor.c and ./sensor
#
# A temperature sensor sends lines of text. Some lines are damaged and some
# readings are missing. Turn the lines into numbers, keep the good ones,
# and report on them.


# --- A value that may be missing ---

# One line from the sensor becomes a temperature, or `none` for a gap.
function parse_line(line: str) -> float or none raises ValueError {
    let text = line.strip()
    if text == "--" {
        return none                       # the sensor had nothing to say
    }
    let value = float(text)               # a damaged line raises ValueError
    if not -40.0 <= value <= 125.0 {
        raise ValueError(f"{value} is outside what the sensor can measure")
    }
    return value
}


# --- An error of our own, carrying data ---

# Any class can be raised. This one says how bad things got.
class TooManyFaults {
    count: int
    limit: int
}

function read_all(lines: list[str], limit: int) -> list[float or none] raises TooManyFaults {
    var readings: list[float or none] = []
    var faults = 0
    for line in lines {
        try {
            readings.append(parse_line(line))
        } except ValueError as err {
            faults += 1
            print(f"  skipped line {faults}: {err}")
            if faults > limit {
                raise TooManyFaults(faults, limit)
            }
        }
    }
    return readings
}


# --- Functions as values ---

# Gives back a new function that remembers `low` and `high`.
function in_range(low: float, high: float) {
    return function(t: float) { return low <= t <= high }
}

# Gives back a function that counts how often it is called. The inner
# function changes `count`, a variable of the outer one, and the change
# is still there on the next call.
function counter() {
    var count = 0
    return function() {
        count += 1
        return count
    }
}


let lines = ["21.5", " 22.0 ", "--", "abc", "23.5", "900", "24.0"]
let readings = read_all(lines, 5)
print("readings:", readings)

# A reading can only be used as a number once `none` is ruled out.
var present: list[float] = []
for reading in readings {
    if reading is not none {
        present.append(reading)
    }
}

let comfortable = present.filter(in_range(21.0, 23.0))
let fahrenheit = present.map(function(c: float) { return c * 1.8 + 32 })
print("present:", present)
print("comfortable:", comfortable)
print("fahrenheit:", fahrenheit)
print(f"average: {sum(present) / len(present):.2f}")

let next_id = counter()
print("ids:", next_id(), next_id(), next_id())

# With a limit of one fault, the same data is too damaged to use.
try {
    read_all(lines, 1)
} except TooManyFaults as fault {
    print(f"gave up after {fault.count} faults (the limit was {fault.limit})")
}
zeno run examples/sensor.zeno
  skipped line 1: cannot turn "abc" into a float
  skipped line 2: 900.0 is outside what the sensor can measure
readings: [21.5, 22.0, none, 23.5, 24.0]
present: [21.5, 22.0, 23.5, 24.0]
comfortable: [21.5, 22.0]
fahrenheit: [70.7, 71.6, 74.30000000000001, 75.2]
average: 22.75
ids: 1 2 3
  skipped line 1: cannot turn "abc" into a float
  skipped line 2: 900.0 is outside what the sensor can measure
gave up after 2 faults (the limit was 1)

See it on GitHub: examples/sensor.zeno.