【问题标题】:Constructor on public record type?公共记录类型的构造函数?
【发布时间】:2015-08-11 13:52:03
【问题描述】:

假设我想要一个记录类型,例如:

type CounterValues = { Values: (int) list; IsCorrupt: bool }

问题是,我想创建一个构造函数,将传递的整数列表转换为没有负值的新列表(它们将被 0 替换),并且只有在找到负值时才具有 IsCorrupt=true在施工时。

这可以用 F# 实现吗?

目前,这就是我所做的,使用属性(但是,嗯,它不是很 F#-ish,它每次都会在 getter 处调用 ConvertAllNegativeValuesToZeroes(),因此效率不高):

type CounterValues
    (values: (int) list) =

    static member private AnyNegativeValues
        (values: (int) list)
        : bool =
            match values with
            | v::t -> (v < 0) || CounterValues.AnyNegativeValues(t)
            | [] -> false

    static member private ConvertAllNegativeValuesToZeroes
        (values: (int) list)
        : (int) list =
            match values with
            | [] -> []
            | v::t ->
                if (v < 0) then
                    0::CounterValues.ConvertAllNegativeValuesToZeroes(t)
                else
                    v::CounterValues.ConvertAllNegativeValuesToZeroes(t)

    member this.IsCorrupt = CounterValues.AnyNegativeValues(values)

    member this.Values
        with get()
            : (int) list =
                CounterValues.ConvertAllNegativeValuesToZeroes(values)

【问题讨论】:

标签: types constructor f# record


【解决方案1】:

fairly idiomatic way in F# is to use signature files to hide implementation details,但与往常一样,需要权衡取舍。

想象一下,你已经这样定义了你的模型:

module MyDomainModel

type CounterValues = { Values : int list; IsCorrupt : bool }

let createCounterValues values =
    {
        Values = values |> List.map (max 0)
        IsCorrupt = values |> List.exists (fun x -> x < 0)
    }

let values cv = cv.Values

let isCorrupt cv = cv.IsCorrupt

请注意,除了检查输入的 create 函数外,此模块还包含 ValuesIsCorrupt 的访问器函数。由于下一步,这是必要的。

到目前为止,MyDomainModel 模块中定义的所有类型和函数都是公共的。

但是,现在您在包含MyDomainModel.fs 文件之前添加一个签名文件.fsi 文件)之前。在签名文件中,你只放你想向外界发布的内容:

module MyDomainModel

type CounterValues
val createCounterValues : values : int list -> CounterValues
val values : counterValues : CounterValues -> int list
val isCorrupt : counterValues : CounterValues -> bool

注意,声明的模块名称是一样的,但类型和函数只在抽象中声明。

因为CounterValues 被定义为一种类型,但没有任何特定的结构,所以没有客户端可以创建它的实例。换句话说,这不会编译:

module Client

open MyDomainModel

let cv = { Values = [1; 2]; IsCorrupt = true }

编译器抱怨“未定义记录标签‘值’”。

另一方面,客户端仍然可以访问签名文件定义的功能。 这样编译:

module Client

let cv = MyDomainModel.createCounterValues [1; 2]
let v = cv |> MyDomainModel.values
let c = cv |> MyDomainModel.isCorrupt

以下是 FSI 的一些示例:

> createCounterValues [1; -1; 2] |> values;;
val it : int list = [1; 0; 2]

> createCounterValues [1; -1; 2] |> isCorrupt;;
val it : bool = true

> createCounterValues [1; 2] |> isCorrupt;;
val it : bool = false

> createCounterValues [1; 2] |> values;;
val it : int list = [1; 2]

其中一个缺点是保持签名文件 (.fsi) 和实现文件 (.fs) 同步会产生开销。

另一个缺点是客户端不能自动访问记录的命名元素。相反,您必须定义和维护访问器函数,例如 valuesisCorrupt


话虽如此,这并不是 F# 中最常见的方法。一种更常见的方法是提供必要的函数来即时计算此类问题的答案:

module Alternative

let replaceNegatives = List.map (max 0)

let isCorrupt = List.exists (fun x -> x < 0)

如果列表不太大,动态计算此类答案所涉及的性能开销可能小到可以忽略(或者可以通过记忆化来解决)。

以下是一些用法示例:

> [1; -2; 3] |> replaceNegatives;;
val it : int list = [1; 0; 3]

> [1; -2; 3] |> isCorrupt;;
val it : bool = true

> [1; 2; 3] |> replaceNegatives;;
val it : int list = [1; 2; 3]

> [1; 2; 3] |> isCorrupt;;
val it : bool = false

【讨论】:

  • +1 非常好。我会选择另一种方法,因为从长远来看,它肯定会让事情更容易维护。
【解决方案2】:

不久前我在抽象数据类型 (ADT) 上看过一些东西,并使用了这个结构。它对我来说效果很好。

type CounterValues = private { Values: int list; IsCorrupt: bool }
[<CompilationRepresentation (CompilationRepresentationFlags.ModuleSuffix)>]
module CounterValues =

  let create values =
    let validated = 
      values 
      |> List.map (fun v -> if v < 0 then 0 else v)
    {Values = validated; IsCorrupt = validated <> values}

  let isCorrupt v =
    v.IsCorrupt

  let count v =
    List.length v.Values

CompilationRepresentation 允许模块具有与类型相同的名称。 private 可访问性将阻止从其他模块直接访问记录字段。您可以将函数添加到 CounterValues 模块以对传入的 CounterValues 类型进行操作和/或返回数据。请注意我是如何添加两个函数 isCorruptcount 来使用 CounterValues 类型的。

【讨论】:

  • 为什么是私有的?为什么是 CompilationRepresentation 属性?你能详细说明这两件事吗?
  • 私有,因为他想要一个 smart 构造函数 (create) 代替
【解决方案3】:

这是凯文回答的一个变体,但把 CounterValues 输入一个模块,并使用一次传递('List.foldBack') 进行验证。

module API =
    type CounterValues = private { Values: (int) list; IsCorrupt: bool }

    /// Create a CounterValues from a list of ints
    let Create intList =

        // helper for foldBack below
        let folder i (values,isCorrupt) =
            if i < 0 then 
                (0::values,true)
            else
                (i::values,isCorrupt)
        // one pass through the list to detect and fix bad values
        let newValues,isCorrupt = List.foldBack folder intList ([],false)

        // create a CounterValues 
        {Values=newValues; IsCorrupt=isCorrupt}

    /// Get the contents of a CounterValues 
    let Get cv =
        cv.Values, cv.IsCorrupt

代码是这样使用的:

// direct access fails
// let cv = API.CounterValues  // error

// using "factory" function
let cv1 = API.Create [1;2;3;4] 
cv1 |> API.Get    // ([1; 2; 3; 4], false)

let cv2 = API.Create [1;2;-3;4] 
cv2 |> API.Get    // ([1; 2; 0; 4], true)

但我同意 Mauricio 认为布尔值不好。你有没有考虑过像这样的有区别的联合类型?

module API =
    type CounterValues = 
        private 
        | NonCorrupt of int list 
        | Corrupt of int list 

    /// Create a CounterValues from a list of ints
    let Create intList =

        // helper for foldBack below
        let folder i (values,isCorrupt) =
            if i < 0 then 
                (0::values,true)
            else
                (i::values,isCorrupt)
        // one pass through the list to detect and fix bad values
        let newValues,isCorrupt = List.foldBack folder intList ([],false)

        // create a CounterValues 
        if isCorrupt then Corrupt newValues else NonCorrupt newValues

    /// Get the contents of a CounterValues using active patterns
    let (|NonCorrupt|Corrupt|) cv =
        match cv with 
        | Corrupt intList -> Corrupt intList 
        | NonCorrupt intList -> NonCorrupt intList 

然后你可以在使用的时候进行模式匹配:

// helper to pattern match
let print cv = 
    match cv with
    | API.Corrupt intList -> 
        printfn "Corrupt %A" intList  
    | API.NonCorrupt intList -> 
        printfn "NonCorrupt %A" intList  

let cv1 = API.Create [1;2;3;4] 
cv1 |> print   // NonCorrupt [1; 2; 3; 4]

let cv2 = API.Create [1;2;-3;4] 
cv2 |> print   // Corrupt [1; 2; 0; 4]

我还有一些做约束类型的例子here

【讨论】:

    【解决方案4】:

    最后,我选择了我最初提出的低效解决方案、@Grundoon 的 foldBack 算法和仅代理在构造时创建的值的高效属性(因此它们不再低效,它们不会被评估)之间的混合每次):

    type CounterValues
        (values: (int) list) =
    
        // helpers for foldBack below
        let folder v (values,isCorrupt) =
            if v < 0 then 
                (0::values,true)
            else
                (v::values,isCorrupt)
    
        // one pass through the list to detect and fix bad values
        let curatedValues,isCorrupt = 
            List.foldBack folder vals ([],false)
    
        member this.IsCorrupt
            with get()
                : bool =
                    isCorrupt
    
        member this.Values
            with get()
                : (int) list =
                    curatedValues
    

    这是最简单的解决方案,IMO。

    【讨论】:

      【解决方案5】:

      你不能有构造函数,但我通常看到使用的是静态工厂方法:

      type CounterValues = 
          { Values: int list; IsCorrupt: bool }
          static member Make(values: int list) = 
               // do your work here, returning the constructed record.
      

      另外,这是一个记录,而不是歧视性工会。

      编辑:我在评论中描述的是这样的:

      type CounterValues = 
          { Values: int list }
          member this.IsCorrupt = 
              this.Values
              |> List.tryFind (fun x -> x < 0)
              |> Option.isSome
      

      这样,您的记录就有一个字段 - Values,您在使用标准语法构造记录时提供该字段。 IsCorrupt 被编译为只读属性,当你访问它时会重新计算,这意味着它不会与 Values 不同步

      【讨论】:

      • 但是这样一个开发者可以创建一个具有 IsCorrupt=false 并且具有负值的 CounterValues 实例,以防他忘记使用 Make()
      • 如果IsCorrupt 根据定义总是派生自Values 字段,那么它首先不应该是该记录的字段。改为使其成为记录的属性。
      • 是否愿意编辑您的答案以反映您刚才所说的内容?
      • IsCorrupt 设为计算属性仍不会阻止Values 包含负值。
      • 但是 IsCorrupt 效率不高;我正在寻找一种方法让 isCorrupt 只被评估一次(在构造函数中)
      猜你喜欢
      • 1970-01-01
      • 2011-03-01
      • 1970-01-01
      • 2012-12-22
      • 2011-02-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多