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