From 3fa0857261815b88ad09530ed63a32b588dc9c39 Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Tue, 23 Jan 2024 02:11:59 +0000 Subject: [PATCH 01/10] Add constrained types RFC --- text/0000-constrained-types.md | 150 +++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 text/0000-constrained-types.md diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md new file mode 100644 index 00000000..8d391c7f --- /dev/null +++ b/text/0000-constrained-types.md @@ -0,0 +1,150 @@ +- Feature Name: Constrained Types +- Start Date: 2024-01-22 +- RFC PR: +- Pony Issue: + +# Summary + +Add library to the standard library to make it easy to express types that are constrained versions of other types. + +# Motivation + +We often want to take a basic type and apply constraints to it. For example, we want to represent a range of values from 0 to 9 as being valid and disallow others. + +A common approach to doing this is to create a class that wraps our type and only allows the class to be created if the constraints are met. I believe it would be nice to include a way to participate in this common pattern in the standard library. + +By providing an approved mechanism in the standard library, we can demonstrate to Pony user's how to encode our constrained types within the type system. + +# Detailed design + +The entire standard library addition can be included in a single file in a new package. + +```pony +type ValidationResult is (Validated | Error) + +primitive Validated + +class Error + let _errors: Array[String val] = _errors.create() + + new create(e: (String val | None) = None) => + match e + | let s: String val => _errors.push(s) + end + + fun ref apply(e: String val) => + _errors.push(e) + + fun ref errors(): Array[String val] => + _errors + +interface val Validator[T] + new val create() + fun apply(i: T): ValidationResult + +class val Valid[T: Any val, F: Validator[T]] + let _value: T val + + new val _create(value: T val) => + _value = value + + fun val apply(): T val => + _value + +primitive ValidConstructor[T: Any val, F: Validator[T] val] + fun apply(value: T): (Valid[T, F] | Error) => + match F(value) + | Validated => Valid[T, F]._create(value) + | let e: Error => e + end +``` + +The library could be used thusly: + +```pony +type LessThan10 is Valid[U64, LessThan10Validator] +type MakeLessThan10 is ValidConstructor[U64, LessThan10Validator] + +primitive LessThan10Validator is Validator[U64] + fun apply(i: U64): ValidationResult => + if i < 10 then + Validated + else + let s: String val = i.string() + " isn't less than 10" + Error(s) + end + +actor Main + new create(env: Env) => + let prints = MakeLessThan10(U64(10)) + match prints + | let p: LessThan10 => Foo(env.out, p).go() + | let e: Error => + for s in e.errors().values() do + env.err.print(s) + end + end + +actor Foo + let out: OutStream + var left: U64 + + new create(out': OutStream, prints: LessThan10) => + out = out' + left = prints() + + be go() => + if left > 0 then + out.print(left.string()) + left = left - 1 + go() + end +``` + +In our example usage code, we are creating a constrained type `LessThan10` that enforces that the value is between 0 and 9. + +# How We Teach This + +There's a few areas of teaching. One, the package documentation should have a couple of "here's how to use" examples with explanation as well as an explanation of why you would want to use the package instead of say checking a U64 repeatedly for being `< 10`. + +Additionally, each "class" in the package should have documentation as well as each method. + +Finally, I think it makes sense to add a Pony Pattern that highlites constrained types (under a domain modeling section) and points to usage of this new package. + +# How We Test This + +There's not a lot here to test. I think it is reasonable to construct a few scenarios like "LessThan10", "ASCIILettersString", and "SmallString" that create a few different constraints and verify that we can't break those constraints. + +It would be difficult for someone to accidentally break the relatively simple library. A few unit-tests of functionality as detailed above should be fine. + +# Drawbacks + +Adding this to the standard library means that if we decide we want to change it, it will take longer to iterate vs having this as a ponylang organization library. + +# Alternatives + +Re where this lives: + +Make this a ponylang organization library. I personally want this functionality and will create the library and maintain under the organization if we decided to not include in the standard library. + +Re design: + +I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making Error generic over the the error representation and use something like `Array[A]`. + +Re names: + +I'm generally happy with the names, but I would entertain that `Validated` instead of `Valid` reads better. So: + +```pony +type LessThan10 is Validated[U64, LessThan10Validator] +``` + +instead of: + +```pony +type LessThan10 is Valid[U64, LessThan10Validator] +``` + +# Unresolved questions + +The package needs a name. I'm thinking `constrained_types` or `constrained`. From 1690354e2ee3b8105c6037ef5d6527eb3af6109c Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Tue, 23 Jan 2024 04:15:02 +0000 Subject: [PATCH 02/10] Update Error to be immutable --- text/0000-constrained-types.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 8d391c7f..15ca85a3 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -24,7 +24,7 @@ type ValidationResult is (Validated | Error) primitive Validated -class Error +class val Error let _errors: Array[String val] = _errors.create() new create(e: (String val | None) = None) => @@ -35,7 +35,7 @@ class Error fun ref apply(e: String val) => _errors.push(e) - fun ref errors(): Array[String val] => + fun ref errors(): this->Array[String val] => _errors interface val Validator[T] @@ -67,12 +67,14 @@ type MakeLessThan10 is ValidConstructor[U64, LessThan10Validator] primitive LessThan10Validator is Validator[U64] fun apply(i: U64): ValidationResult => - if i < 10 then - Validated - else - let s: String val = i.string() + " isn't less than 10" - Error(s) + recover val + if i < 10 then + Validated + else + let s: String val = i.string() + " isn't less than 10" + Error(s) end + end actor Main new create(env: Env) => From 4e294a23c23c6175770eb1876b4d4b6aeeb4575d Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Tue, 23 Jan 2024 04:25:39 +0000 Subject: [PATCH 03/10] Updates --- text/0000-constrained-types.md | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 15ca85a3..0ed25e26 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -105,6 +105,20 @@ actor Foo In our example usage code, we are creating a constrained type `LessThan10` that enforces that the value is between 0 and 9. +Some key points from the design: + +## We can only validate immutable objects + +Validating a mutable item is pointless as it could change and go outside of our constraints after it has been validated. All validated items must be immutable. + +## `Error` is immutable + +We don't want to allow error mesages to be changed after validation is done. Because everything being validated is sendable, we can wrap an entire validator in a `recover` block and build up error messages on a `ref` Error before lifting to `val`. + +## Validators are not composable + +There's no safe way with the Pony type system that I can see to make a Validator composable. You can say for example that `SmallRange` is `GreaterThan5 & LessThan10` and then use a `SmallRange` where one a `LessThan10` is called for. + # How We Teach This There's a few areas of teaching. One, the package documentation should have a couple of "here's how to use" examples with explanation as well as an explanation of why you would want to use the package instead of say checking a U64 repeatedly for being `< 10`. @@ -125,15 +139,17 @@ Adding this to the standard library means that if we decide we want to change it # Alternatives -Re where this lives: +## Where this lives: Make this a ponylang organization library. I personally want this functionality and will create the library and maintain under the organization if we decided to not include in the standard library. -Re design: +## `Error` collection type I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making Error generic over the the error representation and use something like `Array[A]`. -Re names: +Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `Error` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `Error` would itself remain unchanged. + +## Names I'm generally happy with the names, but I would entertain that `Validated` instead of `Valid` reads better. So: @@ -147,6 +163,8 @@ instead of: type LessThan10 is Valid[U64, LessThan10Validator] ``` +If we made that change then `ValidConstructor` should become `ValidatedConstructor`. + # Unresolved questions The package needs a name. I'm thinking `constrained_types` or `constrained`. From c339244e1709e85505b9c01d122b70f8525ffb37 Mon Sep 17 00:00:00 2001 From: Sean T Allen Date: Tue, 23 Jan 2024 13:41:40 -0500 Subject: [PATCH 04/10] Update text/0000-constrained-types.md Co-authored-by: Ryan A. Hagenson --- text/0000-constrained-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 0ed25e26..487e1553 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -13,7 +13,7 @@ We often want to take a basic type and apply constraints to it. For example, we A common approach to doing this is to create a class that wraps our type and only allows the class to be created if the constraints are met. I believe it would be nice to include a way to participate in this common pattern in the standard library. -By providing an approved mechanism in the standard library, we can demonstrate to Pony user's how to encode our constrained types within the type system. +By providing an approved mechanism in the standard library, we can demonstrate to Pony users how to encode our constrained types within the type system. # Detailed design From f709ce577053f66fbf5d3c25566df67215c76c2b Mon Sep 17 00:00:00 2001 From: Sean T Allen Date: Tue, 23 Jan 2024 13:41:52 -0500 Subject: [PATCH 05/10] Update text/0000-constrained-types.md Co-authored-by: Ryan A. Hagenson --- text/0000-constrained-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 487e1553..bffa27d3 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -117,7 +117,7 @@ We don't want to allow error mesages to be changed after validation is done. Bec ## Validators are not composable -There's no safe way with the Pony type system that I can see to make a Validator composable. You can say for example that `SmallRange` is `GreaterThan5 & LessThan10` and then use a `SmallRange` where one a `LessThan10` is called for. +There's no safe way with the Pony type system that I can see to make a `Validator` composable. You can say for example that `SmallRange` is `GreaterThan5 & LessThan10` and then use a `SmallRange` where one a `LessThan10` is called for. # How We Teach This From 111d2f4101669575bd8cd170ad0cabfd8b9da0be Mon Sep 17 00:00:00 2001 From: Sean T Allen Date: Tue, 23 Jan 2024 13:42:00 -0500 Subject: [PATCH 06/10] Update text/0000-constrained-types.md Co-authored-by: Ryan A. Hagenson --- text/0000-constrained-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index bffa27d3..8dec68f9 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -121,7 +121,7 @@ There's no safe way with the Pony type system that I can see to make a `Validato # How We Teach This -There's a few areas of teaching. One, the package documentation should have a couple of "here's how to use" examples with explanation as well as an explanation of why you would want to use the package instead of say checking a U64 repeatedly for being `< 10`. +There's a few areas of teaching. One, the package documentation should have a couple of "here's how to use" examples with explanation as well as an explanation of why you would want to use the package instead of say checking a `U64` repeatedly for being `< 10`. Additionally, each "class" in the package should have documentation as well as each method. From 3fd966e0b94527a9a6053ff3f842580575abe147 Mon Sep 17 00:00:00 2001 From: Sean T Allen Date: Tue, 23 Jan 2024 13:42:06 -0500 Subject: [PATCH 07/10] Update text/0000-constrained-types.md Co-authored-by: Ryan A. Hagenson --- text/0000-constrained-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 8dec68f9..db779da6 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -125,7 +125,7 @@ There's a few areas of teaching. One, the package documentation should have a co Additionally, each "class" in the package should have documentation as well as each method. -Finally, I think it makes sense to add a Pony Pattern that highlites constrained types (under a domain modeling section) and points to usage of this new package. +Finally, I think it makes sense to add a Pony Pattern that highlights constrained types (under a domain modeling section) and points to usage of this new package. # How We Test This From df274f0f190721bbbf135f9608aa528bda734d12 Mon Sep 17 00:00:00 2001 From: Sean T Allen Date: Tue, 23 Jan 2024 13:42:14 -0500 Subject: [PATCH 08/10] Update text/0000-constrained-types.md Co-authored-by: Ryan A. Hagenson --- text/0000-constrained-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index db779da6..45ad3060 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -145,7 +145,7 @@ Make this a ponylang organization library. I personally want this functionality ## `Error` collection type -I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making Error generic over the the error representation and use something like `Array[A]`. +I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making `Error` generic over the error representation and use something like `Array[A]`. Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `Error` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `Error` would itself remain unchanged. From 60dd66b7923fb44db31930628c943b419320a32c Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Tue, 23 Jan 2024 19:45:27 +0000 Subject: [PATCH 09/10] Update based on sync discussion --- text/0000-constrained-types.md | 64 +++++++++++++--------------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 45ad3060..9dea23f0 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -17,14 +17,14 @@ By providing an approved mechanism in the standard library, we can demonstrate t # Detailed design -The entire standard library addition can be included in a single file in a new package. +The entire standard library addition can be included in a single file in a new package. The package will be called `constrained_types`. ```pony -type ValidationResult is (Validated | Error) +type ValidationResult is (ValidationSuccess | ValidationErrors) -primitive Validated +primitive ValidationSuccess -class val Error +class val ValidationErrors let _errors: Array[String val] = _errors.create() new create(e: (String val | None) = None) => @@ -35,14 +35,14 @@ class val Error fun ref apply(e: String val) => _errors.push(e) - fun ref errors(): this->Array[String val] => + fun errors(): this->Array[String val] => _errors interface val Validator[T] new val create() fun apply(i: T): ValidationResult -class val Valid[T: Any val, F: Validator[T]] +class val Constrained[T: Any val, F: Validator[T]] let _value: T val new val _create(value: T val) => @@ -51,28 +51,28 @@ class val Valid[T: Any val, F: Validator[T]] fun val apply(): T val => _value -primitive ValidConstructor[T: Any val, F: Validator[T] val] - fun apply(value: T): (Valid[T, F] | Error) => +primitive MakeConstrained[T: Any val, F: Validator[T] val] + fun apply(value: T): (Constrained[T, F] | ValidationErrors) => match F(value) - | Validated => Valid[T, F]._create(value) - | let e: Error => e + | ValidationSuccess => Constrained[T, F]._create(value) + | let e: ValidationErrors => e end ``` The library could be used thusly: ```pony -type LessThan10 is Valid[U64, LessThan10Validator] -type MakeLessThan10 is ValidConstructor[U64, LessThan10Validator] +type LessThan10 is Constrained[U64, LessThan10Validator] +type MakeLessThan10 is MakeConstrained[U64, LessThan10Validator] primitive LessThan10Validator is Validator[U64] fun apply(i: U64): ValidationResult => recover val if i < 10 then - Validated + ValidationSuccess else let s: String val = i.string() + " isn't less than 10" - Error(s) + ValidationErrors(s) end end @@ -81,7 +81,7 @@ actor Main let prints = MakeLessThan10(U64(10)) match prints | let p: LessThan10 => Foo(env.out, p).go() - | let e: Error => + | let e: ValidationErrors => for s in e.errors().values() do env.err.print(s) end @@ -107,13 +107,13 @@ In our example usage code, we are creating a constrained type `LessThan10` that Some key points from the design: -## We can only validate immutable objects +## We can only constrain immutable objects -Validating a mutable item is pointless as it could change and go outside of our constraints after it has been validated. All validated items must be immutable. +Creating a constrained mutable object is pointless as it could change and go outside of our constraints after it has been validated. All constrained items must be immutable. -## `Error` is immutable +## `ValidationErrors` is immutable -We don't want to allow error mesages to be changed after validation is done. Because everything being validated is sendable, we can wrap an entire validator in a `recover` block and build up error messages on a `ref` Error before lifting to `val`. +We don't want to allow error mesages to be changed after validation is done. Because everything being validated is sendable, we can wrap an entire validator in a `recover` block and build up error messages on a `ref` `ValidationErrors` before lifting to `val`. ## Validators are not composable @@ -139,32 +139,16 @@ Adding this to the standard library means that if we decide we want to change it # Alternatives -## Where this lives: +## Where this lives Make this a ponylang organization library. I personally want this functionality and will create the library and maintain under the organization if we decided to not include in the standard library. -## `Error` collection type +## `ValidationErrors` collection type -I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making `Error` generic over the error representation and use something like `Array[A]`. +I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making `ValidationErrors` generic over the error representation and use something like `Array[A]`. -Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `Error` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `Error` would itself remain unchanged. - -## Names - -I'm generally happy with the names, but I would entertain that `Validated` instead of `Valid` reads better. So: - -```pony -type LessThan10 is Validated[U64, LessThan10Validator] -``` - -instead of: - -```pony -type LessThan10 is Valid[U64, LessThan10Validator] -``` - -If we made that change then `ValidConstructor` should become `ValidatedConstructor`. +Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `ValidationErrors` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `ValidationErrors` would itself remain unchanged. # Unresolved questions -The package needs a name. I'm thinking `constrained_types` or `constrained`. +None From 969b136fc366c1a9b8e0a0ae20f877ee465fc1ad Mon Sep 17 00:00:00 2001 From: "Sean T. Allen" Date: Wed, 31 Jan 2024 01:19:44 +0000 Subject: [PATCH 10/10] Change ValidationErrors name --- text/0000-constrained-types.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/text/0000-constrained-types.md b/text/0000-constrained-types.md index 9dea23f0..361ba27d 100644 --- a/text/0000-constrained-types.md +++ b/text/0000-constrained-types.md @@ -20,11 +20,11 @@ By providing an approved mechanism in the standard library, we can demonstrate t The entire standard library addition can be included in a single file in a new package. The package will be called `constrained_types`. ```pony -type ValidationResult is (ValidationSuccess | ValidationErrors) +type ValidationResult is (ValidationSuccess | ValidationFailure) primitive ValidationSuccess -class val ValidationErrors +class val ValidationFailure let _errors: Array[String val] = _errors.create() new create(e: (String val | None) = None) => @@ -52,10 +52,10 @@ class val Constrained[T: Any val, F: Validator[T]] _value primitive MakeConstrained[T: Any val, F: Validator[T] val] - fun apply(value: T): (Constrained[T, F] | ValidationErrors) => + fun apply(value: T): (Constrained[T, F] | ValidationFailure) => match F(value) | ValidationSuccess => Constrained[T, F]._create(value) - | let e: ValidationErrors => e + | let e: ValidationFailure => e end ``` @@ -72,7 +72,7 @@ primitive LessThan10Validator is Validator[U64] ValidationSuccess else let s: String val = i.string() + " isn't less than 10" - ValidationErrors(s) + ValidationFailure(s) end end @@ -81,7 +81,7 @@ actor Main let prints = MakeLessThan10(U64(10)) match prints | let p: LessThan10 => Foo(env.out, p).go() - | let e: ValidationErrors => + | let e: ValidationFailure => for s in e.errors().values() do env.err.print(s) end @@ -111,9 +111,9 @@ Some key points from the design: Creating a constrained mutable object is pointless as it could change and go outside of our constraints after it has been validated. All constrained items must be immutable. -## `ValidationErrors` is immutable +## `ValidationFailure` is immutable -We don't want to allow error mesages to be changed after validation is done. Because everything being validated is sendable, we can wrap an entire validator in a `recover` block and build up error messages on a `ref` `ValidationErrors` before lifting to `val`. +We don't want to allow error mesages to be changed after validation is done. Because everything being validated is sendable, we can wrap an entire validator in a `recover` block and build up error messages on a `ref` `ValidationFailure` before lifting to `val`. ## Validators are not composable @@ -143,11 +143,11 @@ Adding this to the standard library means that if we decide we want to change it Make this a ponylang organization library. I personally want this functionality and will create the library and maintain under the organization if we decided to not include in the standard library. -## `ValidationErrors` collection type +## `ValidationFailure` collection type -I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making `ValidationErrors` generic over the error representation and use something like `Array[A]`. +I think that `Array[String val]` is a good error representation that is easy to work with and gives people enough of what they would need without taking on a lot of generics fiddling that might make the library harder to use. That said, we could look at making `ValidationFailure` generic over the error representation and use something like `Array[A]`. -Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `ValidationErrors` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `ValidationErrors` would itself remain unchanged. +Additionally, if we thought it would be useful to get back a collection of errors that can be updated by calling code without changing the collection within the `ValidationFailure` type, we could use a persistent collection type like `persistent/Vec`. Using a persistent collection would allow for additional errors to be "added on" later while the collection in `ValidationFailure` would itself remain unchanged. # Unresolved questions