Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constructor on public record type?

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)
like image 796
knocte Avatar asked Dec 15 '22 12:12

knocte


2 Answers

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
like image 120
Mark Seemann Avatar answered Dec 28 '22 02:12

Mark Seemann


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.

like image 44
Kevin Avatar answered Dec 28 '22 00:12

Kevin