7. Absence and failure
Python uses None for "no value" and exceptions for "something went wrong", and neither shows in a function's signature. Luce puts both in the type: T? for a value that may be missing, T! for a result that may be a failure. The compiler then checks that every caller deals with them.
Optionals#
T? holds a T or none:
struct User:
let name: str
let email: str
func find(users: list[User], name: str) -> User?:
for user in users:
if user.name == name:
return user
return none
pub func main(arguments: list[str]) -> int!:
let users = [User(name = "Ada", email = "ada@example.org")]
if let user = find(users, "Ada"):
print(user.email)
else:
print("no such user")
let other = find(users, "Linus")
print(other == none)
let someone = find(users, "Linus") else User(name = "nobody", email = "")
print(someone.name)
return 0ada@example.org true nobody
In Python, forgetting to check for None shows up when the program runs, as an AttributeError. In Luce, a User? is not a User: you cannot read .email from it until you have checked. The ways to check:
if letbinds the value when there is one, as inif (user := find(...)) is not None:.while letdoes the same in a loop.elsegives a fallback, as forsomeoneabove, orages["Linus"] else 0. The fallback can also be areturn, which leaves the function when the value is missing:let user = find(users, name) else return.match, with anonearm.- Comparing with
none,other == none, tells you whether there is a value, but does not unwrap it.
There is no way to force an optional open and fail if it is empty, like a ! or a .get() in other languages. A missing value is always handled where it is read.
Errors#
A function that can fail has ! after its result type, and fails by calling error with an error code and a message:
let bad_age = ErrorCode.package(1)
func parse_age(text: str) -> int!:
let age = int(text)
if age < 0 or age > 150:
error(bad_age, f"{age} is not an age")
return age
pub func main(arguments: list[str]) -> int!:
print(parse_age("36"))
print(parse_age("200"))
print("not reached")
return 036 error: 200 is not an age
Two things happen here that Python does with exceptions:
- A failure passes up by itself.
int(text)can fail, and insideparse_age, which is itself fallible, a failure ofint(text)simply becomes a failure ofparse_age. The same happens inmain. You write nothing at each call, as with an exception in Python. - The signature says so. A function without
!cannot let a failure through: calling a fallible function there is an error unless you handle the failure. Every failure that can reach a caller is visible in a signature on the way.
When a failure reaches the end of main, the program prints error: and the message, and exits with status 1.
An error code is declared once, at the top of a file, as a constant: ErrorCode.package(1) is code 1 of the package the file belongs to. Codes of two different packages never clash, so a caller can tell whose error it has.
Handling a failure#
catch handles a failure, like Python's except:
let bad_age = ErrorCode.package(1)
func parse_age(text: str) -> int!:
let age = int(text)
if age < 0 or age > 150:
error(bad_age, f"{age} is not an age")
return age
func age_or_zero(text: str) -> int:
return parse_age(text) catch failure:
print(f"ignoring {text}: {failure.message}")
recover 0
func describe(text: str) -> str!:
let age = parse_age(text) catch failure:
if failure.code == bad_age:
return "out of range"
error(failure.code, f"cannot read {text}: {failure.message}")
return f"{age} years"
pub func main(arguments: list[str]) -> int!:
print(age_or_zero("36"), age_or_zero("old"))
print(describe("36"), describe("200"))
return 0ignoring old: not an integer 36 0 36 years out of range
expression catch failure: runs its block when the expression fails, with the failure bound to failure, which has a code and a message. The block must then decide what happens:
recover valuegives the expression a value after all, asage_or_zerodoes.returnleaves the function.error(...)fails in turn, here with a message that adds context.
age_or_zero has no !, so it must handle the failure of parse_age: without the catch, it does not compile.
Traps#
Some failures are not meant to be handled, because they mean the program itself is wrong: an int overflowing, an index past the end of a list, division of integers by zero, a list changed while a loop goes through it. These trap: the program stops at once and prints where.
pub func main(arguments: list[str]) -> int!:
let numbers = [1, 2, 3]
print(numbers[3])
return 0trap: main.luc:3:5: index out of range
A trap cannot be caught. That is the difference between the two kinds: an error is a situation the program expects and can handle, such as text that is not a number or a file that is missing; a trap is a mistake to fix in the code. assert(condition) traps when the condition is false, and trap("message") traps with a message of your own.
Where next#
Chapter 8 shows how to write code that works with many types: interfaces and generics.