An overview of the
when
statement: a flexible pattern matching structure in DeltaScript, and an alternative toswitch
I have spent over a year working obsessively to bring my vision of the perfect pixel art editor to life. The fruit of my labor 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 designed and implemented my own scripting language rather than embedding 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 last week (Jan 16, 2025), so I figured now is the perfect time to write about one of the features I am most excited about.
Note: This blog post is adapted from the original on my website. Read it there to see the proper syntax highlighting for DeltaScript code snippets.
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.
when
worksMy 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)
, where T
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:
() {
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 following 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