【问题标题】:F# type constraint for record type with specific property具有特定属性的记录类型的 F# 类型约束
【发布时间】:2020-01-13 00:56:01
【问题描述】:

我正在尝试创建一个通用函数,它要求它的类型参数是一个记录类型,并且它具有特定的属性。这是一个生成相关编译器错误的示例:

let foo<'a> (a : 'a) =
    a' = { a with bar = "baz" }
    a'

编译这个我得到一个错误声明The record label bar is not defined

我尝试添加以下类型约束:

let foo<'a when 'a : (member Id : string)> =
    // ...

但这也没有编译,抱怨This code is not sufficiently generic. The type variable ^a when ^a : (member get_Int : ^a -&gt; string) could not be generalized because it would escape its scope.

有没有办法指定一个类型约束,让我可以正确地做到这一点?

【问题讨论】:

  • 首先你的函数必须是内联的才能被静态约束。您可以创建一个函数来读取具有该属性的任何记录的特定属性,但要克隆它,您不能使用静态约束的with 语法。那时你将需要像反思这样的东西。
  • 你应该看看镜头。
  • @FyodorSoikin 我不认为 Lenses 解决了这个问题。甚至多态镜头也不行。如前所述,AFAIK 无法使用成员 bar 的静态成员约束来调用 {a with bar = .. 构造。

标签: generics f# type-constraints


【解决方案1】:

我建议先阅读 Tomas 的回答。通常应尽可能避免使用静态解析的类型约束。它们是 F# 编译器而不是 .NET 的一个特性,因此它们在一定程度上限制了代码的可重用性。也就是说,它们非常强大,并且允许您在编译时施加有用的约束。

使用它们的语法也不是非常令人愉快,但如果你不害怕,你可以这样做:

type Test = {Bar : string}

let inline foo (a : ^a) =
    "foo " + ((^a) : (member Bar : string) (a))

let foobar = foo {Bar = "bar"} // prints "foo bar"

但是请注意,您实际上不能将类型限制为记录,只是具有string 类型的成员Bar 的东西。所以这也可以解决:

type Test2(str : string) = member this.Bar = str

let foobar2 = foo (Test2("bar")) // prints "foo bar"

【讨论】:

    【解决方案2】:

    我认为没有办法使用静态成员约束来指定这一点 - 静态成员约束受到相当大的限制,它们主要是一种抽象机制,除了其他更常用的技术之外还可以使用。

    如果我试图解决这样的问题,我可能会考虑使用接口(这并不总是最好的方法,但在不了解您的具体情况的情况下,这可能是一种合理的默认方法):

    type ISetA<'T> = 
      abstract WithA : string -> 'T
    
    type MyRecord = 
      { A : string }
      interface ISetA<MyRecord> with
        member x.WithA(a) = { x with A = a }
    

    在实现记录时,您需要添加一个接口实现(因此与仅使用静态成员约束相比,您需要做更多的工作)。但是,您还明确表示这是该类型的预期用途...

    用法也比使用静态约束时简单:

    let setA (setA:ISetA<_>) = 
      setA.WithA "Hello"
    
    setA { A = "Test" }
    

    【讨论】:

      【解决方案3】:

      我不确定你的目标是什么。

      如果您想要读取通用记录的属性,请参阅 TheInnerLight 的工作示例。

      如果您想编写一个克隆多种类型记录的函数,那么您应该更改您的设计。您可以遵循 Tomas 建议的方法。

      除此之外,还有另一种选择:使用嵌套的通用记录。

      type Test<'a> = {Bar : string; Rest : 'a}
      
      type A = {PropA : string}
      type B = {PropB : int}
      
      let a = {Bar = "bar"; Rest  = {PropA = "propA" }}
      
      
      let foo a = {a with Bar = "foo " + a.Bar}
      
      let foobar = foo {Bar = "bar"; Rest = {PropA = "propA"}} 
      // val foobar : Test<A> = {Bar = "foo bar"; Rest = {PropA = "propA";};}
      
      let foobar' = foo {Bar = "bar"; Rest = {PropB = 0}}
      // val foobar' : Test<B> = {Bar = "foo bar"; Rest = {PropB = 0;};}
      

      最后一点,通过这种设计,您可以按照 FyodorSoikin 最初的建议迁移到 Lenses。

      【讨论】:

      • 我喜欢这种方法,但在尝试后我很快就发现了下一步:stackoverflow.com/questions/39306148/…
      • 不,你不能展平类型,除非结果类型是预定义的。目前 F# 中没有匿名类型。
      猜你喜欢
      • 1970-01-01
      • 2014-12-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-11-13
      • 2021-01-30
      相关资源
      最近更新 更多