Search code examples
elm

Elm: Access value of custom type


I have a custom type in Elm to handle error branching. Basically, I have an input, which gives Strings and I need to convert it to Ints.

type Seconds
    = Error Int
    | Valid Int

type alias Model =
    { timeBetweenExercises : Seconds
    , roundsSequenceOne : Seconds
    , roundsSequenceTwo : Seconds
    , roundsSequenceThree : Seconds
    , repititionsPerExercise : Seconds
    , secondsPerExercise : Seconds
    , totalTrainingDuration : Seconds
    }


init : Model
init =
    { timeBetweenExercises = Valid 3
    , roundsSequenceOne = Valid 3
    , roundsSequenceTwo = Valid 3
    , roundsSequenceThree = Valid 3
    , repetitionsPerExercise = Valid 6
    , secondsPerExercise = Valid 6
    , totalTrainingDuration = Valid 6
    }

I got the idea for the custom type from Evan's "Life of a file" talk. I want to remember the number when there is an error (e.g. a user entered a string instead of a number). Here is the attempt at my update function:

update : Msg -> Model -> Model
update msg model =
    case msg of
        TimeBetweenExercisesChanged newTime ->
            case String.toInt newTime of
                Nothing ->
                    { model | timeBetweenExercises = Error model.timeBetweenExercises }

                Just time ->
                    { model | timeBetweenExercises = Valid time }

My problem is, that the compiler yells at me because because model.timeBetweenExercises is of type Seconds. Is there a way I can only get the Int value of the custom type?


Solution

  • It seems to me that the model is wrong, for two reason:

    1. If you have a value that is common across all cases, it's usually more convenient to move it out and up a level.

    2. Your update function suggests that the Error state doesn't actually describe the value it holds, as you're just throwing away newTime if invalid and using the old time for the Error case instead.

    Based on that, I'd suggest the following model instead:

    type alias TimeInput =
        { seconds: Int
        , state: TimeInputState
        }
    
    type TimeInputState = Error | Valid
    
    type alias Model =
        { timeBetweenExercises : TimeInput
          ...
        }
    

    and to change your update function to this:

    update : Msg -> Model -> Model
    update msg model =
        case msg of
            TimeBetweenExercisesChanged newTime ->
                case String.toInt newTime of
                    Nothing ->
                        { model
                            | timeBetweenExercises = 
                                { seconds = model.timeBetweenExercises.seconds
                                , state = Error
                                }
                        }
    
                    Just time ->
                        { model
                            | timeBetweenExercises = 
                                { seconds = time
                                , state = Valid
                                }
                        }
    

    Otherwise, you could always just make a function to extract the Int no matter the case:

    getSeconds : Seconds -> Int
    getSeconds time =
        case time of
            Error seconds -> seconds
            Valid seconds -> seconds