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:

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