How to upgrade from 0.18 to 0.19 without toString abstraction

Hi all, am upgrading from 0.18 to 0.19, and have a function that uses toString (which is no longer available - at least it’s not intended for production use).

Anyone know how I can upgrade the following 0.18 function without destroying the abstraction (which is a lot of refactoring work) over comparable types that it provides?

range : comparable -> comparable -> Input comparable -> Input comparable
range min max input =
    let
        failure =
            fail ("Out of range [" ++ toString min ++ ", " ++ toString max ++ "]") input
    in
    case input.current of
        Nothing ->
            input

        Just value ->
            if (value >= min) && (value <= max) then
                input

            else
                failure

I ran into a similar situation trying to update antivanov/eunit, where it uses toString in the equality testing function to provide a useful error message. So far I’ve just put it into the source directories of the apps, but I’ve considered adding toString as a parameter. The app would provide Debug.toString (probably the only function that could even fill the a -> String role), presumably as a local definition currying the first parameter.

Otherwise I don’t see how third party testing frameworks could provide useful error messages.

2 Likes

Thanks, that sounds a promising approach but I can’t see how to get a handle on the toString function, since it’s inaccessible (closure scoped) at Elm.init(). Can you point me to your code that tackles this (or did you not upgrade yet)?

I share your frustration. I feel like I’ve been slapped down by Health and Safety for using scissors, in case I hurt myself.

taking a step back, does this sort of function actually make sense? Are there better ways to communicate failure here. I think a signature like

range : comparable -> comparable -> Input comparable -> Result (comparable, comparable) (Input comparable)

Would make much more sense. Now the caller can actually format the error message in whatever way they like. Additionally, a Input can always be valid (it looks like it has an error case that you produce with fail right now.

2 Likes

I meant something like

range_ : (a -> String) -> comparable -> comparable -> Input comparable -> Input comparable

range = range_ Debug.toString

I would like to second folkertdev’s answer. Because Elm is all about those cases in which you might hurt yourself accidentally.

With a Result, no caller can ever forget that the computation might fail. And they will have all the information they need in case it does. This is exactly why Elm can promise no runtime errors. I admit, you have to shift away from the thinking in other, more common languages, but the reward is that your code is very robust and thus easy to refactor and debug.

The full example with Result, with some extra Health and Safety stuff in the parameters:

-- Assuming Input looks something like this.
type Input a
    = Input a


range : { min : comparable, max : comparable } -> Input comparable -> Result { min : comparable, max : comparable } (Input comparable)
range bounds (Input value) =
    if (value >= bounds.min) && (value <= bounds.max) then
        Ok <| Input value

    else
        Err bounds

With this, it will be really hard to switch up the min and max.

And for refactoring the call sites, it’s not that bad. Currently, you either use just use the value, ignoring the failure case, or you handle the possible failure right at the call. (I don’t really see what other alternatives there are.)

In case you just use the value, it’s as simple as specifying a default value.

-- So this:
range 0 10 input
-- becomes this:
Result.withDefault 0 <| range {min = 0, max = 10 } input

And for handling the error, you simply pattern match the Result.

case range { min = 0, max = 10 } input of
    ok validinput ->
        -- Continue
        "ok"

    Err { min, max } ->
        let
            errorMessage =
                "Out of range [" ++ String.fromInt min ++ ", " ++ String.fromInt max ++ "]"
        in
        -- Output error message
        errorMessage

I’ve actually checked that Elm infers the correct type for min and max. If you do String.fromChar min it throws a compiler error.

Now how do we know that min and max are numbers? Because we just called range with those as the boundaries. So since we handle the error right were it occurred, we are not really missing the generic Debug.toString.

Why all this hassle? Well, previously either you handled the error anyway and this is just a different method for doing so, or you ignored that the error might occur and risked runtime errors. I find it much more helpful to have the Elm compiler point out that I didn’t handle the error case and thus have a really stable program.

Thanks all for comments and feedback. My dsl requires the given signature for composibility - for illustration, here’s an example snippet of “range” being used:

form : Form
form =
    { age =
        field
            (integer
                >> default 35
                >> range 30 50
            )
    , channel =
        field
            (integer
                >> range 1 100
                >> member channelListIds
            )
    , frequency =
        field
            (float
                >> range 1.2 17.8
            )
    , date =
        field
            (date
                >> range "1 Jan 2000" "1 Jan 2020"
            )

The only options I see for implementing range to support this are:

  1. a polymorphic toString function (removed in 0.19)
  2. adding explicit noise to the dsl
  3. storing and using the relevant toString function within the “Input a” type (which is a record alias)

I was reluctant to store functions in the model - elm discourages that - however I’ve elected that because the others options are less appealing.

Unless anyone has a much better approach, I’ll consider this solved.

Many thanks.

What happens when frequency = field (float >> range 1.2 17.8) is out of bounds? Where does the error get handled?

Because right now it looks like that error is simply ignored, but that means your program will just crash when it happens. So from the example you gave it is not quite clear to me how the range is useful. Could you elaborate?

Validation (and other field management tasks, eg. casting and formatting) is done by executing the composed field function, invoked as required during an Elm update cycle. Errors are accumulated over the composition and made available as a list at the end of computation (disclosing all non-dependent validation errors at once rather than incrementally), which are then rendered in the normal TEA view.

Here are the internal changes made which were not too onerous in the end (somewhat off topic):

-- OLD (Elm 0.18) - relies on polymorphic toString
type alias Input a =
    { previous : Maybe a
    , current : Maybe a
    , string : String
    , errors : List String
    }

-- NEW (Elm 0.19) - relies on function in model
type alias Input a =
    { previous : Maybe a
    , current : Maybe a
    , string : String
    , errors : List String
    , stringify : Maybe (a -> String)
    }

Hope that helps :slight_smile:

Ok, I have to say I don’t really understand how your API works. But that’s fine. :slight_smile: Thanks for being so patient with me!

I think the easiest way it out of this is to implement an intRange and a floatRange.

With the additional context, I think I would look towards having a custom type for the errors. If you didn’t want to use separate functions and constructors for float/int, you would still need to pass toString to the error stringification function. Having the type would give the option to do more customized formatting of the different errors though (e.g. highlighting the numbers)

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