The is operator
Sometimes a program only needs to know which member a sum holds, not what is inside it. Is this token a number? Is it an operator? value is T answers with a plain bool: true when the active member is T.
A typed pattern can answer the same question, but it takes a whole match with an arm for every member. is is the one-line version for when a yes or no is all you want.
The tokens
A tokenizer turns the text price + 2 - discount + 1 into pieces, and each piece is one of four types:
struct Number {
value: int32;
}
struct Name {
text: char8[..];
}
struct Plus {}
struct Minus {}
type Token = Number | Name | Plus | Minus;
Plus and Minus carry no fields at all — for them, the type is the whole message.
The array is annotated Token[7], which makes every element a Token, so one array holds all four member types side by side:
let tokens: Token[7] = [
Name { text: "price" },
Plus {},
Number { value: 2 },
Minus {},
Name { text: "discount" },
Plus {},
Number { value: 1 }
];
Asking which member
Inside the loop, is sorts the tokens without unpacking any of them:
for token in tokens {
if token is Number {
numbers++;
}
if token is (Plus | Minus) {
operators++;
}
}
token is Number is true for the two numbers. Put a smaller sum in parentheses, token is (Plus | Minus), and the test is true when the active member is any one of them — the three operators here. It is the is form of a subset pattern.
The parentheses are required. Without them, token is Plus | Minus is refused, and the compiler's help shows the grouped form to write instead.
An ordinary bool
The result is a bool like any other, so it can be stored, printed or combined with && and ||:
let startsWithName = tokens[0] is Name;
PrintLine("starts with a name: {}", startsWithName);
PrintLine("ends with a number: {}", tokens[6] is Number);
What is does not do
is only asks. Two things follow from that.
It does not narrow. Inside if token is Number { … }, token is still the whole Token, not a Number. Reading token.value there is rejected, because a Name, a Plus or a Minus has no value. To use what is inside, bind it with a typed pattern in a match.
It does not quietly answer false. A test that could never be true is a mistake, and the compiler says so. token is bool is refused because bool is not a member of Token. On a value that is not a sum at all, is compares exact types, and a mismatch is an error too.
flowchart LR
q{"Do you need the<br/>value inside?"} -- "no — only which member" --> is["value is T<br/>value is (A | B)"]
q -- "yes" --> m["match with a typed pattern<br/>n: Number => n.value"]| You want | Write | You get |
|---|---|---|
| to know the member | token is Number | a bool |
| to know if it is one of several | token is (Plus | Minus) | a bool |
| the value inside | match token { n: Number => … } | n, a Number |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// Sometimes a program only needs to know *which* member a sum holds, not what is inside it.
// `value is T` answers that with a plain `bool`: true when the active member is `T`. Put a smaller
// sum in parentheses, `value is (Plus | Minus)`, and it is true when the active member is any one
// of them. The parentheses are required: `token is Plus | Minus` is refused with
// error: a sum type after 'is' must be grouped
// help: write 'value is (Plus | Minus)'
//
// Two things `is` does not do:
//
// - It does not narrow. Inside `if token is Number { ... }`, `token` is still the whole sum, so
// `token.value` is rejected:
// error: type 'Minus | Name | Number | Plus' has no field 'value'
// To use what is inside, bind it with a typed pattern in a `match`.
//
// - It does not quietly answer `false`. A test that could never be true is a mistake, and the
// compiler says so. `token is bool` is refused because `bool` is not one of the members:
// error: type 'bool8' is not a member or subset of sum 'Minus | Name | Number | Plus'
// On a value that is not a sum at all, `is` compares exact types, and a mismatch is an error
// too. The counter `numbers` is an `int`, so `numbers is bool` gives
// error: 'is bool8' can never be true for a value of type 'int'
import Io::PrintLine;
// The pieces of the expression `price + 2 - discount + 1`, as a tokenizer might hand them over.
struct Number {
value: int32;
}
struct Name {
text: char8[..];
}
struct Plus {}
struct Minus {}
type Token = Number | Name | Plus | Minus;
func Main() -> int {
// The annotation makes every element a `Token`, so one array holds all four member types.
let tokens: Token[7] = [
Name { text: "price" },
Plus {},
Number { value: 2 },
Minus {},
Name { text: "discount" },
Plus {},
Number { value: 1 }
];
var numbers = 0;
var operators = 0;
for token in tokens {
// `is` only asks; the token is untouched and keeps its type.
if token is Number {
numbers++;
}
if token is (Plus | Minus) {
operators++;
}
}
PrintLine("{} numbers, {} operators", numbers, operators);
// The result is an ordinary boolean, so it can be stored and printed like one.
let startsWithName = tokens[0] is Name;
PrintLine("starts with a name: {}", startsWithName);
PrintLine("ends with a number: {}", tokens[6] is Number);
return 0;
}
Run it
cd Examples/SumTypes/Is
rux run
2 numbers, 3 operators
starts with a name: true
ends with a number: true
Common mistakes
is without parentheses.token is Plus | Minus fails with error: a sum type after 'is' must be grouped, and the help says write 'value is (Plus | Minus)'.is to narrow.if token is Number { PrintLine("{}", token.value); } fails with error: type 'Minus | Name | Number | Plus' has no field 'value'. After the test token is still the whole sum. Use match token { n: Number => …, else => {} } to get at the number.token is bool fails with error: type 'bool8' is not a member or subset of sum 'Minus | Name | Number | Plus'. A test that can never be true is an error, not a constant false.The counter
numbers is an int, so numbers is bool fails with error: 'is bool8' can never be true for a value of type 'int'. On an ordinary value, is compares exact types.Try it yourself
- Count the
Nametokens as well, and print all three counts. - Write
func IsOperator(token: Token) -> boolwithis, and use it in the loop. - Add up the values of all the
Numbertokens. Why does this one need amatchwith anelse => {}arm rather thanis?
Learn more
- Typed pattern and Subset pattern — the patterns
ismirrors - Sum widening — the next lesson
- Type tests in the Rux Reference