I’m not actually sure what the best terminology is here, a guarded type, a restricted type? The idea is to wrap a basic type in an opaque custom type, and expose a lower case constructor for it, such that only instances meeting certain guard conditions can ever be created. An example:
module Unitary exposing (Unitary, unitary, value) -- Unitary is opaque.
{-| Numbers between zero and one. -}
type Unitary
= Unitary Float
unitary : Float -> Maybe Unitary
unitary val =
if val < 0 then
Nothing
else if val > 1 then
Nothing
else
Unitary val |> Just
value : Unitary -> Float
value (Unitary val) =
val
So this can be quite annoying to use because the constructor returns a Maybe
. An alternative might be:
unitary : Float -> Unitary
unitary val =
clamp 0.0 1.0 val |> Unitary
half = unitary 0.5
When I define the value half
, I know it will work, but if the answer was a Maybe
I would have to include some failure processing code to deal with Nothing
, whilst knowing that will never happen, and this way I don’t have to - Also, in 0.19 Debug.crash
is not an option for the never followed failure branch.
On the other hand, some bugs may be silently masked by the default value.
This technique is only viable in situations where a reasonable default can be assumed. What about if the guard type was over strings, and the strings must have a minimum length? The caller passes a string in of length 9, but this particular constructor needs 10 chars minimum. Padding the string out with whitespace would seem wrong - but not impossible. What if it were a regular expression match? Mapping the input to the nearest regex match would definitely seem wrong and not easy to do either.
I also note that in the case where constructors return a Maybe, some common values can be provided by the module implementing the guard type, since they do have access to the upper case constructor:
zero = Unitary 0.0
half = Unitary 0.5
one = Unitary 1.0
This can work well if the guard type is an enum - all the enum values can be listed out.
type Fruit
= Fruit String
apple = Fruit "Apple"
orange = Fruit "Orange"
banana = Fruit "Banana"
What if I want to help the caller understand why something could not be created? Instead of Maybe
perhaps I should use Result
? Maybe is for things that are optional, Result is for things that can result in errors. If the caller passes 2.0
, a good response might be Err "The input must be between 0.0 and 1.0, but you gave the value 2.0."
.
Would love to hear your views on this, or alternative ideas.
Always return Maybe, or allow defaults if the case is right for it?
Maybe vs Result?