Constructor on public record type?

核能气质少年 提交于 2019-12-06 05:43:29

问题


Let's say I want a record type such as:

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

The thing is, I want to create a constructor that converts the list passed of integers to a new list which has no negative values (they would be replaced by 0s), and have IsCorrupt=true only if there were negative values found at construction time.

Is this possible with F#?

For now, this is what I've done, using properties (but, meh, it's not very F#-ish and it calls ConvertAllNegativeValuesToZeroes() at the getter every time so it's not very efficient):

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)

回答1:


A fairly idiomatic way in F# is to use signature files to hide implementation details, but as always, there are trade-offs involved.

Imagine that you have defined your model like this:

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

Notice that apart from a create function that checks the input, this module also contains accessor functions for Values and IsCorrupt. This is necessary because of the next step.

Until now, all types and functions defined in the MyDomainModel module are public.

However, now you add a signature file (a .fsi file) before the .fs file that contains MyDomainModel. In the signature file, you put only what you want to publish to the outside world:

module MyDomainModel

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

Notice that the name of the module declared is the same, but the types and functions are only declared in the abstract.

Because CounterValues is defined as a type, but without any particular structure, no clients can create instances of it. In other words, this doesn't compile:

module Client

open MyDomainModel

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

The compiler complains that "The record label 'Values' is not defined".

On the other hand, clients can still access the functions defined by the signature file. This compiles:

module Client

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

Here are FSI some examples:

> 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]

One of the disadvantages is that there's an overhead involved in keeping the signature file (.fsi) and implementation file (.fs) in sync.

Another disadvantage is that clients can't automatically access the records' named elements. Instead, you have to define and maintain accessor functions like values and isCorrupt.


All that said, that's not the most common approach in F#. A more common approach would be to provide the necessary functions to compute answers to such questions on the fly:

module Alternative

let replaceNegatives = List.map (max 0)

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

If the lists aren't too big, the performance overhead involved in computing such answers on the fly may be small enough to ignore (or could perhaps be addressed with memoization).

Here are some usage examples:

> [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



回答2:


I watched something on Abstract Data Types (ADT) a while back and this construct was used. Its worked well for me.

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 allows the module to have the same name as the type. The private accessibility will prevent direct access to the records fields from other modules. You can add functions to your CounterValues module to operate on and/or return data from a passed in CounterValues type. Notice how I added the two functions isCorrupt and count to work with the CounterValues type.




回答3:


This is a variant on Kevin's answer, but putting the CounterValues type in a module, and using one pass ('List.foldBack') to do the validation.

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

The code is used like this:

// 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)

But I'm with Mauricio that booleans are bad. Have you considered a discriminated union type like this?

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 

And then you can pattern match when using it:

// 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]

I have some more examples of doing constrained types here.




回答4:


In the end I chose a mixture between my initial unefficient proposed solution, the foldBack algorithm from @Grundoon, and efficient properties that just proxy to values created at construction time (so they are not inefficient anymore, they don't get evaluated each time):

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

Which is the simplest solution, IMO.




回答5:


You can't have a constructor, but what I typically see used is a static factory method:

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

Also, this is a record, not a discriminated union.

Edit: What I described in the comment was something like this:

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

That way your record has one field - Values, that you provide when you construct the record using the standard syntax. IsCorrupt gets compiled as a read-only property that will be recalculated when you access it, meaning it won't get out of sync with Values



来源:https://stackoverflow.com/questions/31943698/constructor-on-public-record-type

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!