【问题标题】:Extending records with additional fields使用附加字段扩展记录
【发布时间】:2020-01-16 14:38:06
【问题描述】:

我有一个数据管道,其中每一步都需要更多数据字段。我想通过尊重不变性以一种功能性的方式做到这一点。我想知道是否有 F# 方法可以做到这一点?

// code that loads initial field information and returns record A

type recordA = {
    A: int
}

// code that loads additional field information and returns record AB

type recordAB = {
    A: int
    B: int
}

// code that loads additional field information and returns record ABC

type recordABC = {
    A: int
    B: int
    C: int
}

由于记录是密封的,我不能只是继承它们。如何避免必须使用与上一步完全相同的字段定义新记录并添加必填字段?最好我希望有一条记录,其中包含所有必填字段,并且在每个步骤中将这些字段分配给它们的值。

请注意,每一步添加的字段数可能超过 1 个。

【问题讨论】:

    标签: f#


    【解决方案1】:

    我认为这对于最近在 F# 中引入的 anonymous records 来说是一个很好的用例。

    let a = {| X = 3 |}
    let b = {| a with Y = "1"; Z = 4.0|}
    let c = {| b with W = 1 |}
    printfn "%d, %s, %f, %d" c.X c.Y c.Z c.W
    

    【讨论】:

    • 我仍然必须以这种方式单独定义每条记录,然后复制粘贴上一条记录中的字段。如果我对基本记录进行更改,我想避免更改所有记录。有没有更干净的方法来定义?是这样的可能 type recordAB = { record A with B: int }
    • @Gehaktmolen 实际上,在这种情况下,您不必单独定义记录或在它们之间复制数据。这个答案使用匿名记录,在使用它们之前不必定义,它使用'copy-with-update'语法let b = {|a with Y = 2|}复制绑定为a的记录实例并另外设置Y 的值,创建一个绑定到标识符 b 的新实例。
    • 遗憾的是,该项目仍在旧版本的 F# 上,我无法使用此功能:(。它完全可以实现我想要做的事情。
    【解决方案2】:

    以非常 FP 风格执行此操作的一种方法是使用带有案例的 DU,用于工作流的每个步骤,并为每个案例中的每个步骤使用适当的数据:

    type WorfklowState =
    | StepOne of int
    | StepTwo of int * int
    | StepThree of int * int * int
    

    然后,您的整个工作流程状态,包括您当前所处的步骤以及该步骤产生/使用的数据,都将以数据类型建模。当然,您可能会为每个案例的数据创建记录类型,而不是使用逐渐变大的元组。

    【讨论】:

    • AFAIK 匿名记录可以在联合案例中用作具有命名字段的元组。
    【解决方案3】:

    根据应用程序,这可能是动态数据容器的(错误)用例。

    F# 可能会通过提供用户定义的动态查找运算符来提供帮助,为此会发生特殊的句法转换。

    let (?) (m : Map<_,_>) k = m.Item k
    // val ( ? ) : m:Map<'a,'b> -> k:'a -> 'b when 'a : comparison
    let (?<-) (m : Map<_,_>) k v = m.Add(k, v)
    // val ( ?<- ) : m:Map<'a,'b> -> k:'a -> v:'b -> Map<'a,'b> when 'a : comparison
    
    let m = Map.empty<_,_>
    let ma = m?A <- "0"
    let mabc = (ma?B <- "1")?C <- "2"
    ma?A    // val it : string = "0"
    mabc?C  // val it : string = "2"
    

    【讨论】:

      【解决方案4】:

      您可以“继承”记录:

      type RecordA =
          {
              a : int
          }
      
      
      type RecordAB =
          {
              a : RecordA
              b : int
          }
      
      
      type RecordABC =
          {
              ab : RecordAB
              c : int
          }
      

      然后您可以访问所有元素,尽管随着您越来越深入,链会越来越长。

      但是,为什么不直接使用元素列表来存储结果呢?

      首先,我将创建一个类型来处理您在每个步骤中可能拥有的所有类型,例如:

      type Step =
          | Int of int
          | String of string
          // ...
      

      那么您可以将工作流简单地表示为:

      type WorkflowState = list<Step>
      

      如果您想确保始终拥有至少一个元素,那么您可以使用:

      type WorkflowState = Step * list<Step>
      

      但是,记录有标签,而上面的结构没有它们!因此,如果您确实需要标签,那么您可以使用强类型通过地图来表示它们:

      type Label =
          | A
          | B
          // ...
      
      type WorkflowMappedState = Map<Label, Step>
      

      或者只是一个基于字符串的,例如

      type WorkflowMappedState = Map<string, Step>
      

      与上述答案相比,基于列表或地图的方法的好处是您不必知道可能的最大步骤数。如果步数超过 100 步怎么办?您想创建一个包含 100 多个标签的记录吗?很可能不是!匿名记录很棒,但是如果您想在创建它们的模块之外使用它们怎么办?我认为这会引起一些麻烦。

      说了这么多,我想我会采用基于列表的方法:type WorkflowState = list&lt;Step&gt;。它是非常 F# 的方式,并且很容易进一步转换。

      【讨论】:

        猜你喜欢
        • 2019-08-19
        • 2013-12-11
        • 2014-10-20
        • 1970-01-01
        • 2010-11-08
        • 1970-01-01
        • 1970-01-01
        • 2020-05-30
        • 2022-01-18
        相关资源
        最近更新 更多