Welcome to the Salo MVP document. If you’re here, you’re probably interested in what Salo is, what it hopes to accomplish, and what advantages it provides. Without further ado, let’s get started!
In short…
- Salo is a “wrapper language” to Nix
- It is statically typed (and type inferred), has an ML-inspired syntax, and provides macros
- Each top-level Salo configuration must return an Attrset by the end of the file
Salo aims to:
- Use a static type system to check code at compile-time
- Provide a familiar, ML-like syntax
- Give extra functionality to configurations with macros
This section outlines the basics of Salo’s syntax. If you’d like to skip the basics and check out an actual example, scroll down to the Example configuration.
Before starting, let’s get familiar with Salo’s REPL. Run salo
in the command line once Salo is installed to open up the prompt:
> Welcome to the Salo REPL!
>
The REPL supports a few commands:
:t x
: get the type ofx
:q
: quit- [TODO] More to come!
Now that we’re familiar with the REPL, we can continue to learning Salo’s configuration language. From this point on, every code block that begins with ~> ~ is run in the REPL.
Salo has many types, mostly springing off of Nix’s types. These include:
- Bool
- Int
- String
- Attrset
- Array<T>
- Derivation
- Function
Values are created with a (mostly optional) type signature and value, as such:
a : String
a = "Hello, world!"
This is very similar to ML syntax.
In the example above, the definition of a
could have been rewritten as:
a = "Hello, world!"
Because Salo is smart enough to infer that a
’s type is String.
Functions are defined in a slightly different syntax:
> f : String -> String
> f x = x
Very Haskell-esque, indeed!
If you’re unfamiliar with ML syntax, this defines a function that takes a String and returns a String. In the implementation, f
takes x
and returns x
without modifications.
Salo types curry by default. Take the following code example (note the REPL prompt):
> :t f
f : String -> String
> g : String -> String -> String
> g x y = x + y
> :t g
g : String -> String -> String
> :t g "Hello, "
g "Hello, " : String -> String
Cool, right?
Salo supports pattern matching, e.g.:
name : Bool -> String name true = "Bob" name false = "Jeffrey"
In this case, if the Bool given to name
is true, it will evaluate to “Bob”. If it is given false, then it will evaluate to “Jeffrey”.
Salo pattern matches must be exhaustive. Meaning, this won’t work:
isOne : Int -> Bool isOne 1 = true
Salo will complain during compile time that this match does not cover every variant. What if we pass on 5, 6, or 7? Salo has no idea what to evaluate to. This, however, will work:
isOne : Int -> Bool isOne 1 = true isOne _ = false
With the _
character, Salo can match every other variant.
Functions don’t have to have strict types - with polymorphism, we’re able to allow any type to pass into our program, as long as it can validly compile (more on this later).
Again, similar to Haskell:
generic : a -> a -> a generic x y = x + y
This function will have a different type signature per call. For example, if we run:
generic "A" "B"
The type signature will be generic : String -> String -> String
. Salo knows the very second it sees that first argument "A"
that the other two values in the type signature must also be a String.
Earlier in this document, we mentioned that each top-level Salo configuration file must return an Attrset. Now, let’s examine how this is done.
return true
This is a minimal, valid Salo file. Crazy, right? Just kidding.
Anyways, note the return
keyword here. This indicates to Salo that this value should be returned, i.e. this file evaluates to true
.
Salo is also able to import other files using the import
keyword. Imports can either bring a library file or a local file into scope. For example:
import std::prelude::*;
Will import everything in the prelude
module of the standard library. This line is actually automatically inserted into every Salo file for ease-of-use. Note that glob imports are not recommended, but are possible.
import ./emacs.sa::backgroundColor
Will search for ./emacs.sa
. If not found, Salo will throw a compile-time error. If found, it will import the backgroundColor
value in emacs.sa.
Finally, we have the ability to import the returned value of a file, e.g.
git : Attrset git = import ./git.sa
Assuming ./git.sa
exists and returns an Attrset, the git
value will contain that value. If any Salo rules are violated during the import - the file does not exist or the returned value isn’t an Attrset - a compile-time error will be thrown.
description : String; -- type is string
description = "A system flake for my x86_64 server"; -- set value
-- Note that `description` is not specifically used in the result
-- Type is inferred : Array<Derivation>
packages = [
pkgs.git -- type is Derivation
];
hardware.pulseaudio = { -- an Attrset
enable = true; -- Booleans
extraModules = [ pkgs.pulseaudio-modules-bt ]; -- guess what type this is :P
package = pkgs.pulseaudioFull;
support32Bit = true;
extraConfig = "
load-module module-bluetooth-policy auto_switch=2
"; -- multiline Strings also work
}; -- end of Attrset
{
networking.hostName = "MyServer", -- can inline value
environment.systemPackages = packages, -- can use variable's value as long as the type checks
hardware, /* desugars into `hardware = hardware`
hardware is an Attrset which contains
Attrset, `pulseaudio`. */
} -- Note that the semicolon is omitted here, because this is what will be returned
-- If we placed a semicolon here, Salo would complain that nothing is returned
Evaluates to:
{ config, pkgs, ... }:
{
networking.hostName = "MyServer";
environment.systemPackages = [ pkgs.git ];
hardware.pulseaudio = {
enable = true;
extraModules = [ pkgs.pulseaudio-modules-bt ];
package = pkgs.pulseaudioFull;
support32Bit = true;
extraConfig = "load-module module-bluetooth-policy auto_switch=2";
};
}