问题
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