When To Say When: Reinventing the Switch Statement
January 16, 2025
An overview of the when
statement: a flexible pattern matching structure in DeltaScript, and an alternative to switch
I have spent over a year working obsessively to bring my vision of the perfect pixel art editor to life. The fruit of my labour is Stipple Effect, a program that lets users write scripts for a variety of use cases, including transforming the project for display in the preview window in real time:
I was very particular about how I wanted the script-writing process to feel for users: quick, clear, painless, iterative. To achieve that, I opted to design and implement my own scripting language rather than embed Lua or another established scripting language in my program.
The result is DeltaScript, a scripting language "sketelon" designed to be extended for specific application domains. Stipple Effect's scripting API is one such extension.
I released the language specification for DeltaScript v0.1.0 today, so I figured now is the perfect time to write about one of the language's features that I am most excited about.
Why when
?
DeltaScript was always supposed to be a high-level interpreted language. As such, I wanted the language to have powerful, flexible control flow structures that could express complex logic concisely and still be readable and maintainable.
One of my biggest frustrations as a programmer is the limitations and the implementation philosophy of the traditional switch
statement: limited to literals in case labels, fallthrough, etc.
I do most of my programming in Java, and I must say, recent Java language versions have drastically extended the functionality of switch
and turned it into a near-perfect pattern matching structure. I wanted to do something similar for DeltaScript.
How when
works
My when
statement supports three different kinds of non-trivial cases:
is
- matches the control expression against one or more expressions, checking for equalitymatches
- uses the special identifier_
to replace the control expression and defines a pattern in the form of a boolean expressionpasses
- accepts a test function (predicate) of type(T -> bool)
, whereT
is the type of the control expression
These cases can be arranged inside a when
statement in any order. Each case is checked in order until a case passes its check, at which point the case body is executed. There is no fallthrough; once the runtime execution identifies a successful match case and executes its body, the execution drops out of the when
statement and executes the statement that follows it.
Consider this example:
(color c) {
~ string pfx = "The color is ";
when (c) {
matches _.alpha == 0 -> print(pfx + "transparent");
is #000000 -> print(pfx + "black");
is #ffffff -> print(pfx + "white");
matches _.r == _.g && _.r == _.b && opaque(_) ->
print(pfx + "a shade of grey");
is #ff0000, #00ff00, #0000ff -> print(pfx + "an RGB primary color");
passes ::bright_opaque -> print("bright");
otherwise -> print(pfx + "not a match");
}
}
bright_opaque(color c -> bool) {
int max = max([ c.r, c.g, c.b ]);
return max == 0xff && opaque(c);
}
opaque(color c -> bool) -> c.alpha == 0xff
This logic cannot be expressed by a traditional switch
statement. Expressing it with an if
...else if
would look like this:
(color c) {
~ string pfx = "The color is ";
if (c.alpha == 0) print(pfx + "transparent");
else if (c == #000000) print(pfx + "black");
else if (c == #ffffff) print(pfx + "white");
else if (c.r == c.g && c.r == c.b && opaque(c))
print(pfx + "a shade of grey");
else if (c == #ff0000 || c == #00ff00 || c == #0000ff)
print(pfx + "an RGB primary color");
else if (bright_opaque(c)) print("bright");
else print(pfx + "not a match");
}
bright_opaque(color c -> bool) {
int max = max([ c.r, c.g, c.b ]);
return max == 0xff && opaque(c);
}
opaque(color c -> bool) -> c.alpha == 0xff
You can read the full semantics of the when
statement in the language specification.
I'll leave you with this long-form example that shows off a few additional language features of interest:
Example:
() {
string[] words = [
"Racecar", "Pilot", "Madam",
"Able was I ere I saw Elba",
"Nurses run", "Highway 61",
"A man, a plan, a canal - Panama"
];
(string -> bool) no_whitespace_palindrome =
(s -> palindrome(no_whitespace(s)));
for (word in words) {
when (word) {
passes ::palindrome -> print("\"" + _ + "\" is a pure palindrome!");
passes no_whitespace_palindrome ->
print("\"" + _ + "\" is a palindrome if whitespace is ignored");
passes (s -> palindrome(only_letters(s))) ->
print("\"" + _ + "\" is a palindrome if whitespace and punctuation are ignored");
otherwise -> print("\"" + _ + "\" is not a palindrome");
}
}
}
palindrome(string s -> bool) {
string lc = lowercase(s);
return lc == reverse(lc);
}
reverse(string s -> string) {
string res = "";
for (c in s)
res = c + res;
return res;
}
lowercase(string s -> string) {
string res = "";
for (c in s) {
int unicode = (int) c;
if (uppercase_letter(c))
res += (char) ((int) 'a' + (unicode - (int) 'A'))
else
res += c;
}
return res;
}
no_whitespace(string s -> string) {
~ char{} WHITESPACE = { ' ', '\t' '\n' };
string res = "";
for (c in s)
if (!WHITESPACE.has(c))
res += c;
return res;
}
only_letters(string s -> string) {
string res = "";
for (c in s)
if (uppercase_letter(c) || lowercase_letter(c))
res += c;
return res;
}
uppercase_letter(char c -> bool) {
int unicode = (int) c;
return unicode >= (int) 'A' && unicode <= (int) 'Z';
}
lowercase_letter(char c -> bool) {
int unicode = (int) c;
return unicode >= (int) 'a' && unicode <= (int) 'z';
}
This script produces the output:
"Racecar" is a pure palindrome!
"Pilot" is not a palindrome
"Madam" is a pure palindrome!
"Able was I ere I saw Elba" is a pure palindrome!
"Nurses run" is a palindrome if whitespace is ignored
"Highway 61" is not a palindrome
"A man, a plan, a canal - Panama" is a palindrome if whitespace and punctuation are ignored