From c4b88d4dc3044e59543e396dc979d7ba9a30767e Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Fri, 25 Jun 2021 15:54:12 -0400 Subject: [PATCH 01/11] Implement basic ADT framework --- adt.nix | 86 +++++++++++++++++++++++++++++++++++++++++++++++++++++ default.nix | 2 ++ 2 files changed, 88 insertions(+) create mode 100644 adt.nix diff --git a/adt.nix b/adt.nix new file mode 100644 index 0000000..650e555 --- /dev/null +++ b/adt.nix @@ -0,0 +1,86 @@ +let + list = import ./list.nix; + set = import ./set.nix; + + applyList = f: xs: list.foldl' (f': x: f' x) f xs; +in +{ + /* + create a new algebraic data type based on a specification. + + examples: + + > adt.new "optional" { just = [ "value" ]; nothing = null; } + > adt.new "result" { ok = 1; err = 1; } + > adt.new "pair" { make = 2; } + > adt.new "point" { make = { x = null; y = null; }; } + */ + new = name: constructors: + assert builtins.isAttrs constructors; + assert builtins.all + (spec: + builtins.any (x: x) [ + (spec == null) + (builtins.isList spec && builtins.all builtins.isString spec) + (builtins.isAttrs spec && builtins.all (x: x == null) (builtins.attrValues spec)) + (builtins.isInt spec && spec > 0) + ] + ) + (builtins.attrValues constructors); + assert builtins.all (name: name != "_tag") (builtins.attrNames constructors); + let + needsTag = builtins.length (builtins.attrNames constructors) > 1; + genNaryCtor = base: args: + let + len = builtins.length args; + go = acc: i: + if i >= len + then acc + else + let + field = builtins.elemAt args i; + in x: go (acc // { ${field} = x; }) (i + 1); + in go base 0; + makeCtor = ctorName: spec: + let + baseAttrs = if needsTag then { _tag = ctorName; } else {}; + in + if spec == null then + # nullary + baseAttrs + else if builtins.isList spec then + # positional, struct field named by string + genNaryCtor baseAttrs spec + else if builtins.isAttrs spec then + # one attrset argument, fields named by strings both ways + # TODO: could just intersectAttrs here, but wouldn't get checking that + # all keys are present + args: list.foldl' (x: y: x // { ${y} = args.${y}; }) baseAttrs (builtins.attrNames spec) + else if builtins.isInt spec then + genNaryCtor baseAttrs (list.map (n: "_${builtins.toString n}") (list.range 0 (spec - 1))) + else builtins.throw "std.adt.new: invalid constructor specification for '${ctorName}'"; + ctors = set.map makeCtor constructors; + match = + let + makeApply = _: spec: + if spec == null then + # nullary + (f: _: f) + else if builtins.isList spec then + (f: v: f (list.foldl' (x: y: x // y) {} (list.map (k: { ${k} = v.${k}; }) spec))) + else if builtins.isAttrs spec then + (f: v: f (set.map (k: _: v.${k}))) + else # int + (f: v: applyList f (list.map (k: v."_${builtins.toString k}") (list.range 0 (spec - 1)))) + ; + apply = set.map makeApply constructors; + only = as: as.${builtins.head (builtins.attrNames as)}; + in + if builtins.length (builtins.attrNames constructors) == 0 then + builtins.throw "std.adt: match on empty ADT: ${name}" + else if !needsTag then + val: matches: (only apply) (only matches) val # TODO: should this be an attrset of matches? + else + val: matches: apply.${val._tag} matches.${val._tag} val; + in { inherit match ctors; }; +} diff --git a/default.nix b/default.nix index 5ee4045..73e3338 100644 --- a/default.nix +++ b/default.nix @@ -1,4 +1,6 @@ rec { + adt = import ./adt.nix; + applicative = import ./applicative.nix; bool = import ./bool.nix; From f927d0326a548ff1056c576718455b98de449c26 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Fri, 25 Jun 2021 15:56:32 -0400 Subject: [PATCH 02/11] Add type name to ADT-generated attrs --- adt.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/adt.nix b/adt.nix index 650e555..39ac248 100644 --- a/adt.nix +++ b/adt.nix @@ -27,7 +27,7 @@ in ] ) (builtins.attrValues constructors); - assert builtins.all (name: name != "_tag") (builtins.attrNames constructors); + assert builtins.all (name: !builtins.elem name [ "_tag" "_type" ]) (builtins.attrNames constructors); let needsTag = builtins.length (builtins.attrNames constructors) > 1; genNaryCtor = base: args: @@ -43,7 +43,7 @@ in in go base 0; makeCtor = ctorName: spec: let - baseAttrs = if needsTag then { _tag = ctorName; } else {}; + baseAttrs = if needsTag then { _tag = ctorName; _type = name; } else { _type = name; }; in if spec == null then # nullary From 7737c39be2544cd040c45af2c373e403f0e44b85 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Fri, 25 Jun 2021 17:48:44 -0400 Subject: [PATCH 03/11] Fix positional args, document, make field specification clearer --- adt.nix | 155 ++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 134 insertions(+), 21 deletions(-) diff --git a/adt.nix b/adt.nix index 39ac248..0fcea0b 100644 --- a/adt.nix +++ b/adt.nix @@ -1,21 +1,125 @@ let list = import ./list.nix; set = import ./set.nix; + string = import ./string.nix; - applyList = f: xs: list.foldl' (f': x: f' x) f xs; + unique = xs: + let + xs' = builtins.sort builtins.lessThan xs; + in builtins.length xs <= 1 || list.all (x: x) (list.zipWith (x: y: x != y) xs' (list.tail xs')); in -{ - /* - create a new algebraic data type based on a specification. +rec { + fields = rec { + /* positional :: [string] -> ConstructorSpecification + + Specifies that the variant should contain the provided field names, but + the constructor and match function both expect appropriate positional + arguments. The field names are only used in the internal representation. + + > point = adt.struct "point" (adt.fields.positional ["x" "y"]) + > p = point.ctors.make 1 2 + > p + { _type = "point"; x = 1; y = 2; } + > point.match p (x: y: x + y) + 3 + */ + positional = fields: + assert builtins.isList fields && builtins.all builtins.isString fields; + assert unique fields; + fields; + + /* record :: [string] -> ConstructorSpecification + + Specifies that the variant should contain the provided field names, but + the constructor and match function both expect a single attrset argument + with the appropriate fields. + + > point = adt.struct "point" (adt.fields.record ["x" "y"]) + > p = point.ctors.make { x = 1; y = 2; } + > p + { _type = "point"; x = 1; y = 2; } + > point.match p ({ x, y }: x + y) + 3 + */ + record = fields: + assert builtins.isList fields && builtins.all builtins.isString fields; + assert unique fields; + list.fold set.monoid (list.map (x: { ${x} = null; }) fields); + + /* none :: ConstructorSpecification + + Specifies that the variant should contain no fields. The constructor will + not be a function, but a singleton attrset. The corresponding match + branch will expect a value, not a function, when matching. + + > optional = adt.enum "optional" { just = adt.fields.positional ["value"]; nothing = adt.fields.none; } + > optional.ctors.just 5 + { _tag = "just"; _type = "optional"; value = 5; } + > optional.ctors.nothing + { _tag = "nothing"; _type = "optional"; } + > optional.match optional.ctors.nothing { just = x: x + 2; nothing = 7; } + 7 + */ + none = null; + + /* anon :: int -> ConstructorSpecification + + Like `fields.positional`, but instead of providing the names for the + fields, the given number of anonymous field names are used instead. + + > point = adt.struct "point" (adt.fields.anon 2) + > p = point.ctors.make 1 2 + > p + { _type = "point"; _0 = 1; _1 = 2; } + > point.match p (x: y: x + y) + 3 + */ + anon = x: + assert builtins.isInt x && x > 0; + positional (list.map (n: "_${builtins.toString n}") (list.range 0 (x - 1))); + }; - examples: + /* struct :: string -> ConstructorSpecification -> ADT - > adt.new "optional" { just = [ "value" ]; nothing = null; } - > adt.new "result" { ok = 1; err = 1; } - > adt.new "pair" { make = 2; } - > adt.new "point" { make = { x = null; y = null; }; } + Create a new algebraic data type with one one constructor, named 'make'. + */ + struct = name: constructor: new name { make = constructor; }; + + /* enum :: string -> { string: ConstructorSpecification } -> ADT + + Create a new algebraic data type consisting of a sum type with the + constructors provided. This is simply an alias for 'new' to communicate + intention when a sum type is being created. + */ + enum = new; + + /* new :: string -> { string: ConstructorSpecification } -> ADT + + Create a new algebraic data type based on a specification of its + constructors. + + Examples: + + > adt.new "optional" { + just = adt.fields.positional [ "value" ]; + nothing = adt.fields.none; + } + + > adt.new "result" { + ok = adt.fields.anon 1; + err = adt.fields.anon 1; + } + + > adt.new "pair" { + make = adt.fields.anon 2; + } + + > adt.new "point" { + make = adt.fields.record [ "x" "y" ]; + } */ new = name: constructors: + assert builtins.isString name; assert builtins.isAttrs constructors; assert builtins.all (spec: @@ -23,7 +127,6 @@ in (spec == null) (builtins.isList spec && builtins.all builtins.isString spec) (builtins.isAttrs spec && builtins.all (x: x == null) (builtins.attrValues spec)) - (builtins.isInt spec && spec > 0) ] ) (builtins.attrValues constructors); @@ -41,6 +144,7 @@ in field = builtins.elemAt args i; in x: go (acc // { ${field} = x; }) (i + 1); in go base 0; + applyList = f: xs: list.foldl' (f': x: f' x) f xs; makeCtor = ctorName: spec: let baseAttrs = if needsTag then { _tag = ctorName; _type = name; } else { _type = name; }; @@ -56,9 +160,7 @@ in # TODO: could just intersectAttrs here, but wouldn't get checking that # all keys are present args: list.foldl' (x: y: x // { ${y} = args.${y}; }) baseAttrs (builtins.attrNames spec) - else if builtins.isInt spec then - genNaryCtor baseAttrs (list.map (n: "_${builtins.toString n}") (list.range 0 (spec - 1))) - else builtins.throw "std.adt.new: invalid constructor specification for '${ctorName}'"; + else builtins.throw "std.adt.new: invalid constructor specification for constructor ${string.escapeNixString ctorName}"; ctors = set.map makeCtor constructors; match = let @@ -67,20 +169,31 @@ in # nullary (f: _: f) else if builtins.isList spec then - (f: v: f (list.foldl' (x: y: x // y) {} (list.map (k: { ${k} = v.${k}; }) spec))) - else if builtins.isAttrs spec then - (f: v: f (set.map (k: _: v.${k}))) - else # int - (f: v: applyList f (list.map (k: v."_${builtins.toString k}") (list.range 0 (spec - 1)))) + (f: v: applyList f (list.map (k: v.${k}) spec)) + else # attrs + (f: v: f (set.map (k: _: v.${k}) spec)) ; apply = set.map makeApply constructors; only = as: as.${builtins.head (builtins.attrNames as)}; in if builtins.length (builtins.attrNames constructors) == 0 then - builtins.throw "std.adt: match on empty ADT: ${name}" + builtins.throw "std.adt: match on empty ADT: ${string.escapeNixString name}" else if !needsTag then - val: matches: (only apply) (only matches) val # TODO: should this be an attrset of matches? + val: matches: + let + matcher = + if builtins.isAttrs matches then + only matches val + else if builtins.isFunction matches then + matches + else + builtins.throw "std.adt: expected function or attrset for matcher on ${string.escapeNixString name}"; + in (only apply) matcher val else - val: matches: apply.${val._tag} matches.${val._tag} val; + val: matches: + if builtins.isAttrs matches then + apply.${val._tag} matches.${val._tag} val + else + builtins.throw "std.adt: expected attrset for matcher on ${string.escapeNixString name}"; in { inherit match ctors; }; } From 1bc0a3b42e7026786e48ae45d0d5d04669554a10 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 03:29:50 -0400 Subject: [PATCH 04/11] Implement typechecking of ADTs and their fields --- adt.nix | 103 ++++++++++++++++++++++++++++++++++++++++++++---------- types.nix | 6 ++++ 2 files changed, 91 insertions(+), 18 deletions(-) diff --git a/adt.nix b/adt.nix index 0fcea0b..9ac43d2 100644 --- a/adt.nix +++ b/adt.nix @@ -2,6 +2,7 @@ let list = import ./list.nix; set = import ./set.nix; string = import ./string.nix; + types = import ./types.nix; unique = xs: let @@ -10,13 +11,13 @@ let in rec { fields = rec { - /* positional :: [string] -> ConstructorSpecification + /* positional :: [{ name: string, type: type }] -> ConstructorSpecification Specifies that the variant should contain the provided field names, but the constructor and match function both expect appropriate positional arguments. The field names are only used in the internal representation. - > point = adt.struct "point" (adt.fields.positional ["x" "y"]) + > point = adt.struct "point" (adt.fields.positional [{ name = "x"; type = types.float; } { name = "y"; type = types.float; }]) > p = point.ctors.make 1 2 > p { _type = "point"; x = 1; y = 2; } @@ -24,17 +25,32 @@ rec { 3 */ positional = fields: + assert builtins.isList fields + && builtins.all + (f: + builtins.isAttrs f + && f ? name && builtins.isString f.name + && f ? type + ) + fields; + fields; + + /* positional_ :: [string] -> ConstructorSpecification + + Like `fields.positional`, but the fields are untyped. + */ + positional_ = fields: assert builtins.isList fields && builtins.all builtins.isString fields; assert unique fields; - fields; + positional (list.map (f: { name = f; type = types.any; }) fields); - /* record :: [string] -> ConstructorSpecification + /* record :: { string: type } -> ConstructorSpecification Specifies that the variant should contain the provided field names, but the constructor and match function both expect a single attrset argument with the appropriate fields. - > point = adt.struct "point" (adt.fields.record ["x" "y"]) + > point = adt.struct "point" (adt.fields.record { x = types.float; y = types.float; }) > p = point.ctors.make { x = 1; y = 2; } > p { _type = "point"; x = 1; y = 2; } @@ -42,9 +58,17 @@ rec { 3 */ record = fields: + assert builtins.isAttrs fields; + fields; + + /* record_ :: [string] -> ConstructorSpecification + + Like `fields.record`, but the fields are untyped. + */ + record_ = fields: assert builtins.isList fields && builtins.all builtins.isString fields; assert unique fields; - list.fold set.monoid (list.map (x: { ${x} = null; }) fields); + list.fold set.monoid (list.map (x: { ${x} = types.any; }) fields); /* none :: ConstructorSpecification @@ -52,7 +76,7 @@ rec { not be a function, but a singleton attrset. The corresponding match branch will expect a value, not a function, when matching. - > optional = adt.enum "optional" { just = adt.fields.positional ["value"]; nothing = adt.fields.none; } + > optional = adt.enum "optional" { just = adt.fields.positional_ ["value"]; nothing = adt.fields.none; } > optional.ctors.just 5 { _tag = "just"; _type = "optional"; value = 5; } > optional.ctors.nothing @@ -62,21 +86,29 @@ rec { */ none = null; - /* anon :: int -> ConstructorSpecification + /* anon :: [type] -> ConstructorSpecification Like `fields.positional`, but instead of providing the names for the fields, the given number of anonymous field names are used instead. - > point = adt.struct "point" (adt.fields.anon 2) + > point = adt.struct "point" (adt.fields.anon [types.float types.float]) > p = point.ctors.make 1 2 > p { _type = "point"; _0 = 1; _1 = 2; } > point.match p (x: y: x + y) 3 */ - anon = x: + anon = types: + assert builtins.isList types; + positional (list.imap (i: t: { name = "_${builtins.toString i}"; type = t; }) types); + + /* anon_ :: int -> ConstructorSpecification + + Like `fields.anon`, but the fields are untyped. + */ + anon_ = x: assert builtins.isInt x && x > 0; - positional (list.map (n: "_${builtins.toString n}") (list.range 0 (x - 1))); + positional_ (list.map (n: "_${builtins.toString n}") (list.range 0 (x - 1))); }; /* struct :: string -> ConstructorSpecification -> ADT @@ -125,13 +157,24 @@ rec { (spec: builtins.any (x: x) [ (spec == null) - (builtins.isList spec && builtins.all builtins.isString spec) - (builtins.isAttrs spec && builtins.all (x: x == null) (builtins.attrValues spec)) + ( + builtins.isList spec + && + builtins.all + (f: + builtins.isAttrs f + && f ? name && builtins.isString f.name + && f ? type + ) + spec + ) + (builtins.isAttrs spec) ] ) (builtins.attrValues constructors); assert builtins.all (name: !builtins.elem name [ "_tag" "_type" ]) (builtins.attrNames constructors); let + only = as: as.${builtins.head (builtins.attrNames as)}; needsTag = builtins.length (builtins.attrNames constructors) > 1; genNaryCtor = base: args: let @@ -142,7 +185,7 @@ rec { else let field = builtins.elemAt args i; - in x: go (acc // { ${field} = x; }) (i + 1); + in x: go (acc // { ${field.name} = x; }) (i + 1); in go base 0; applyList = f: xs: list.foldl' (f': x: f' x) f xs; makeCtor = ctorName: spec: @@ -160,7 +203,8 @@ rec { # TODO: could just intersectAttrs here, but wouldn't get checking that # all keys are present args: list.foldl' (x: y: x // { ${y} = args.${y}; }) baseAttrs (builtins.attrNames spec) - else builtins.throw "std.adt.new: invalid constructor specification for constructor ${string.escapeNixString ctorName}"; + else + builtins.throw "std.adt.new: invalid constructor specification for constructor ${string.escapeNixString ctorName}"; ctors = set.map makeCtor constructors; match = let @@ -169,12 +213,11 @@ rec { # nullary (f: _: f) else if builtins.isList spec then - (f: v: applyList f (list.map (k: v.${k}) spec)) + (f: v: applyList f (list.map (k: v.${k.name}) spec)) else # attrs (f: v: f (set.map (k: _: v.${k}) spec)) ; apply = set.map makeApply constructors; - only = as: as.${builtins.head (builtins.attrNames as)}; in if builtins.length (builtins.attrNames constructors) == 0 then builtins.throw "std.adt: match on empty ADT: ${string.escapeNixString name}" @@ -195,5 +238,29 @@ rec { apply.${val._tag} matches.${val._tag} val else builtins.throw "std.adt: expected attrset for matcher on ${string.escapeNixString name}"; - in { inherit match ctors; }; + check = + let + makeCtorCheck = ctorName: spec: + if spec == null then + _: true + else if builtins.isList spec then + t: builtins.all ({ name, type }: builtins.hasAttr name t && type.check t.${name}) spec + else if builtins.isAttrs spec then + t: builtins.all (name: builtins.hasAttr name t && spec.${name}.check t.${name}) (builtins.attrNames spec) + else + builtins.throw "std.adt.new: invalid constructor specification for constructor ${string.escapeNixString ctorName}" + ; + ctorChecks = set.map makeCtorCheck constructors; + in t: + (t ? _type && t._type == name) + && + ( + if needsTag then + t ? _tag + && builtins.elem t._tag (builtins.attrNames constructors) + && ctorChecks.${t._tag} t + else + (only ctorChecks) t + ); + in { inherit match check ctors; }; } diff --git a/types.nix b/types.nix index 48d0b90..671fdeb 100644 --- a/types.nix +++ b/types.nix @@ -94,6 +94,12 @@ rec { inherit name check description show; }; + any = { + name = "any"; + check = _: true; + description = "any value"; + }; + bool = mkType { name = "bool"; description = "boolean"; From edbc22fdd25ff9a192ccbc8125c0a9e43d7cafbc Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 03:36:39 -0400 Subject: [PATCH 05/11] Correct adt.fields docs --- adt.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adt.nix b/adt.nix index 9ac43d2..17301f5 100644 --- a/adt.nix +++ b/adt.nix @@ -17,7 +17,7 @@ rec { the constructor and match function both expect appropriate positional arguments. The field names are only used in the internal representation. - > point = adt.struct "point" (adt.fields.positional [{ name = "x"; type = types.float; } { name = "y"; type = types.float; }]) + > point = adt.struct "point" (adt.fields.positional [{ name = "x"; type = types.int; } { name = "y"; type = types.int; }]) > p = point.ctors.make 1 2 > p { _type = "point"; x = 1; y = 2; } @@ -50,7 +50,7 @@ rec { the constructor and match function both expect a single attrset argument with the appropriate fields. - > point = adt.struct "point" (adt.fields.record { x = types.float; y = types.float; }) + > point = adt.struct "point" (adt.fields.record { x = types.int; y = types.int; }) > p = point.ctors.make { x = 1; y = 2; } > p { _type = "point"; x = 1; y = 2; } @@ -91,7 +91,7 @@ rec { Like `fields.positional`, but instead of providing the names for the fields, the given number of anonymous field names are used instead. - > point = adt.struct "point" (adt.fields.anon [types.float types.float]) + > point = adt.struct "point" (adt.fields.anon [types.int types.int]) > p = point.ctors.make 1 2 > p { _type = "point"; _0 = 1; _1 = 2; } From 97df9456e1e44a9ba815f7e84fdbdc5ad1874109 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 11:50:30 -0400 Subject: [PATCH 06/11] Fix docs on adt.new --- adt.nix | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/adt.nix b/adt.nix index 17301f5..549ce91 100644 --- a/adt.nix +++ b/adt.nix @@ -133,21 +133,21 @@ rec { Examples: > adt.new "optional" { - just = adt.fields.positional [ "value" ]; + just = adt.fields.positional_ [ "value" ]; nothing = adt.fields.none; } > adt.new "result" { - ok = adt.fields.anon 1; - err = adt.fields.anon 1; + ok = adt.fields.anon_ 1; + err = adt.fields.anon_ 1; } > adt.new "pair" { - make = adt.fields.anon 2; + make = adt.fields.anon_ 2; } > adt.new "point" { - make = adt.fields.record [ "x" "y" ]; + make = adt.fields.record_ [ "x" "y" ]; } */ new = name: constructors: From 8a4e00febae0229939fd32aebd73c14315e820d1 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 11:55:54 -0400 Subject: [PATCH 07/11] Allow values for matches on types with cardinality 1 --- adt.nix | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/adt.nix b/adt.nix index 549ce91..fb1443d 100644 --- a/adt.nix +++ b/adt.nix @@ -230,8 +230,10 @@ rec { else if builtins.isFunction matches then matches else - builtins.throw "std.adt: expected function or attrset for matcher on ${string.escapeNixString name}"; - in (only apply) matcher val + builtins.throw "std.adt: invalid matcher for ${string.escapeNixString name}"; + in if only constructors == null + then matches + else (only apply) matcher val else val: matches: if builtins.isAttrs matches then From 92c58228830aad6374a7a7c2a99de7cf4690d45f Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 12:03:47 -0400 Subject: [PATCH 08/11] Fix pattern matching on single-constructor types using sets --- adt.nix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/adt.nix b/adt.nix index fb1443d..c9afbf5 100644 --- a/adt.nix +++ b/adt.nix @@ -226,7 +226,7 @@ rec { let matcher = if builtins.isAttrs matches then - only matches val + only matches else if builtins.isFunction matches then matches else From 24d190d9861abd10b11388be5a5c2a0b0f6e0791 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 12:04:56 -0400 Subject: [PATCH 09/11] Require attrsets as matchers on single-constructor types If we optionally allow providing a function as a matcher on a single-constructor type, then it follows that we would allow a value for matching on a unary type as well: ``` > unit = adt.struct "unit" adt.fields.none > unit.match unit.ctors.make 5 5 ``` However, this would make using the long-form attrset for matching on the same type ambiguous: ``` > unit = adt.struct "unit" adt.fields.none > unit.match unit.ctors.make { make = 5; } \# should this be `5` or `{ make = 5; }`? ``` To prevent this, this switches back to requiring attrsets to avoid ambiguity. --- adt.nix | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/adt.nix b/adt.nix index c9afbf5..79b67d6 100644 --- a/adt.nix +++ b/adt.nix @@ -227,13 +227,9 @@ rec { matcher = if builtins.isAttrs matches then only matches - else if builtins.isFunction matches then - matches else - builtins.throw "std.adt: invalid matcher for ${string.escapeNixString name}"; - in if only constructors == null - then matches - else (only apply) matcher val + builtins.throw "std.adt: expected attrset for matcher on ${string.escapeNixString name}"; + in (only apply) matcher val else val: matches: if builtins.isAttrs matches then From 58dc4555be1e01c79305df9b6e2dd6502dd623b6 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Sun, 27 Jun 2021 12:15:51 -0400 Subject: [PATCH 10/11] Remove match shorthand from adt docs --- adt.nix | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adt.nix b/adt.nix index 79b67d6..721d25d 100644 --- a/adt.nix +++ b/adt.nix @@ -21,7 +21,7 @@ rec { > p = point.ctors.make 1 2 > p { _type = "point"; x = 1; y = 2; } - > point.match p (x: y: x + y) + > point.match p { make = x: y: x + y; } 3 */ positional = fields: @@ -54,7 +54,7 @@ rec { > p = point.ctors.make { x = 1; y = 2; } > p { _type = "point"; x = 1; y = 2; } - > point.match p ({ x, y }: x + y) + > point.match p { make = { x, y }: x + y; } 3 */ record = fields: @@ -95,7 +95,7 @@ rec { > p = point.ctors.make 1 2 > p { _type = "point"; _0 = 1; _1 = 2; } - > point.match p (x: y: x + y) + > point.match p { make x: y: x + y; } 3 */ anon = types: From 04ef411af2fd56bedc65db1ed7f8949a8e7f3036 Mon Sep 17 00:00:00 2001 From: Nicole Prindle Date: Thu, 4 Nov 2021 22:08:46 -0400 Subject: [PATCH 11/11] Add option to define all unary variants in 'enum' --- adt.nix | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/adt.nix b/adt.nix index 721d25d..b54e539 100644 --- a/adt.nix +++ b/adt.nix @@ -117,13 +117,22 @@ rec { */ struct = name: constructor: new name { make = constructor; }; - /* enum :: string -> { string: ConstructorSpecification } -> ADT + /* enum :: string -> { string: ConstructorSpecification } | [ string ] -> ADT - Create a new algebraic data type consisting of a sum type with the - constructors provided. This is simply an alias for 'new' to communicate - intention when a sum type is being created. + Create a new algebraic data type, with the provided constructors acting as + variants of the sum type. The constructor specification, if an attribute + set, has the same format as 'adt.new'. If the constructor specification is + a list of strings, each constructor will be assumed to be unary; that is, + each will be specified to 'adt.fields.none'. */ - enum = new; + enum = name: ctors: + if builtins.isList ctors then + assert builtins.all builtins.isString ctors; + let + toUnary = ctorName: { "${ctorName}" = fields.none; }; + in new name (list.fold set.monoid (list.map toUnary ctors)) + else + new name ctors; /* new :: string -> { string: ConstructorSpecification } -> ADT