Patterns

Pattern matching is a very big part of Hash and the productivity of the language. Patterns are a declarative form of equality checking, similar to patterns in Rust or Haskell.

Pattern matching within match statements is more detailed within the Conditional statements section of the book. This chapter is dedicated to documenting the various kinds of patterns that there are in Hash.

Literal patterns

Literal patterns are patterns that match a specific value of a primitive type, like a number or a string. For example, consider the following snippet of code:

#![allow(unused)]
fn main() {
foo := get_foo(); // foo: i32

match foo {
    1 => print("Got one");
    2 => print("Got two");
    3 => print("Got three");
    _ => print("Got something else");
}
}

On the left-hand side of the match cases there are the literal patterns 1, 2 and 3. These perform foo == 1, foo == 2 and foo == 3 in sequence, and the code follows the branch which succeeds first. If no branch succeeds, the _ branch is followed, which means "match anything". Literals can be integer literals for integer types (signed or unsigned), string literals for the str type, or character literals for the char type:

#![allow(unused)]
fn main() {
match my_char {
    'A' => print("First letter");
    'B' => print("Second letter");
    x => print("Letter is: " + conv(x));
}

match my_str {
    "fizz" => print("Multiple of 3");
    "buzz" => print("Multiple of 5");
    "fizzbuzz" => print("Multiple of 15");
    _ => print("Not a multiple of 3 or 5");
}
}

Binding patterns

Nested values within the value being pattern matched can be bound to symbols, using binding patterns. A binding pattern is any valid Hash identifier:

#![allow(unused)]
fn main() {
match fallible_operation() { // fallible_operation: () -> Result<f32, i32>
    Ok(success) => print("Got success " + conv(result)); // success: f32
    Err(failure) => print("Got failure " + conv(failure)); // failure: i32
}
}

Tuple patterns

Tuple patterns match a tuple type of some given arity, and contain nested patterns. They are irrefutable if their inner patterns are irrefutable, so they can be used in declarations.

#![allow(unused)]
fn main() {
Cat := struct(name: str);

// Creating a tuple:
my_val := (Cat("Bob"), [1, 2, 3]); // my_val: (Cat, [i32])

// Tuple pattern:
(Cat(name), elements) := my_val;

assert(name == "Bob");
assert(elements == [1, 2, 3]);
}

Constructor patterns

Constructor patterns are used to match the members of structs or enum variants. A struct is comprised of a single constructor, while an enum might be comprised of multiple constructors. Struct constructors are irrefutable if their inner patterns are irrefutable, while enum constructors are irrefutable only if the enum contains a single variant. For example:

#![allow(unused)]
fn main() {
Option := <T> => enum(Some(value: T), None);

my_val := Some("haha");

match my_val {
    // Matching the Some(..) constructor
    Some(inner) => assert(inner == "haha"); // inner: str
    // Matching the None constructor
    None => assert(false);
}
}

The names of the members of a constructor need to be specified if the matching isn't done in order:

#![allow(unused)]
fn main() {
Dog := struct(name: str, breed: str);

Dog(breed = dog_breed, name = dog_name) = Dog(
    name = "Bob",
    breed = "Husky"
) // dog_breed: str, dog_name: str

// Same as:
Dog(name, breed) = Dog(
    name = "Bob",
    breed = "Husky"
) // breed: str, name: str
}

List patterns

A list pattern can match elements at certain positions of a list by using the following syntax:

#![allow(unused)]
fn main() {
match arr {
    [a, b] => print(conv(a) + " " + conv(b));
    _ => print("Other"); // Matches everything other than [X, Y] for some X and Y
}
}

The ... spread operator can be used to capture or ignore the rest of the elements of the list at some position:

#![allow(unused)]
fn main() {
match arr {
    [a, b, ...] => print(conv(a) + " " + conv(b));
    _ => print("Other"); // Only matches [] and [X] for some X
}
}

If you want to match the remaining elements with some pattern, you can specify a pattern after the spread operator like so:

#![allow(unused)]
fn main() {
match arr {
    [a, b, ...rest] => print(conv(a) + " " + conv(b) + " " + conv(rest));
    [...rest, c] => print(conv(c)); // Only matches [X] for some X, rest is always []
    _ => print("Other"); // Only matches []
}
}

One obvious limitation of the spread operator is that you can only use it once in the list pattern. For example, the following pattern will be reported as an error by the compiler:

#![allow(unused)]
fn main() {
[..., a, ...] := arr;
}
error: Failed to typecheck:
 --> 1:6 - 1:9, 1:15 - 1:18
  |
1 | [..., a, ...] := arr;
  |  ^^^     ^^^
  |
  = You cannot use multiple spread operators within a single list pattern.

Module patterns

Module patterns are used to match members of a module. They are used when importing symbols from other modules. They follow a simple syntax:

#![allow(unused)]
fn main() {
// imports only a and b from the module
{a, b} := import("./my_lib");

// imports c as my_c, and d from the module.
{c as my_c, d} := import("./other_lib"); 

// imports Cat from the nested module as NestedCat
{Cat as NestedCat} := mod {
    pub Cat := struct(name: str, age: i32);
};
}

You do not need to list all the members of a module in the pattern; the members which are not listed will be ignored. To read more about modules, you can click here.

Or-patterns

Or-patterns are specified using the | pattern operator, and allow one to match multiple different patterns, and use the one which succeeds. For example:

#![allow(unused)]
fn main() {
symmetric_result: Result<str, str> := Ok("bilbobaggins");

(Ok(inner) | Err(inner)) := symmetric_result; // inner: str
}

The pattern above is irrefutable because it matches all variants of the Result enum. Furthermore, each branch has the binding inner, which always has the type str, and so is a valid pattern. The same name binding can appear in multiple branches of an or-pattern, given that it is bound in every branch, and always to the same type. Another use-case of or-patterns is to collapse match cases:

#![allow(unused)]
fn main() {
match color {
    Red | Blue | Green => print("Primary additive");
    Cyan | Magenta | Yellow => print("Primary subtractive");
    _ => print("Unimportant color");
}
}

Conditional patterns

Conditional patterns allow one to specify further arbitrary boolean conditions to a pattern for it to match:

#![allow(unused)]
fn main() {
match my_result {
    Ok(inner) if inner > threshold * 2.0 => {
        print("Phew, above twice the threshold");
    };
    Ok(inner) if inner > threshold => {
        print("Phew, above the threshold but cutting it close!");
    };
    Ok(inner) => {
        print("The result was successful but the value was below the threshold");
    };
    Err(_) => {
        print("The result was unsuccessful... Commencing auto-destruct sequence.");
        auto_destruct();
    };
}
}

They are specified using the if keyword after a pattern. Conditional patterns are always refutable, at least as far as the current version of the language is concerned. With more advanced type refinement and literal types, this restriction can be lifted sometimes.

Pattern grouping

Patterns can be grouped using parentheses (). This is necessary in declarations for example, if one wants to specify a conditional pattern:

#![allow(unused)]
fn main() {
// get_value: () -> bool;
true | false := get_value(); // Error: bitwise-or not implemented between `bool` and `void`
(true | false) := get_value(); // Ok
}

Grammar

The grammar for patterns is as follows:

pattern = 
    | single_pattern
    | or_pattern

single_pattern =
    | binding_pattern
    | constructor_pattern
    | tuple_pattern
    | module_pattern
    | literal_pattern
    | list_pattern

or_pattern = ( single_pattern "|" )+ single_pattern

binding_pattern = identifier

tuple_pattern_member = identifier | ( identifier "=" single_pattern )

constructor_pattern = access_name ( "(" ( tuple_pattern_member "," )* tuple_pattern_member? ")" )?

tuple_pattern = 
    | ( "(" ( tuple_pattern_member "," )+ tuple_pattern_member? ")" ) 
    | ( "(" tuple_pattern_member "," ")" )

module_pattern_member = identifier ( "as" single_pattern )?

module_pattern = "{" ( module_pattern_member "," )* module_pattern_member? "}"

literal_pattern = integer_literal | string_literal | character_literal | float_literal

list_pattern_member = pattern | ( "..." identifier? )

list_pattern = "[" ( list_pattern_member "," )* list_pattern_member? "]"