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
implfunction 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(excludingdefault.nix).
- a subfolder, under
- 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.