Skip to content

Instantly share code, notes, and snippets.

@jepotter1-archive
Last active February 18, 2021 22:31
Show Gist options
  • Save jepotter1-archive/a2b641434358e1d8699de5ffad764314 to your computer and use it in GitHub Desktop.
Save jepotter1-archive/a2b641434358e1d8699de5ffad764314 to your computer and use it in GitHub Desktop.
Stalwart language tutorial

Stalwart

What is Stalwart?

Stalwart is a modern, high-level, statically-typed language that lets you program using both functional and imperative styles. The goal of Stalwart is to make it easy to write bug-free code, and also easy to read through and maintain large codebases. Although performance is important, it is not a priority.

Stalwart is inspired in some way by almost every modern programming language. Depending on your language background, you may characterize Stalwart as:

  • Python, but with static typing and without OOP
  • OCaml, but simpler and with a more readable syntax
  • Go, but with functional programming and more language features
  • Rust, but with a garbage collector and high-level abstractions
  • Erlang/Elixir, but with static typing and a more readable syntax

Introduction

The obligatory "Hello, World" program:

fn Main() {
    Console.Print("Hello, World!")
}

As you can see, the program entrypoint in Stalwart is the Main function. Unlike scripting languages like Python and Ruby, Stalwart source files don't contain imperative statements at the top level; everything is contained within functions.

Now, let's make our Hello World program a bit more complex:

fn pure hello(name : String) String {
    return "Hello, ${name}!"
}

fn Main() {
    Console.Print(hello("Joe"))
}

The output should be:

Hello, Joe!

This example gives us a better idea of Stalwart's syntax. Stalwart is a statically typed language, so type annotations are required for functions. Similar to JavaScript, string interpolation is possible with ${}.

Note that the function hello is annotated with pure. This means that the function is a pure function that does not have any side effects; in other words, if you run it with the same parameters, it will return the same result, no matter the context, and it won't modify anything outside of the function. Stalwart's compiler uses this information to lazily evaluate the function for performance, as well as check to make sure that the function is really pure. Ideally, most code would be pure, but today's applications are heavily IO-bound, so most functions are going to be interacting with outside things like database connections, standard output, etc.

Also note that the function Main begins with an uppercase character whereas the function hello begins with a lowercase character. In Stalwart, public functions (functions that are exported) use UpperCamelCase whereas private functions (functions only accessible within the module) use snake_case. This convention is inspired by Go. We'll cover more on modules later on.

Variables

Like most languages under the sun, Stalwart has variables. No special keywords are required to declare variables, just use an equals sign like Python:

message = "Hello, World!"

The convention for variables is to use snake_case. All variables are scoped locally; there are no global variables. Stalwart does not have null or undefined so all variables are guaranteed to have a value.

If you want to declare a variable with a slight change from a previous variable, you can use a single tick mark (') at the end (this is a convention taken from Haskell):

message = "hello, world!"
message' = String.Titlecase(message) # Hello, World!

Similar to Ruby, boolean variables conventionally have a question mark at the end:

cool? = True

You can add type annotations to variables by specifying a type after the variable name:

message: String = "hello, world!"

Variables are immutable and constant by default, so you can't redefine or manipulate them without defining a new variable. If you would like to mutate a variable, you can make it mutable by adding the keyword mut before the type annotation and after the variable name:

cool: mut Boolean = True # cool, at least for now

Note, however, that this practice is discouraged in Stalwart since you should be using functional programming practices to avoid state mutation.

Comments

Comments begin with a pound sign (#) like in Python.

# I'm a comment!

Pipe Operator

Because Stalwart is a functional language without methods, function chaining can often get very verbose, like this:

A(B(C(D)))

The pipe operator (|>), similar to that of Elixir and F#, helps alleviate the problem. Instead, you can write this:

D |> C(_) |> B(_) |> A(_)

The underscore (_) signifies the argument space that the pipe operator should pass the result of the previous expression to. This allows you to chain functions where the output of the previous function is the second argument to the next function:

D |> C(x, _)

Which is equivalent to this:

C(x, D)

So our Hello World example could have been written like this:

"Joe" |> hello(_) |> Console.Print(_)

Pattern Matching

Stalwart doesn't have if/else statements. Instead, it uses the much more powerful paradigm of pattern matching, inspired by Rust and OCaml. Instead of an if statement, you would write something like this:

x = True
match x:
| True -> Console.Print("Hello, World!")
| False -> {}

Each pipe character (|) denotes a new branch to match on. The order in which the branches are specified doesn't matter; in Stalwart, branches are not allowed to overlap with each other, so every single possible value can match only a single branch.

Notice the empty pair of curly braces ({}) in the False branch. Empty curly braces tell Stalwart to do nothing and move on, similar to the pass keyword in Python.

Pattern matching can match on many different types of things. You can match on literal values, like this:

Console.Print("Which language do you prefer?")
x = Console.Input()
match x:
| "Rust" or "Haskell" -> Console.Print("Hurrah!")
| "PHP" or "JavaScript" -> Console.Print("Oh no")
| _ -> Console.Print("Never heard of it")

Notice that multiple cases can be joined with the or statement. Also notice that the "default" case uses the underscore character (_): an underscore signifies a wildcard value that all values will match. The default case is necessary because switches cannot fall through in Stalwart: all conditions must be handled.

You can also match on enums (a.k.a. union types if you come from ML):

type enum Language = Rust | Haskell | PHP | JavaScript

fn Main() {
    language = Rust
    match language:
    | Rust or Haskell -> # ...
    | PHP or JavaScript -> # ...
}

If the enums contain values, you can match on them too. One common use for these is with the Result and Option types, inspired by Rust:

x = "Hey"
match Int.OfString(x):
| Ok[x'] -> x' |> Int.Add(_, 5) |> String.OfInt(_) |> Console.Print(_)
| Err -> Console.Print("woops!")

Notice that square brackets are used for type parameters in Stalwart. x' is a new variable of type Int that is locally scoped to the Ok branch, which is the value of Int.OfString(x) in the case that x can be converted to an Int. We name it x' here by convention, but you can name it anything you like.

Since Hey cannot be converted to an integer, this snippet will print:

woops!

String.OfInt returns String instead of Result[String] because there is no possible situation in which an Int cannot be converted to a String.

You can match on multiple values, separated by commas:

match food, drink:
| "Dumplings", "Water" -> # ...
| "Dumplings", "Tea" -> # ...
| "Xialongbao", "Tea" or "Xiaolongbao", "Water" -> # ...

Pattern matching can also be used as an expression:

good? = match language:
| Haskell or Rust => True
| PHP or JavaScript => False

Nested pattern matches can be achieved using multiple pipe characters:

match process:
| Combustion => match molecule:
|| "CH4" => # ...
|| "C2H5OH" => # ...
| Decomposition => match molecule:
|| "O3" => # ...
|| "H2O2" => # ...

Functions

You've already seen how functions can be defined in Stalwart, for example:

fn Concat(x : String, y : String) String {
    # ...
}

Implicit returns are not allowed, so we must explicitly return a value.

Besides pure functions, Stalwart also has recursive functions. Recursive functions can call themselves using the special *self identifier:

fn recursive zero(x : Int) Int {
    match Int.Compare(x, 0) {
        case Greater {
            *self(Int.Subtract(x, 1))
        }
        case Less {
            *self(Int.Add(x, 1))
        }
        case Equals {
            return x
        }
    }
}

This is somewhat of a stupid function: it always returns 0, no matter what, and it will waste a bunch of computer resources in doing so. But it illustrates recursion decently well: if it gets any value that's not zero, it will attempt to add or subtract one and then call itself with that value. Eventually, it will get to zero, after calling itself many times.

Like many languages, functions are first-class in Stalwart: they can be passed around as values. Anonymous functions are defined in exactly the same way normal functions are, except without a name:

arr = List.Map(List{1, 2, 3, 4, 5}, fn pure (x : Int) Int {
    return Int.Power(x, 2)
})

Working with Numbers

Stalwart has no infix functions. Common mathematical functions are, from the programmer's point of view, just normal standard library functions, invoked like any other function.

Int.Add(5, 2) # 7
Int.Subtract(4, 3) # 1

There are three types of numbers in Stalwart:

  • Int - Whole numbers. Can be either positive or negative.
  • Float - Decimal numbers. Mathematical computations are more performant but also less accurate.
  • Decimal - Decimal numbers. Mathematical computations are less performant but more accurate.

Decimal is generally preferred over Float for correctness, but Float can be used when performance is more important.

Enums and Structs

Since it is not an object-oriented language, Stalwart does not have classes. Instead, it has structs and enums.

type enum Condition = New | LikeNew | Used

type struct Product {
    Name : String
    Condition : Condition
    Price : Decimal
}

Structs can have default values, like this:

type struct Product {
    Name : String
    Condition : Condition = New
    Price : Decimal
}

To instantiate a struct:

new_product = Product{ Name = "Foo", Price = 5.00 }

Note that Condition is omitted, since it has a default value and is therefore optional to specify.

To instantiate an enum:

new_condition = Condition.LikeNew

Collections

Stalwart has three types of collections, all of which are immutable:

  • List[T] - List with any size, but all elements must be of the same type.
  • Tuple[X, Y, ...] - List with fixed size and order, but each element can be of a different type.
  • Map[K, V] - Simple key-value store of unlimited size. All the keys must be of the same type and all the values must be of the same type.

Initializing a collection is similar to initializing a struct:

x = Tuple{1, 2, "Hello"}
y = List{1, 2, 3, 4, 5}
z = Map{ "Joe" = 5, "Gloria" = 10 }

Collections can optionally be type annotated with bracket notation. The above example would look like this with type annotations:

x = Tuple[Int, Int, String]{1, 2, "Hello"}
y = List[Int]{1, 2, 3, 4, 5}
z = Map[String, Int]{ "Joe" = 5, "Gloria" = 10 }

Modules

Standard library modules are imported by default, so there is no need to import them explicitly.

Stalwart source file names must use UpperCamelCase, because module names of source files are derived from their filenames. To import another file, use the import keyword along with a relative path, like this:

# a/Main.sw

import ../b/Hello

fn Main() {
    Hello.HelloWorld()
}

# b/Hello.sw

fn HelloWorld() {
    Console.Print("Hello, World!")
}

As you can see, imports are namespaced by default. If you want to skip the namespace for specific functions, you can use import ... from, like this:

import HelloWorld, Foobar from ../Hello

fn Main() {
    HelloWorld()
    Foobar()
}

To avoid namespace fatigue, if a type or function has the same name as the module it is in, the namespace is dropped. So, for example:

# a/Main.sw

import ../b/Hello

fn Main() {
    Hello()
}

# b/Hello.sw

fn Hello() {
    Console.Print("Hello, World!")
}

You can also do absolute imports. Stalwart will walk up from the directory of the source file until it finds a Stalwart.toml, which signifies the root of a Stalwart project. If an import begins with /, it is understood to be a reference to the root of the project.

So, for example, in a directory structure like this:

project
├── a
│   └── b
│       └── Main.sw
└── Foobar.sw

Inside Main.sw you could import Foobar.sw like this:

import /Foobar

You can also use the tilde character (~) to signify the subdirectory vendor/Stalwart of the root directory, where third-party packages are stored.

So, for example, in the following directory structure:

project
├── src
│   └── a
│       └── b
│           └── c
│               └── Main.sw
├── Stalwart.toml
└── vendor
    └── Stalwart
        └── Foobar
            └── Foobar.sw

Foobar in this example is a third-party package, providing one module called Foobar. Inside Main.sw, you can import Foobar.sw like this:

import ~/Foobar/Foobar

Metaprogramming

Stalwart supports powerful metaprogramming through hygienic macros:

fn hello_world() {
    Console.Print("Hello, World!")
}

fn macro call_function(name : Identifier) {
    {{ name }}()
}

fn Main() {
    call_function("hello_world")
}

In the example above, call_function is a macro. When you call a macro function, the compiler copies the contents of the macro function into the place where you called it. In other words, macros generate code rather than execute it.

Type signatures for macros are a bit different from those of regular functions. Macros don't need a return type in the type signature since they don't return a value. In addition, macros can take a few special types as arguments:

  • The Identifier type, which must be a valid Stalwart identifier contained within a string
  • The Expression type, which must be a valid Stalwart expression contained within a string

The above function would expand to this at compile time:

fn hello_world() {
    Console.Print("Hello, World!")
}

fn Main() {
    hello_world()
}

As you can see, the above function used double curly braces ({{, }}) to substitute values at compile time. You can also use {% and %} to write expressions within a macro:

type enum Language = English | Chinese | Spanish

fn macro greet(name : String, language : Language) {
    {% greeting = match language:
    | Chinese -> "你好"
    | English -> "Hello"
    | Spanish -> "Hola"
    %}
    Console.Print({{ "${greeting}, ${name}!" }})
}

fn Main() {
    name = "Jeremy"
    language = Language.Chinese
    greet(name, language)
}

This would evaluate to:

type enum Language = English | Chinese | Spanish

fn Main() {
    name = "Jeremy"
    language = Language.Chinese
    Console.Print("你好, Jeremy!")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment