From argument patterns to pattern matching

With |> already in use in Nix, I’ve been thinking about another small language extension.

Nix already has partial functions at the argument-matching level: { a, ... }: ... accepts some arguments and rejects others. Adding just two trivial builtins would let us compose these into pattern matching.

1. builtins.functionDefinedAt: Ask whether a function accepts an argument

Introduce builtins.functionDefinedAt f x, which checks the argument pattern without evaluating the body:

functionDefinedAt ({ name, ... }: name) {}                    # false
functionDefinedAt ({ name, ... }: name) { name = "Alice"; }   # true
functionDefinedAt ({ name, ... }: name) { name = throw "x"; } # true
functionDefinedAt (x: throw "not called") 42                  # true

This is about matching the first argument, not predicting whether the function will evaluate successfully. Defaults and unused fields should remain lazy.

For callable sets, we could use a convention: an optional __defined = x: ... predicate describes their domain.

2. builtins.functionOrElse: Combine partial functions

Introduce builtins.functionOrElse f g: try f if its argument matches; otherwise use g.

Using the proposed domain check, the combinator itself can be written in pure Nix:

functionOrElse = f: g: {
  __functor = self: x:
    if functionDefinedAt f x then f x else g x;

  __defined = x:
    functionDefinedAt f x || functionDefinedAt g x;
};

Order matters: the first matching branch wins. An error inside a selected body remains an error; it does not cause fall-through.

(If checking and then destructuring the argument again looks wasteful, an apply-or-fallback primitive, with an __applyOrElse hook on functors, can do the matching once - the idea is from Scala’s PartialFunction. The same applies to guards described below.)

3. Give it an operator

Let f | g mean functionOrElse f g. Combined with |>, this gives us pretty syntax:

dependency |>
    { package, output }: package.${output}
  | { package }: package
  | package: package

Here a dependency can be a package, a wrapper around a package, or a request for a particular output.

The same branches can be used wherever a function is expected:

builtins.map (
    { file, line, ... }: "${file}:${toString line}"
  | { file, ... }: file
  | _: "<unknown>"
) locations

| would delimit function branches and bind more tightly than the pipe operators. Branches remain ordinary function values: they can be named, passed around, or assembled elsewhere.

4. Optionally, add guards and more sugar

A guard extends the applicability check. It could also express preconditions on package functions while keeping their dependency arguments visible to callPackage:

{ stdenv, ... } if stdenv.hostPlatform.isLinux: ...

Literal parameters would just be sugar for equality guards. For example, a build option selecting serial or parallel make:

makeFlags = jobs |>
    1: []
  | n if builtins.isInt n && n > 1: [ "-j${toString n}" ]
  | _: throw "jobs must be a positive integer";

Guards would participate in functionDefinedAt: a false guard means the argument does not match. Errors in the guard itself still propagate.

Optional type annotations could also desugar to the same guards. For example, a possible spelling:

(x :: Int): x + 1

could mean:

x if builtins.isInt x: x + 1
6 Likes

Sounds like good stuff, although I would caution against expanding the language too much before we have a proper langref. Do you have a prototype to play with?

1 Like