Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Motivation

Adios aims to be a radically simple alternative to the NixOS module system that solves many of it's design problems. Modules are contracts that are typed using the Korora type system.

NixOS module system problems

  • Lack of flexibility

NixOS modules aren't reusable outside of a NixOS context. The goal is to have modules that can be reused just as easily on a MacOS machine as in a Linux development shell.

  • Global namespace

The NixOS module system is a single global namespace where any module can affect any other module.

  • Resource overhead

Because of how NixOS modules are evaluated, each evaluation has no memoisation from a previous one. This has the effect of very high memory usage.

Adios modules are designed to take advantage of lazy evaluation and memoisation.

Concept

Modules

Module definition

Adios module definitions are plain Nix attribute sets.

Module loading

The module definition then needs to be loaded by the adios loader function:

adios {
  name = "my-module";
}

Module loading is responsible for

  • Wrapping the module definition with a type checker

    Module definitions are strictly typed and checked.

  • Wrapping of module definitions impl function that provides type checking.

Callable modules

Callable modules are modules with an impl function that takes an attrset with their arguments defined in options:

{ adios }:
let
  inherit (adios) types;
in
{
  name = "callable-module";

  options = {
    foo = {
      type = types.string;
      default = "foo";
    };
  };

  # impl takes the values set for each option. The user can specify their own
  # value for `options.foo`, or just fall back on the default
  impl =
    { options }:
    {
      # Evaluating someValue.bar will typecheck options.foo
      someValue.bar = options.foo;
    };
}

Note that module returns are not type checked. It is expected to pass the return value of a module into another module until you have a value that can be consumed.

Laziness

Korora does eager evaluation when type checking values. Adios module type checking however is lazily, with some caveats:

  • Each option, type, test, etc returned by a module are checked on-access

  • When calling a module each passed option is checked lazily

But defined struct's, listOf etc thunks will be forced. It's best for options definitions to contain a minimal interface to minimize the overhead of eager evaluation.

Example modules

Nixpkgs

{ adios }:
let
  inherit (adios) types;
in
{
  options = {
    pkgs = {
      type = types.attrs;
      default = import <nixpkgs> { };
    };
  };
}

Hello world

{ adios }:
let
  inherit (adios) types;
in
{
  options = {
    enable = {
      type = types.bool;
      default = false;
    };

    package = {
      type = types.derivation;
      defaultFunc = { inputs }: inputs."nixpkgs".pkgs.hello;
    };
  };

  inputs = {
    nixpkgs = {
      path = "/nixpkgs";
    };
  };

  impl =
    {
      options,
      inputs,
    }:
    let
      inherit (inputs.nixpkgs.pkgs) lib;
    in
    lib.optionalAttrs options.enable {
      packages = [
        options.package
      ];
    };
}

Helper functions

importModules

Adios comes with a function importModules, that will automatically import all the modules in a directory (provided they follow a certain schema).

Usage

Given this directory structure:

./modules
├── default.nix
├── foo
│   └── default.nix
└── bar
    ├── baz
    │   └── default.nix
    └── default.nix

If the root module at default.nix is defined like this:

{ adios }:

{
  name = "root";
  # Other contents omitted
  modules = adios.lib.importTree ./.;
}

Then importTree will generate:

{ adios }:

{
  name = "root";
  # Other contents omitted
  modules = {
    foo = import ./foo { inherit adios; };
    bar = import ./bar { inherit adios; };
  };
}

Notably, importModules is not recursive - the baz/ module was completely ignored. If the bar module wants to depend on another module defined within its folder, it should import those modules itself, like this:

{ adios }:

{
  name = "bar";
  # Other contents omitted
  modules = adios.lib.importModules ./.;
}

Limitations

importTree expects all modules to:

  • Either be defined as:
    • a subfolder, under $MODULE_NAME/default.nix.
    • a Nix file, under $MODULE_NAME.nix (excluding default.nix).
  • take { adios }: as the file's inputs
  • use the same name as the folder it's contained within

If your module tree doesn't follow this schema, then it's recommended to define your import logic manually. importTree is only a convenience function, and it's okay to not use it if your tree doesn't fit its schema.