Skip to content

Instantly share code, notes, and snippets.

@bobbbay
Last active July 29, 2021 21:27
Show Gist options
  • Save bobbbay/7c3dbf0b028712c10d6699fb97b987f0 to your computer and use it in GitHub Desktop.
Save bobbbay/7c3dbf0b028712c10d6699fb97b987f0 to your computer and use it in GitHub Desktop.

Salo MVP

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!

Overview

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

Goals

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

The basics

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.

The REPL

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 of x
  • :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.

Types

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

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.

Currying

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?

Pattern matching

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.

Generic parameters

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.

Returning

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.

Imports

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.

Example configuration

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";
  };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment