Closed robhanlon22 closed 2 years ago
We have dry-struct for this, don't we?
Also, .constrained
can be chained. It doesn't seem there's a practical need for combining two arbitrary types. I mean, I've been using dry-types for ages and I cannot recall a use case for this. Do you have any?
Other than this, I can see there's a temptation to have something like this in the lib.
That’s a good point. I was looking into some issues with nested composition of schemas over in dry-schema, and the approach to combining schemas is to merge their types into a Dry::Types::Schema; it seemed easiest to me to be able to conjoin the types that were Dry::Logic::Operations::And’ed together with an intersection operator; I’m all ears for other ideas! Thanks
I have been wanting this capability for a while, here's an example use-case.
I often express object interfaces, like
module Types
Callable = Interface(:call)
Procable = Interface(:to_proc)
Function = Interface(:call, :to_proc)
end
Using a Sum here does not work. It would accept anything that responds to either #call
or #to_proc
, but does not enforce both.
You can express multiple interfaces at definition time, it would be really nice to compose two interfaces together.
Function = Callable & Procable
@alassek closing this because I won't have the time to work on it; feel free to use this initial work if you'd like to implement this! thanks
I didn't have time to properly respond to this but in a nutshell, there's an issue with duplicated predicates. For example,
String.constrained(min_size: 5) & String.constrained(max_size: 10)
will check if valid input is a string two times. Deduplicating predicates is not a reliable solution since it will depend on the order of predicates. Moreover, types can be nested and combined in a tree structure.
OTOH, types like
String.constrained(min_size: 5) | String.constrained(max_size: 10)
have the same issue for invalid input. So maybe I'm just being over-cautious.
Yeah, there's always an issue with duplicated predicates. You can easily end up with never
types, like something that is both true and false. However, I think that @alassek's example is a very good one; it's nice to be able to compose multiple types into a single type, like the compound interface in the example. dry-logic allows for this with its operations, and you can imagine combining two constrained strings:
ContainsFoo = String.constrained(format: /foo/)
IsLongEnough = String.constrained(min_size: 10)
ContainsFooAndIsLongEnough = ContainsFoo & IsLongEnough
One could argue that you could extract these constraints out to their parameters and merge them to create your new type, but that breaks out of the compositional thinking that you might use when sum / intersection types.
There's also another concern, support for such types has to be added to dry-schema increasing its complexity. I mean generating error messages etc.
support for such types has to be added to dry-schema
No it doesn't have to be added :) We could add it maybe in the future, but if we decide to add them here, dry-schema could simply produce an exception saying "oops type not supported".
I meant more like eventually
FWIW, I tried this branch out with no changes in dry-schema and it A) worked with no changes to the message compiler (because it already has visit_and
) and it B) greatly simplified the complex TypesMerger
code that I just wrote. Further convinces me that adding this and perhaps implication to Dry::Types would be a good move.
For completeness, dry-types should include an Intersection type along side its Sum type.
This PR is certainly not yet complete (refactoring and specs needed), but does pass some initial smoke tests from the console.