-
Notifications
You must be signed in to change notification settings - Fork 21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New type: constrained type #553
Comments
I think this is how I would actually do this. Somehow it feels cleaner to me but it has a similar amount of boilerplate: [<AutoOpen>]
module Qty =
type Qty = private Qty of int
let (|Qty|) (Qty q) = q
module Qty =
let tryMake qty =
if qty >= 0 && qty <= 99
then Ok (Qty qty)
else Error (sprintf "%i is not a valid Qty" qty)
Qty.tryMake 10 // Ok (Qty 10)
Qty.tryMake 100 // Error "100 is not a valid Qty"
let f (Qty (q:int)) = q // compiles
There are several valid approaches and I think adding this feature as it is would be too prescriptive. And also, if there are multiple arguments during creation, how would the compiler know which ones were invalid? Presumably, the test would be an arbitrary boolean expression that could compare multiple inputs, launch missiles etc. |
@theprash Thanks for your input! However that's not really any more readable... Also, your example doesn't compile for me. The inner
|
That sample only works in F# 4.1. Otherwise you need to add I agree it's still not pretty but I think it provides a better API. Perhaps still not good enough to be baked into the language. |
@Richiban records are the way to go for what you want type [<Struct>] Qty = private {
Value:int
} with
static member Create qty =
if qty >= 0 && qty <= 99
then { Value = qty }
else invalidArg "qty" (sprintf "%i is not a valid Qty" qty) but as for this feature I'm not if favor of it. This would just be a poor man's dependent type without any compile time constraints. this mainly seems to a way to reduce boilerplate which could be addressed by type providers once they are able to generate F# types and use types as input |
@cloudRoutine I agree, this kind of is dependent types but without the compile-time checking. Since I very much doubt we're going to be getting dependent types any time soon, I'd still like to have the checking at runtime. Who knows? Maybe the syntax could even be shared between the two. Unfortunately your record solution doesn't solve the problem: using the type you created I can still bypass the Create method completely and write let invalid = { Qty.Value = -1 } This compiles fine and throws no error at runtime. |
@Richiban you sure about that? |
@cloudRoutine Ahh... it turns out that you have to put the type in a different module. Then the field is inaccessible. So that's fine, but for your type you would also need to write the public property in order to be able to get the int field out again. |
I guess I want to be clear when I ask for this feature I'm not asking for dependent types or anything complicated like that... All I want is to be able to write a (structurally equal type) in F# that will protect its invariants. I would accept a record type that allowed a type Qty = {
value : int
} do
if value < 0 || value > 99 then invalidArg "value" "That is not a valid quantity" This would roughly mirror the way one would add code to the body of the constructor in a class: type Qty(value : int) =
do
if value < 0 || value > 99 then invalidArg "value" "That is not a valid quantity"
member this.Value = value Except the type is still a record, and therefore gets the structural equality etc for free that you would have to write yourself in the class example. |
@Richiban Possibly the simplest path to this is to allow Specifically, class types where there are no extra captured fields implied by the (method-captured) I believe it's actually quite simple to implement this. It does help reduce the dissonance between classes and records (without requiring us to add more and more class-like features to records) So:
or this (note
or this (note
The only issue is about the meaning this:
Does Also, there would be a requirement that all arguments to the constructor are type-annotated, rather than type inferred, as for structs today, though I'm sure you're happy with that. |
@Richiban If you're happy with that proposal then please change the title, and I think we can consider it approved-in-principle. It's one of the cleanup items I've been meaning to do for a long time, to complete the matrix of possibilities |
Thanks Dom, that would be good. Would it be more appropriate to amend this
issue or to open a new one? Not sure how bothered we are about the
conversation history attached to an issue.
…On Fri, 24 Mar 2017, 17:39 Don Syme, ***@***.***> wrote:
@Richiban <https://github.com/Richiban> If you're happy with that
proposal then please change the title, and I think we can consider it
approved-in-principle. It's one of the cleanup items I've been meaning to
do for a long time, to complete the matrix of possibilities
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#553 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/AAoyjyqXBFEUHZZMEmW5KS9PQH7qXQGeks5ro_-sgaJpZM4MmsgV>
.
|
I have opened a new issue (#554) as I felt that modifying this issue to reflect the new solution to the problem would leave a large number of comments that no longer made sense. I will close this issue now. Thanks all for your input! |
I propose we introduce a new type with structural equality that simply wraps one or more values but, more importantly, allows one to constrain the values that are allowed. For example, imagine trying to introduce a type into your domain to represent a quantity. You might be tempted to write:
or
But the problem is that we also have the requirement that any quantity in this domain must be greater than or equal to zero, and also less than or equal to 99. There is no way of capturing that requirement in F#.
My proposal is to add a new syntax for defining a type, and I call it a constrained type:
This syntax will essentially generate a single-case union type called Qty with the associated constructor function / pattern:
so it's exactly as normal, except that calling
let x = Qty (-1)
will cause an InvalidArgumentException to be thrown.The existing way of approaching this problem in F# is to either hack around with trying to make single-case unions private and reintroducing member properties and writing a static method to simulate a constructor:
This sort of works but involves a fair amount of boilerplate code (which always increases the risk of introducing a bug) and leaves us in the unsatisfactory situation that
Qty
is actually not a type--it's a module.Qty.T
is the type.The other alternative is to simply fall back to OO:
but then you lose all the structural equality that you get from an F#-native type.
Pros and Cons
The advantages of making this adjustment to F# are that problem domains can be more accurately modeled by encapsulating the requirements of a value in the type itself.
The disadvantages of making this adjustment to F# are that it's work to do and another syntax for F# users to learn. I think it's incredibly readable though.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): M
Affadavit (must be submitted)
Please tick this by placing a cross in the box:
Please tick all that apply:
The text was updated successfully, but these errors were encountered: