about summary refs log blame commit diff
path: root/nix/tag/default.nix
blob: 4fe589825848a8c1512388745fafa92465c166b4 (plain) (tree)

























                                                                  
                                                                      

















                                                                  

                             





































































































                                                                                     
{ 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;
    };

  # 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 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
     assertIsTag
     discr
     discrDef
     match
     matchLam
     tests
     ;
}