Most of the ecosystem has moved to this formatter, and many people configured their editors to autoformat it with this formatter. Closes: https://git.snix.dev/snix/snix/issues/62 Change-Id: Icf39e7836c91fc2ae49fbe22a40a639105bfb0bd Reviewed-on: https://cl.snix.dev/c/snix/+/30671 Reviewed-by: Florian Klink <flokli@flokli.de> Tested-by: besadii Autosubmit: Ilan Joselevich <personal@ilanjoselevich.com>
182 lines
4.5 KiB
Nix
182 lines
4.5 KiB
Nix
{ depot, lib, ... }:
|
||
let
|
||
# Takes a tag, checks whether it is an attrset with one element,
|
||
# if so sets `isTag` to `true` and sets the name and value.
|
||
# If not, sets `isTag` to `false` and sets `errmsg`.
|
||
verifyTag =
|
||
tag:
|
||
let
|
||
cases = builtins.attrNames tag;
|
||
len = builtins.length cases;
|
||
in
|
||
if builtins.length cases == 1 then
|
||
let
|
||
name = builtins.head cases;
|
||
in
|
||
{
|
||
isTag = true;
|
||
name = name;
|
||
val = tag.${name};
|
||
errmsg = null;
|
||
}
|
||
else
|
||
{
|
||
isTag = false;
|
||
errmsg = (
|
||
"match: an instance of a sum is an attrset "
|
||
+ "with exactly one element, yours had ${toString len}"
|
||
+ ", namely: ${lib.generators.toPretty { } cases}"
|
||
);
|
||
name = null;
|
||
val = null;
|
||
};
|
||
|
||
# Returns the tag name of a given tag attribute set.
|
||
# Throws if the tag is invalid.
|
||
#
|
||
# Type: tag -> string
|
||
tagName = tag: (assertIsTag tag).name;
|
||
|
||
# Returns the tagged value of a given tag attribute set.
|
||
# Throws if the tag is invalid.
|
||
#
|
||
# Type: tag -> any
|
||
tagValue = tag: (assertIsTag tag).val;
|
||
|
||
# like `verifyTag`, but throws the error message if it is not a tag.
|
||
assertIsTag =
|
||
tag:
|
||
let
|
||
res = verifyTag tag;
|
||
in
|
||
assert res.isTag || throw res.errmsg;
|
||
{
|
||
inherit (res) name val;
|
||
};
|
||
|
||
# Discriminator for values.
|
||
# Goes through a list of tagged predicates `{ <tag> = <pred>; }`
|
||
# and returns the value inside the tag
|
||
# for which the first predicate applies, `{ <tag> = v; }`.
|
||
# They can then later be matched on with `match`.
|
||
#
|
||
# `defTag` is the tag that is assigned if there is no match.
|
||
#
|
||
# Examples:
|
||
# discrDef "smol" [
|
||
# { biggerFive = i: i > 5; }
|
||
# { negative = i: i < 0; }
|
||
# ] (-100)
|
||
# => { negative = -100; }
|
||
# discrDef "smol" [
|
||
# { biggerFive = i: i > 5; }
|
||
# { negative = i: i < 0; }
|
||
# ] 1
|
||
# => { smol = 1; }
|
||
discrDef =
|
||
defTag: fs: v:
|
||
let
|
||
res = lib.findFirst (t: t.val v) null (map assertIsTag fs);
|
||
in
|
||
if res == null then { ${defTag} = v; } else { ${res.name} = v; };
|
||
|
||
# Like `discrDef`, but fail if there is no match.
|
||
discr =
|
||
fs: v:
|
||
let
|
||
res = discrDef null fs v;
|
||
in
|
||
assert lib.assertMsg (
|
||
res != { }
|
||
) "tag.discr: No predicate found that matches ${lib.generators.toPretty { } v}";
|
||
res;
|
||
|
||
# The canonical pattern matching primitive.
|
||
# A sum value is an attribute set with one element,
|
||
# whose key is the name of the variant and
|
||
# whose value is the content of the variant.
|
||
# `matcher` is an attribute set which enumerates
|
||
# all possible variants as keys and provides a function
|
||
# which handles each variant’s content.
|
||
# You should make an effort to return values of the same
|
||
# type in your matcher, or new sums.
|
||
#
|
||
# Example:
|
||
# let
|
||
# success = { res = 42; };
|
||
# failure = { err = "no answer"; };
|
||
# matcher = {
|
||
# res = i: i + 1;
|
||
# err = _: 0;
|
||
# };
|
||
# in
|
||
# match success matcher == 43
|
||
# && match failure matcher == 0;
|
||
#
|
||
match =
|
||
sum: matcher:
|
||
let
|
||
cases = builtins.attrNames sum;
|
||
in
|
||
assert
|
||
let
|
||
len = builtins.length cases;
|
||
in
|
||
lib.assertMsg (len == 1) (
|
||
"match: an instance of a sum is an attrset "
|
||
+ "with exactly one element, yours had ${toString len}"
|
||
+ ", namely: ${lib.generators.toPretty { } cases}"
|
||
);
|
||
let
|
||
case = builtins.head cases;
|
||
in
|
||
assert lib.assertMsg (matcher ? ${case}) (
|
||
"match: \"${case}\" is not a valid case of this sum, "
|
||
+ "the matcher accepts: ${lib.generators.toPretty { } (builtins.attrNames matcher)}"
|
||
);
|
||
matcher.${case} sum.${case};
|
||
|
||
# A `match` with the arguments flipped.
|
||
# “Lam” stands for “lambda”, because it can be used like the
|
||
# `\case` LambdaCase statement in Haskell, to create a curried
|
||
# “matcher” function ready to take a value.
|
||
#
|
||
# Example:
|
||
# lib.pipe { foo = 42; } [
|
||
# (matchLam {
|
||
# foo = i: if i < 23 then { small = i; } else { big = i; };
|
||
# bar = _: { small = 5; };
|
||
# })
|
||
# (matchLam {
|
||
# small = i: "yay it was small";
|
||
# big = i: "whoo it was big!";
|
||
# })
|
||
# ]
|
||
# => "whoo it was big!";
|
||
matchLam = matcher: sum: match sum matcher;
|
||
|
||
tests = import ./tests.nix {
|
||
inherit
|
||
depot
|
||
lib
|
||
verifyTag
|
||
discr
|
||
discrDef
|
||
match
|
||
matchLam
|
||
;
|
||
};
|
||
|
||
in
|
||
{
|
||
inherit
|
||
verifyTag
|
||
tagName
|
||
tagValue
|
||
discr
|
||
discrDef
|
||
match
|
||
matchLam
|
||
tests
|
||
;
|
||
}
|