Slightly prettier error message etc. Change-Id: I9af4ec6ce337c65b5ca0820dc15a4e084832c529 Reviewed-on: https://cl.tvl.fyi/c/depot/+/3542 Tested-by: BuildkiteCI Reviewed-by: tazjin <mail@tazj.in>
		
			
				
	
	
		
			162 lines
		
	
	
	
		
			4.5 KiB
		
	
	
	
		
			Nix
		
	
	
	
	
	
			
		
		
	
	
			162 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 != 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
 | ||
|      tagName
 | ||
|      tagValue
 | ||
|      discr
 | ||
|      discrDef
 | ||
|      match
 | ||
|      matchLam
 | ||
|      tests
 | ||
|      ;
 | ||
| }
 |