How do I create a type alias for a limited range/set of values?

Suppose I have the following type alias:

type alias MyType =
    { my_string : String
    , my_num : Int
    }

As of now my_string can consist of any string and my_num can consist of any int.
Could I create a type alias that would allow me to restrict it’s possible values further beyond just it’s base type? Say I wanted my_num to only be some integer between 0 and 127, if I was defining a type in Elixir I could write something like

@type MyType :: %{
  my_string: String.t(),
  my_num: 0..127
}

where 0..127 is shorthand for “a range of integers from 0 to 127”.
I could even write

@type MyType :: %{
  my_string: String.t(),
  my_num: 0..127 | String.t()
}

if I wanted to allow my_num to also be an arbitrary string (for some reason).

Is there any equivalent in Elm?

You can use a custom type to do this. By not exporting the constructor for the type, you can limit it’s creation to functions that ensure the value is within the range you have.

E.g:

type BoundedInt 
    = BoundedInt Int
    
fromInt : Int -> Maybe BoundedInt
fromInt value =
    if value >= 0 && value <= 127 then
        BoundedInt value |> Just
    else
        Nothing

If you don’t expose the actual constructor to BoundedInt, the only values must be in that range. Of course, you will have to provide other operations to make the type actually useful.

The main downside is the inability to prove that a constant is a valid member of this type. You can do certain things to alleviate this (e.g: expose some constants like max and min for common values that “cheat” and directly use the constructor as you can assert they are correct), but there is no way to prove it to the compiler that I know of.

For a real-world use case, in “Massive Decks” I use this to validate codes from strings to a custom object that only represents valid codes. You can see I get around the constant issue by having a function that allows you to construct one with potentially invalid strings. This does introduce the potential to get it wrong, but means it is hard to do something wrong by accident, at least.

In your second case of a union type, you would just add two constructors, one taking a string, one taking an int, and then you can pattern match over them to deal with the cases.

An alternative to returning Maybe is to clamp the value, like this:

fromInt : Int -> BoundedInt
fromInt value =
    BoundedInt (clamp 0 127 value)

Yeah, that’s a good point as well, depending on your use case, it may make sense to expose different methods of creating the value—a Maybe is the obvious answer, but for numbers clamping, wrapping, or something else might be best.

You’ll see in the code I linked, I’m actually correcting the value by upper casing the given characters and filtering out invalid characters. What makes sense will depend on the use case—in some situations you want to just try and get a usable value, in others you might want to ensure you error out if the value doesn’t make sense.

You mean you want dependent types? They tend to open up a can of worms so I wouldn’t expect to be able to do this.

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.