Reputation: 4210
I have a problem with generic type constraints.
Background: I'm just getting started with WPF and am creating a class to help with validation. The goal is easy usage from XAML as well as type safety in the validation from the back-end.
To that end, I have created a class that wraps another value. It has a property Raw
that I bind to in XAML, and a property Valid
that gives me a final, valid and converted result. The class is instantiated with both a converter and a list of checks on the converted value. The converter as well as the checks can, of course, fail.
I want to create two overloads: One without the checks (to make a wrapper that only converts) and one without the converter (to make a wrapper that only checks and doesn't convert). I'm having trouble with the latter. Here's the class (INotifyPropertyChanged and IDataErrorInfo removed):
type Validated<'raw, 'converted>
(init:'raw,
convert:'raw->Result<'converted,string>,
checks:('converted->Result<unit,string>) list) =
let getValidationError converted validate =
match validate converted with
| Ok () -> None
| Error err -> Some err
new(init:'raw, convert:'raw->Result<'converted,string>) =
Validated(init, convert, [])
// This is the one I'm having trouble with
new(init:'raw, checks:('converted->Result<unit,string>) list) =
Validated(init, Ok, checks)
member val Raw = init with get, set
member private this.Converted = convert this.Raw
member this.Errors =
match this.Converted with
| Ok x -> checks |> List.choose (getValidationError x)
| Error x -> [x]
member this.ValidValue =
match this.Converted, this.Errors with
| Ok x, [] -> Some x
| _ -> None
In the second overload, I get the following error: This construct code causes code to be less generic than indicated by type annotations. The type variable 'raw has been constrained to be type 'converted.
Which of course makes sense - since it's not converting, the types would have to be the same.
Is it possible to create such an overload for this class, or do I have to use another class with a single type parameter for that?
Note that I can create an instance using e.g. new Validated("", Ok, [])
which then is of type Validated<string, string>
. It's just the actual constructor overload I'm having trouble with.
Upvotes: 1
Views: 142
Reputation: 80915
What you're trying to do is to have one of your class constructors construct instances of a differently typed class. This, of course, cannot happen: a constructor of a class can only construct instances of that class.
If you want to have specialized constructors, just make them outside functions:
let validatedNoChecks init convert = Validated( init, convert, [] )
let validatedNoConvert init checks = Validated( init, Ok, checks )
Also note that your implementation is a bit inefficient: the body of your properties will run on every access, not just once at construction.
In general, I would recommend not using classes at all. They are only present in F# in order to support interop with C# and other .NET languages. They create problems (like the ones in your code), and don't add anything. There is absolutely no sane reason to use them.
For example, your Validated
class could be built as a record + constructor function:
type Validated 'raw 'converted = { raw: 'raw; errors: string list; validValue: 'converted option }
let validated init convert checks =
let getValidationError converted validate =
match validate converted with
| Ok () -> None
| Error err -> Some err
let converted = convert init
let errors = match converted with
| Ok x -> checks |> List.choose (getValidationError x)
| Error x -> [x]
let validValue = match converted, errors with
| Ok x, [] -> Some x
| _ -> None
{ raw = init; errors = errors; validValue = validValue }
And then specialized constructors:
let validatedNoChecks init convert = validated init convert []
let validatedNoConvert init checks = validated init Ok checks
If you really need to extend an "object-oriented" interface to some external C# code, use interfaces: just turn the above record into an interface and construct it with an object expression. Still no reason to use classes.
If you want results to be lazily evaluated, use Lazy<'t>
as type of errors
and validValue
. Still no reason to use classes.
Upvotes: 1