F#泛型类型约束与鸭子类型

12

我正在尝试在F#中实现鸭子类型,并发现你可以通过以下方式在F#泛型中使用成员约束

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

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

然而,当我试图引用该属性时,上述代码将无法编译。我会得到一个编译器错误:
此代码不够通用。类型变量^T,当^T:(member get_Name: ^T -> string)时,不能泛化,因为它将逃离其作用域。
是否可能通过泛型约束来实现鸭子类型?

1
请注意,这并不是真正的“鸭子类型”,而是结构(子)类型。 - Eyvind
3个回答

27
最近有类似的问题,其中成员约束被用在类型声明中。我不确定如何更正您的示例以使其编译,但如果无法实现那也不会让我感到惊讶。成员约束旨在与静态解析类型参数一起使用,特别是与inline函数或成员一起使用,我不认为将其与类的类型参数一起使用是F#代码的惯用方式。我认为对于您的示例更符合惯用方式的解决方案是定义一个接口:
type INamed = 
  abstract Name : string

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

事实上,ListEntryViewModel 可能不需要类型参数,只需将 INamed 作为构造函数参数即可,但以这种方式编写可能会有一些好处。

现在,您仍然可以使用鸭子类型,并在具有 Name 属性但未实现 INamed 接口的对象上使用 ListEntryViewModel!这可以通过编写一个返回 INamed 并使用静态成员约束来捕获现有 Name 属性的 inline 函数来实现:

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

您可以通过编写ListEntryViewModel(namedModel someObj)来创建您的视图模型,其中someObj不必实现接口,但需要Name属性。

我更喜欢这种风格,因为通过使用接口,您可以更好地记录所需的模型内容。如果您有其他不符合该方案的对象,则可以进行适应,但如果您正在编写一个模型,则实现接口是确保它公开所有所需功能的好方法。


可能已经太晚问这个问题了,但是内联方法中的对象表达式会在每次调用内联方法时创建一个新对象吗?这是否可以被认为是使用内联方法作为适配器接口的缺点?特别是如果名称被高频访问的情况下。 - GrumpyRodriguez

6
为使您的原始代码正常运行:
type ListEntryViewModel< ^T when ^T : (member Name : string)>(model:^T) = 
    inherit ViewModelBase()

    member inline this.Name with get() = (^T : (member Name : string) model)

因此,您必须将成员标记为“inline”,并在成员函数中重复约束。

我同意Tomas的看法,使用基于接口的方法通常是F#的首选。


6
你能否通过泛型约束实现鸭子类型?
不行。除了一些特殊情况外,F#只实现了名义类型,无法实现鸭子类型。正如其他答案所解释的那样,惯用的“解决”方法是为您希望其遵循该接口的所有类添加接口,但在大多数需要鸭子类型的情况下,这显然是不可行的。
请注意,F#的这种限制来自.NET。如果您想看到类似于鸭子类型的更实用的解决方案,请查看OCaml的结构类型多态变量和对象。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接