Search code examples
genericsf#inline

F# generic type constraints and duck typing


I'm trying to implement duck typing in F# and I spotted that you can have a member constraint in F# generics as follows:

type ListEntryViewModel<'T when 'T : (member Name : string)>(model:'T) = 
  inherit ViewModelBase()

  member this.Name with get() = model.Name

However, the above code won't compile when I try to reference the property. I get a compiler error:

This code is not sufficiently generic. The type variable ^T when ^T : (member get_Name : ^T -> string) could not be generalized because it would escape its scope.

Is it possible to implement duck typing via a generic constraint?


Solution

  • There was a similar question recently where member constraints were used in the type declaration.

    I'm not sure how to correct your sample to make it compile, but I would not be surprised if that was not possible. Member constraints are designed to be used with statically resolved type parameters and especially with inline functions or members and I do not think it is idiomatic F# code to use them with type parameters of a class.

    I think that a more idiomatic solution to your example would be to define an interface:

    type INamed = 
      abstract Name : string
    
    type ListEntryViewModel<'T when 'T :> INamed>(model:'T) =  
      member this.Name = model.Name
    

    (In fact, the ListEntryViewModel probably does not need a type parameter and can just take INamed as a constructor parameter, but there may be some benefit in writing it in this way.)

    Now, you can still use duck typing and use ListEntryViewModel on things that have Name property, but do not implement the INamed interface! This can be done by writing an inline function that returns INamed and uses static member constraints to capture the existing Name property:

    let inline namedModel< ^T when ^T : (member Name : string)> (model:^T)= 
      { new INamed with
          member x.Name = 
            (^T : (member Name : string) model) }
    

    You can then create your view model by writing ListEntryViewModel(namedModel someObj) where someObj does not have to implement the interface, but needs just the Name property.

    I would prefer this style, because by taking an interface, you can better document what you require from the model. If you have other objects that do not fit the scheme, you can adapt them, but if you're writing a model, then implementing an interface is a good way to make sure it exposes all the required functionality.