I love the simplicity of types like
type Code = Code of string
But I would like to put some restrictions on string (in this case - do not allow empty of spaces-only strings). Something like
type nonemptystring = ???
type Code = Code of nonemptystring
How do I define this type in F# idiomatic way? I know I can make it a class with constructor or a restricted module with factory function, but is there an easy way?
A string
is essentially a sequence of char
values (in Haskell, BTW, String
is a type alias for [Char]
). A more general question, then, would be if it's possible to statically declare a list as having a given size.
Such a language feature is know as Dependent Types, and F# doesn't have it. The short answer, therefore, is that this is not possible to do in a declarative fashion.
The easiest, and probably also most idiomatic, way, then, would be to define Code
as a single-case Discriminated Union:
type Code = Code of string
In the module that defines Code
, you'd also define a function that clients can use to create Code
values:
let tryCreateCode candidate =
if System.String.IsNullOrWhiteSpace candidate
then None
else Some (Code candidate)
This function contains the run-time logic that prevents clients from creating empty Code
values:
> tryCreateCode "foo";;
val it : Code option = Some (Code "foo")
> tryCreateCode "";;
val it : Code option = None
> tryCreateCode " ";;
val it : Code option = None
What prevents a client from creating an invalid Code
value, then? For example, wouldn't a client be able to circumvent the tryCreateCode
function and simply write Code ""
?
This is where signature files come in. You create a signature file (.fsi
), and in that declare types and functions like this:
type Code
val tryCreateCode : string -> Code option
Here, the Code
type is declared, but its 'constructor' isn't. This means that you can't directly create values of this types. This, for example, doesn't compile:
Code ""
The error given is:
error FS0039: The value, constructor, namespace or type 'Code' is not defined
The only way to create a Code
value is to use the tryCreateCode
function.
As given here, you can no longer access the underlying string value of Code
, unless you also provide a function for that:
let toString (Code x) = x
and declare it in the same .fsi
file as above:
val toString : Code -> string
That may look like a lot of work, but is really only six lines of code, and three lines of type declaration (in the .fsi
file).
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