Conditional Blocks¶
S++ conditional blocks combine standard if-else statements with pattern-matching. This simplifies the language by reducing the number of constructs needed to handle conditional logic.
A wide range of pattern matching is supported:
Literals:
1
,1.0
,true
,false
,"hello"
Wildcards:
_
Capture variables:
x
,y
,z
Or patterns:
"yes" | "no"
Array destructuring:
[1, 2, 3]
Tuple destructuring:
(1, 2, 3)
Object destructuring:
Person(name="john", age=30)
Guard clauses:
and ...
Attribute aliasing:
Person(name as new_name, age)
Skipping multiple elements:
[1, .., last]
,(1, .., last)
,Person(name="john", ..)
Nested destructuring:
[a, (0, y, z), Person(name="john", ..), _, other, ..]
Basic Branching¶
The following example shows the most basic version of the case-else
block. It follows the standard if-else
syntax of
most programming languages.
case some_condition {
std::console::print("hello world")
}
else case some_other_condition_1 {
std::console::print("hello galaxy")
}
else case some_other_condition_2 {
std::console::print("hello universe")
}
else {
std::console::print("hello multiverse")
}
Imitation Ternary¶
There is no ?:
ternary operator, as the case
and else
keywords can be used to achieve the same result. This also
follows the “left-ro-right” reading order of the language.
let result = case some_condition { "true" } else { "false" }
Partial Fragments (Expressions)¶
Partial fragments can be used to introduce multiple comparisons or pattern matches against the same value. Each
comparison contains a pattern fragment, which when combined with the partial fragment, creates a full pattern. Fragments
are joined by a comparison operator: ==
, !=
, <
, >
, <=
, >=
, or is
.
This subsection focuses on regular comparison operators, while the is
operator is discussed in a later section.
Pattern fragments are introduced with the of
keyword.
case person of
== john { "hello john" }
== jane { "hello jane" }
else { "hello stranger" }
case age of
<= -1 { "error" }
== 00 { "newborn" }
< 18 { "child" }
< 60 { "adult" }
else { "senior" }
Partial expression are expanded into their full form by the compiler: the first case
expression effectively becomes
case person.eq(john) { }
. See binary operator expansion for more information on expansion.
Multiple Pattern Fragments¶
Multiple pattern fragments can be used on the same branch, separated by a comma. This allows for more complex pattern matching, and can be used to match multiple values at once.
case person of
== john, jane { "hello john or jane" }
== jack, jill { "hello jack or jill" }
else { "hello stranger" }
Multiple pattern fragments per branch can only be used with non-is
pattern fragments. The reason behind this is
explored in the destructuring sections.
Destructuring Objects¶
Destructuring objects in a pattern match is done by using the is
operator. This follows the standard binary is
functionality, where the right-hand-side can be a destructure of any variation.
case person of
is Person(name="john", ..) { "hello john" }
is Person(name="jane", ..) { "hello jane" }
else { "hello stranger" }
The ..
token is used to mark missing attributes.
Destructuring Variant Objects¶
Because equality checks of variants can match their inner types (if the variant is the let-hand-side), the is
operator
can be used for destructuring a vairiant object.
case optional of
is Some[Str](val) { "some" }
is None { "none" }
Due to current generic implementations,
Some[Str](val)
must be used instead ofSome(val)
. There is an issue with generics inside variant types. This will be fixed in a future release.
Destructuring Generic Types¶
There is no keyword-equivalent to C++’s if constexpr(...)
to check generic types inside a function. As such,
destructuring generic types was introduced to allow for pattern matching on generic types.
fun func[T](a: T) -> Void {
case a of
is Str() { std::console::print("Str") }
is U32() { std::console::print("U32") }
}
Destructuring Tuples, Arrays¶
Tuples and arrays can be destructured, because their size is known, and so the number of elements in the pattern match
can be verified. Unless the ..
token is present, as with object identifiers, all elements must be matched. The ..
token can appear anywhere in the pattern match, but only once per pattern.
case tuple of
is (1, ..) { "tuple starts with 1" }
is (2, ..) { "tuple starts with 2" }
else { "tuple is something else" }
case array of
is [1, ..] { "array starts with 1" }
is [2, ..] { "array starts with 2" }
else { "array is something else" }
Destructuring with Bindings¶
Variables can be bound during a destructure, and used in the branch. This allows for more complex pattern matching, and
can be used to extract values from the object. Destructures that contain bindings are expanded into let
statements, as
their semantics are almost identical.
Note that for nested bindings, such as Line(point=Point(a, b), ..)
, the variable point
does exist, but in a
partially moved state, because let a = point.a
and let b = point.b
will have both been executed to get the values
for a
and b
.
case tuple of
is (a, 1) { "tuple is ($a, 1)" }
is (b, 2) { "tuple is ($b, 2)" }
else { "tuple is something else" }
case array of
is [a, 1] { "array is [$a, 1]" }
is [b, 2] { "array is [$b, 2]" }
else { "array is something else" }
case person of
is Person(name="john", age, ..) { "hello john, you are ${age}" }
is Person(name="jane", age, ..) { "hello jane, you are ${age}" }
else { "hello stranger" }
Patterns can be nested too, to any depth and with any combination of object/variant/array/tuple destructuring, enabling highly complex pattern matching.
Pattern Guards on Destructures¶
Pattern guards can only be used on destructures, as the and
would conflict with normal expressions.
case person of
is Person(name="john", age, ..) and age < 18 { "hello john, you are a child" }
is Person(name="john", age, ..) and age < 60 { "hello john, you are an adult" }
is Person(name="john", age, ..) { "hello john, you are a senior" }
else { "hello stranger" }