Mikhail Shilkov
Mikhail Shilkov

Reputation: 35144

Type to represent a string which is not empty or spaces in F#

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

Answers (2)

Mark Seemann
Mark Seemann

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

TheInnerLight
TheInnerLight

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 FullStrings. 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

Related Questions