tvl-depot/nix/tag/default.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

163 lines
4.5 KiB
Nix
Raw Normal View History

{ 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 lib.assertMsg res.isTag 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 != null)
"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 variants 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
;
}