cmeeren
cmeeren

Reputation: 4210

Type constraint mismatch in class with two generic types, overloaded to make the two types equal

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

Answers (1)

Fyodor Soikin
Fyodor Soikin

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

Related Questions