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