Language Validation/Constraint Support For Types #1155
Replies: 16 comments 3 replies
-
Note that another way to do this is via a static member, like so: type private String3To5 =
| String3To5 of string
static member create str =
if String.length str < 3 then None
elif String.length str > 5 then None
else Some (String3To5 str)
static member value (String3To5 str) = str
match String3To5.create "123" with
| Some s -> s |> String3To5.value
| None -> ":(" Though I would probably use a normal class at this point just to make convenient use of type String3To5 private (str) =
static member Create str =
if String.length str > 5 || String.length str < 3 then
None
else
Some (String3To5(str))
member this.Value = str
match String3To5.Create("123") with
| Some s -> s.Value
| None -> ":(" So at the very least you can ensure that it's all contained in the same entity. |
Beta Was this translation helpful? Give feedback.
-
Thank you so much for your prompt and awesome reply. I especially like the class approach and will be using that instead from this point (at least instead of single case DU). That being said, there are a number of benefits that baking this into the language would provide. As I stated, I took some corners in the interest of making this brief (What can I say? I'm a lazy programmer). I also emphasized the domain modeling aspect and insidiously inserted a tiny bit about usage. Your awesome alternative fits most of the domain modeling concern. I will create a more comprehensive example to illustrate what I mean and post here (hopefully later today). Until then, here is a sample list (partially based on what I've seen from Scott Wlaschin & other sources):
Again, thanks for your awesome response and apologies for not using a more thorough example. I will post one shortly. |
Beta Was this translation helpful? Give feedback.
-
You need
|
Beta Was this translation helpful? Give feedback.
-
Sorry for the delayed response - had some computer problems. Here's a more detailed example of what I'm proposing:
|
Beta Was this translation helpful? Give feedback.
-
I can sympathize with your suggestion, I guess we've all been there. But functional languages separate data from business logic, and validation is part of business logic. Typically you'd use a module with the same name as your type that contains It's not uncommon to create a type with a hidden constructor that encapsulates creation behavior for data that should be validated. But I believe it's far more common to use a DU with the core types and combine with result for validation. There's a trade off for each approach and it depends on the domain which is most suitable. There are some suggestions out there that could make this easier to bake into a DU, like allowing private constructor overrides. But I'm not sure they'll fully cover this, as when you construct a type, there's no mechanism, other than raising an exception, to return an invalid instance. Though, you could make the invalid instance part of the DU, but then we're back at the separation of concerns issue: that's what |
Beta Was this translation helpful? Give feedback.
-
Btw, your last post reminds me of Design By Contract, which is an OO concept. I believe there are some libraries that inject such code for you, and you use attributes to add the "contract" requirements. Such libraries should work with F# just the same. |
Beta Was this translation helpful? Give feedback.
-
Thanks a lot for your input @abelbraaksma! I can definitely understand the separation of concerns idea and benefits. In my mind, though, given the promotion of domain driven design with F#, it makes sense for constraints to be a part of the definition. Everything I've been reading touts being able to look at a type definition and having even mere mortals have a good intuition about what it does. I believe constraints fit squarely into that idea. I'm not saying the way I presented it is the best (it's probably horrible), just that I think this is an integral part of a type and should be a first class citizen. I don't see how a domain design can be considered complete without specifying the constraints. Yes, there are bunch of code "workarounds" to add constraints, as shown by your post where you suggest yet another. My concern is just that, it's not standard. You have to learn what each project is doing. Looking at the design/definitions tell you little. All these workarounds end up compromising the use of the original intended type in some way. They also are not meant to be understood by the mere mortals. On the topic of separation of concerns, I believe that can be a bit subjective. If we look at this from the perspective of the single responsibility principle, I can argue that the the one reason for a "change" (in the case of types, a new type to be created), is if the constraints or constituent types change (in type or number). IOW, I'm arguing that structure and constraints form the single responsibility. By splitting them into two separate entities, we've achieved little as a change to one breaks or requires a change in the other (or changes the entire contract). As an aside: That is one of my go to rules for deciding single responsibility. If I split something into two, can I update the two new pieces independently? If yes, these are two single responsibilities. If no, I already had a single responsibility. In the case of my String3To5 example, changing the name or the constituent type would require changing the constraint and vice versa. That is one reason I don't see the need for separation. For argument sake, let's say someone changes the name to String3to6 but forgets to change the constraint. This is much easier to spot with the integrated constraint than it is with the separated workarounds. At the risk of this response getting overwhelmingly long, I'll address your "business logic" concern. I lean in the direction of disagreeing with this. One, because the point of DDD is to model the domain and two, because types themselves, by definition, are constraints. If the domain expert tells me a ProductID can be 0..255, using "ProductID: byte" is effectively a constraint. I wouldn't use "ProductID: float". IOW, I'm arguing we are allowing some constraints in the definition but not others. Other examples, "Speed: float<m/s>", "Temperature: float<fahrenheit/.>". Thanks again and I look forward to your additional feedback. |
Beta Was this translation helpful? Give feedback.
-
Exactly. And then, your input gets an int. So you create a function Similarly, you can have a dedicated type that is restricted to a string of 2 to 5 chars. When your input comes from strings that aren't restricted, you create a function In both cases it's a design decision whether or not you create a type that's validating the input on creation. But in this approach, you would consider it an exception if the type is created with an out of range value. The I don't disagree with your points, but as often it depends to what approach is most sensible. You have all the freedom to create types that can only be instantiated with valid inputs. You have the freedom to put this logic 'on the type' using I'm not trying to suggest your proposal doesn't have merit. It has. But I'm unsure it should be part of the language. It may be a better fit a library instead. But I can be wrong, and I do see the/some benefit of adding this in some way to the language. |
Beta Was this translation helpful? Give feedback.
-
Thanks @abelbraaksma. I just want to clarify one thing.
This is the same in either version. The point I was trying to make is that given the domain expert understands what "byte" means, or just from the dev perspective, looking at "ProductID: byte", you know there is no way possible to construct this type with an out of range ProductID. This is regardless of tryCreate. I look at this definition and I know: this record contains a product ID, the product ID is constrained to be 0..255, this is the contract and I cannot create an instance that violates these constraints, period. |
Beta Was this translation helpful? Give feedback.
-
@wilbennett you may be interested in this for your needs DependentTypes |
Beta Was this translation helpful? Give feedback.
-
Oh yes, thank you for your input @greatim. Thanks @jackfoxy! That seems very interesting. I don't fully understand the entire thing yet but looking at the example, it's not quite the same as what I'm proposing. I'll definitely look into it some more though. |
Beta Was this translation helpful? Give feedback.
-
Let me do a compare and contrast that will hopefully shed more light on this idea. I'll use @greatim's suggestion. I'm not picking on you @greatim. Your suggestion is elegant and succinct. I'm adding a private qualifier as I believe you accidentally left that off. The domain expert says a product has an ID and a name:
You explain that "int" means a number and "string" means a sequence of characters. He/she says, well the product ID must be between 200 and 999 and the name must be 3 to 5 characters. So you create additional types:
The expert says, oops, I meant 3 to 7 characters:
Here are the issues I see with this approach:
Now let's look at the alternative:
The expert says, well the product ID must be between 200 and 999 and the name must be 3 to 5 characters:
The expert says, oops, I meant 3 to 7 characters:
So what have we achieved?
How do we create these types?
I know you are probably saying, "But Wil, can you really trust what the constraint says or must you trust but verify". Very clever my friend but the difference is, the constraints are "global" and need only be verified once. They can then be used in any number of definitions with confidence. And remember, the compiler could run constraints at design time so we get the additional benefits we do with regular type constraints. I don't believe this to be a foreign concept - just an extension of the constraints we already express when we create types. With just the plain definition, we are constraining ProductID to contain only numbers and to be within a specific range. All we are doing here is further constraining the range. Just by using the Product record, we are constraining products to consist of only a ProductID and a Name. |
Beta Was this translation helpful? Give feedback.
-
@abelbraaksma, another thought on why I do not consider these constraints "business logic". F# isn't as rich in type constraints as other languages. If we take Delphi/Object Pascal, for example. You can define types like the following:
If we could do something similar in F#, it would be like:
Hopefully you would now agree that using these types would not be considered "business logic". If F# would allow constraints in this form, it would be even more acceptable. The caveat being we end up manually defining more types than the proposal and not be as flexible. Using the proposal, for example, we can have a constraint that uses a regular expression to constrain a string to being in a valid email address format. Then again, maybe we could do something like |
Beta Was this translation helpful? Give feedback.
-
I'll convert this to a discussion - it's a great discusssion about validation techniques but there's not a specific concrete proposal which is viable for the language, though one might emerge #516 is related btw |
Beta Was this translation helpful? Give feedback.
-
The issue discussed here would benefit greatly with |
Beta Was this translation helpful? Give feedback.
-
Press F7 to pay respect |
Beta Was this translation helpful? Give feedback.
-
Disclaimer
I'm not an expert F# developer so please forgive any syntactic mistakes in the following as I'm not writing this in an IDE.
Overview
One of the big draws to F# for me is the promise of domain modeling in the type system. An area where this seems to fall short, IMHO, is supporting validation on types. Again, I'm not an expert F# developer so please don't hesitate to correct any of my misconceptions.
My understanding is, looking at declared types, you should be able to easily reason about how they are handled and the compiler should prevent invalid usage.
Issue
Let's examine a simple case:
I want to create a string subtype that only allows strings of length 3 to 5:
The existing way of approaching this problem in F# is ...
There are several things I don't like about this solution:
I propose we use one of the following options or something similar...
I omitted some cases and took some corners for the sake of brevity but hopefully the intent is clear.
Pros and Cons
The advantages of making this adjustment to F# are ...
The disadvantages of making this adjustment to F# are ...
Extra information
Estimated cost (XS, S, M, L, XL, XXL):
Related suggestions:
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
Beta Was this translation helpful? Give feedback.
All reactions