Reputation: 35144
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?
Upvotes: 10
Views: 4395
Reputation: 233207
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).
Upvotes: 9
Reputation: 12184
Unfortunately there isn't convenient syntax for declaring a restricted subset of types but I would leverage active patterns to do this. As you rightly say, you can make a type and check it's validity when you construct it:
/// String type which can't be null or whitespace
type FullString (string) =
let string =
match (System.String.IsNullOrWhiteSpace string) with
|true -> invalidArg "string" "string cannot be null or whitespace"
|false -> string
member this.String = string
Now, constructing this type naively may throw runtime exceptions and we don't want that! So let's use active patterns:
let (|FullStr|WhitespaceStr|NullStr|) (str : string) =
match str with
|null -> NullStr
|str when System.String.IsNullOrWhiteSpace str -> WhitespaceStr
|str -> FullStr(FullString(str))
Now we have something that we can use with pattern matching syntax to build our FullString
s. This function is safe at runtime because we only create a FullString
if we're in the valid case.
You can use it like this:
let printString str =
match str with
|NullStr -> printfn "The string is null"
|WhitespaceStr -> printfn "The string is whitespace"
|FullStr fstr -> printfn "The string is %s" (fstr.String)
Upvotes: 0