How to make input[][] return a Just Int instead of String in Elm?

Viewed 50

I'm a complete beginner to Elm an I'm struggling with types. I've been struggling a lot with types and I couldn't figure out a convenient way of fixing this problem. The piece of code in question is this:

view model =
    div []
        [ h1 [] [ text "Robot" ]
        , input [ onInput SetX, value model.x ] []
        , input [ onInput SetY, value model.y ] []
        , input [ type_ "Int" , onInput SetCommands, value model.x] []
        , button [ onClick ButtonPressed] []
        , input [ readonly True ] []
        ]

X and Y are Ints used in the following function:

execute_orders : Int -> Int -> String -> String -> String -> { x : Int, y : Int, dir : String }
execute_orders x y commands lang dir =
    let
        moves =
            case lang of
                "English" ->
                    "RLF"

                "French" ->
                    "HVG"

                "Swedish" ->
                    "DGT"

                _ ->
                    Debug.todo "No Language Found"

        fw =
            right 1 moves

        directions =
            facing dir

        rght =
            left 1 moves

        lft =
            slice 1 2 moves

        first_move =
            left 1 (toUpper commands)

        rest =
            slice 1 (length commands) (toUpper commands)
    in
    if first_move == "" then
        { x = x, y = y, dir = fromJust (head directions) }

    else if first_move == fw then
        if dir == "N" then
            execute_orders x (y - 1) rest lang dir

        else if dir == "E" then
            execute_orders (x + 1) y rest lang dir

        else if dir == "S" then
            execute_orders x (y + 1) rest lang dir

        else
            execute_orders (x - 1) y rest lang dir

    else if first_move == lft then
        execute_orders x y rest lang (fromJust (head (turn_left directions)))

    else if first_move == rght then
        execute_orders x y rest lang (fromJust (head (turn_right directions)))

    else
        Debug.todo "branch '_' not implemented"

I need x and y to be Int's but casting them from String results in the Maybe Integer type...

2 Answers

There is no way to simply "cast" a String to an Int because there's no total mapping in that direction. There is, for example, no obvious integer representation for the string "banana". What you are struggling with isn't "types", but different things being different for different reasons, and having to make a decision to consolidate those differences.

String.toInt returns a Maybe Int because it's up to you to decide what to do when there's no reasonable and obvious integer representation for a given string. It's your choice whether you want to just default to 0, -1, or perhaps display the string - instead. These are all reasonable choices for different scenarios, but since String.toInt can't possibly know which scenario you're in, you have to make that decision.

If you simply want to fall back to a default integer, or you're certain that any given string will have a valid integer representation, you can use Maybe.withDefault:

String.toInt "42" |> Maybe.withDefault 0     -- 42
String.toInt "banana" |> Maybe.withDefault 0 -- 0

Or you can use a case expression to do something completely different:

case String.toInt s of
  Just n ->
    span [] [ text (n + 1 |> String.fromInt) ]

  Nothing ->
    button [ onClick ... ] [ text "-" ]

Since the HTML input event always deals with strings, you need to parse the string you receive at some point. There are three options that I see.

First, the payload of your event message can be a String, and then you parse that string in your update function.

type Msg
    = SetX String

type alias Model =
    { x : Int }

update msg model =
    case msg of
        SetX xString ->
            case String.toInt xString of
                Nothing ->
                    model
                Just x ->
                    { model | x = x }

A variation of the above would be to store x as a Maybe String on your model if that makes sense, and/or storing some value for tracking an error state if you want to show a message when an invalid value is sent. This is the simplest and most flexible approach.

A second option is to have the payload of your event message be a Maybe Int. Html.Events.onInput is a function which takes a "tagger" function which accepts a String and outputs a message. While you can just give a message constructor that takes a String payload, you can also give a function which does some processing before creating a Msg value.

type Msg
    = SetY (Maybe Int)

view model =
    input [ type_ "number", onInput (\value -> String.toInt value |> SetY) ] []

The third option is to have the payload of your event message be an Int. This will require you to create your own version of onInput using Html.Events.on. on takes a Json.Decode.Decoder msg instead of the (String -> msg) function that onInput takes. The message will only be produced if the decoder succeeds; that is, events which produce a failed decoder will be ignored. This allows you to handle the case where the parsing fails within the event handler.

type Msg
    = SetY Int

view model =
    input [ type_ "number", onNumericInput SetY ] []

-- based on https://github.com/elm/html/blob/97f28cb847d816a6684bca3eba21e7dbd705ec4c/src/Html/Events.elm#L115-L122
onNumericInput : (Int -> msg) -> Attribute msg
onNumericInput toMsg =
    let
        alwaysStop : a -> ( a, Bool )
        alwaysStop x =
            ( x, True )

        failIfNothing : Maybe a -> Decode.Decoder a
        failIfNothing maybe =
            case maybe of
                Nothing ->
                    Decode.fail "Parsing failed"

                Just a ->
                    Decode.succeed a

        decoder : Decode.Decoder msg
        decoder =
            Events.targetValue
                |> Decode.map String.toInt
                |> Decode.andThen failIfNothing
                |> Decode.map toMsg
    in
    Events.stopPropagationOn "input" (decoder |> Decode.map alwaysStop)

You can see the last two options in a full example at https://ellie-app.com/h6fstFTyjXDa1

Related