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)
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With