Constructor by type of public record?

Say I want a record type, for example:

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

The fact is that I want to create a constructor that converts a list passed from integers to a new list that has no negative values ​​(they will be replaced with 0) and have IsCorrupt = true only if negative values ​​were found during construction .

Is this possible with F #?

At the moment, this is what I did using the properties (but, meh, this is not very F #, and each time it calls ConvertAllNegativeValuesToZeroes () on the getter, so it is 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) 
+4
source share
5 answers

In the end, I chose a mixture between my original ineffective solution proposed, @Grundoon's foldBack algorithm and efficient properties, which are just proxies for values ​​created at build time (therefore they are no longer efficient, they don't get evaluated every 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 

What is the easiest solution, IMO.

+1
source

A rather idiomatic way in F # is to use signature files to hide implementation details , but, as always, there are tradeoffs.

Imagine that you defined your model as follows:

 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 

Note that in addition to the create function, which validates input, this module also contains access functions for Values and IsCorrupt . This is necessary due to the next step.

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

However, now before the .fs file containing MyDomainModel , you add the signature file (a .fsi ). 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 

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

Because CounterValues defined as a type, but without any specific structure, clients cannot instantiate. In other words, this does not compile:

 module Client open MyDomainModel let cv = { Values = [1; 2]; IsCorrupt = true } 

The compiler complains that "Record Sign" Values ​​"is not defined".

On the other hand, clients can 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 some examples of 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] 

One of the drawbacks is that the overhead is associated with keeping the signature file ( .fsi ) and the implementation file ( .fs ) in sync.

Another disadvantage is that clients cannot automatically access items with record names. Instead, you need to define and maintain access functions such as Values and IsCorrupt .


All that was said is not the most common approach in F #. A more general approach would be to provide the necessary functions to calculate 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 are not too large, the performance overhead associated with computing such answers on the fly can be small enough to ignore (or possibly be addressed using memoization).

Here are a few 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 
+5
source

I watched something in Abstract Data Types (ADT) for a while, and this construct was used. It worked 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 availability of private will prevent direct access to record fields from other modules. You can add functions to your CounterValues ​​module to work and / or return data from the type passed to CounterValues. Notice how I added the two functions isCorrupt and count to work with the CounterValues ​​type.

+4
source

This is Kevin’s answer, but type CounterValues into the module and use one pass ("List.foldBack") to perform the check.

 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 as follows:

 // 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 am with Mauricio that boolean is bad. Have you considered a type of discriminatory union 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 map the pattern 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 performing limited types here .

+2
source

You cannot have a constructor, but what I usually see is used as 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. 

It is also a record, not a discriminatory 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 

Thus, your record has one field - Values , which you provide when building a record using standard syntax. IsCorrupt compiled as a read-only property that will be recalculated upon access to it, i.e. it will not be synchronized with Values

0
source

All Articles