Safe unsafe operations in Elm

Hi everyone!

I wrote a blog post how you can simplify your code and have more guarantees using the “safe unsafe” pattern.

I think it is a pretty good overview of how you can build elm-review rules, as we go from a simple rule to a reasonably complex one.

So if you are interested in either having more guarantees or learning more about elm-review, check out the blog post: is: https://jfmengels.net/safe-unsafe-operations-in-elm/

Let me know what you think about this!
There is an #elm-review Slack channel if you want to learn more about the tool :slight_smile:

25 Likes

Hey! I’ve never heard of elm-review, and I have to say I’m impressed with what it can accomplish.

This regex situation is a great example of the awkwardness of being safe when you can just look at the code and easily reason that it is correct, and the correctness of the code is fundamentally determinable at compile time, but the compiler doesn’t know how to make that judgment.

I’ll definitely look into this further and maybe this module will find its way into one of my projects.

2 Likes

Nice trick! Can be used in a lot of places.
I also had similar issues when trying to parse express.js like routes from string literals “/user/:id”
It will be helpful to validate them at compile(review) time as well.
It would be awesome for developer to be able to extend elm compiler itself with custom rules like this.

Yep, I can see this being very useful for creating Url instances from a constant string too - good stuff @jfmengels!

Enjoyed the article.

Instead of doing this:

fromLiteral : String -> Regex
fromLiteral string =
    case Regex.fromString string of
        Just regex ->
            regex

        Nothing ->
            fromLiteral string

I have been tending to do:

type Error =
    IllegalRegex String 

fromLiteral : String -> Result Error Regex
fromLiteral string =
    case Regex.fromString string of
        Just regex ->
            Ok regex

        Nothing ->
            IllegalRegex string |> Err

and propagate the error up the call stack, possibly turning it into a common string format, or mapping it into broader error types as it goes, until I get it to a “catch all” point in my top-level update function. Then I log it as a bug for development to fix. Disadvantage of this is; its a pain to propagate errors everywhere; and its a shame to introduce something that acts like a runtime error into the program. However, if I avoid ever using Result.withDefault (which masks errors), then it does at least force my programs to always report these kinds of errors.

If you think about it, Result.withDefault is a bit like ignoring errors with catch (Exception e) {} in Java, which is nasty.

I like what you are proposing though, its kind of like a secondary and more advanced type checker for Elm isn’t it? Nice if you have a rule like this ready to go - I guess the downside of this approach is having to write more rules for other situations.

Another example might be a type that is restricted to certain values:

type Percent =  
    Percent Float
   
percent : Float -> Percent

and a rule for calling the percent function that only allows literal values from 0.0 to 100.0.

If the values are not being obtained from static code, but are being decoded from HTTP API calls, or user input in the UI. Then the Result approach is likely better (but its not necessarily a bug in that case).

A further downside is having to restrict to literal values - I know certain math operations will give me a valid result, but the review rules may not be so smart. The general case is getting into thoerem proving.

1 Like

Absolutely. I think you should handle the error in the closest possible place in the call stack.

I tend to use the recursive pattern when I am sure of something but could not make it impossible with data or API modeling, but I cringe every time I use it (which is not that often thankfully).

I think your way of handling it is quite nice, especially wrapping it in a custom type that will give more information about the error. But as you said, it’s painful to use.

its kind of like a secondary and more advanced type checker for Elm isn’t it?

Yes. The point is giving you more guarantees than what the Elm compiler does. In my mind, if you see compiler as assistants, then elm-review is another assistant. It’s like having two people look over your shoulder to help you out.

I guess the downside of this approach is having to write more rules for other situations.

I haven’t written about it in the article, but I believe that you can make a single rule that would handle multiple usecases. In the rule I wrote, I have already made it kind of generic by making the target names variables, and the target function check can just be a boolean.

What I’m saying is: You could make the rule work for several functions. You could hardcode, or even take as the rule’s arguments (by changing rule to rule : List TargetFunctions -> Rule), a list of target functions that could look like

type alias TargetFunctions =
  { name : String
  , moduleName : String -- Or List String
  , goodUseCheck : String -> Bool
  , errorMessage : String
  , errorDetails : String
  }

Then you’d loop over this list to handle all the functions. You’d need it to be more configurable if you want to target functions that work with number literals (like your percent example), List literals or more than one functions. But the idea is the same.

If the values are not being obtained from static code… Then the Result approach is likely better

Definitely. I can’t stress this enough: If it’s possible to have false negatives (not reporting something that will be unsuccessful), then don’t use this technique and especially don’t create an unsafe function.

A further downside is having to restrict to literal values - I know certain math operations will give me a valid result, but the review rules may not be so smart.

I wouldn’t call it a downside, since that means you need to resort to normal/unreviewed Elm, where you have less guarantees. The “safe unsafe” pattern just makes things simpler when you know things will be correct. Outside those boundaries, you’ll have to go back to Elm code that can fail, but which is the Elm code you know and love.

I think as more rules get built, we’ll end up with more and better tools, with type inference and computing of expressions (1 + 2 => 3). They may be tedious and CPU intensive, but they could make way for more and nicer rules.

Yes, if it is possible to recover from it, as that way you are nearest to the context in which it occured and therefore most likely to be able to make sense of it with respect to how you recover. In that case using Result.withDefault might be an acceptable way to recover.

The errors that I classify as bugs work more like runtime exceptions - they should just be passed up the stack to a top-level handler which has the responsibility to make a best effort to report them to the technical team for fixing. Its really just this class of errors that could be masked by Result.withDefault.

Right. That is the annoying case isn’t it? just want to do something very simple that you know is correct yet its type is Maybe (or Result). :+1:

1 Like

Just read the blog post, this is great :slight_smile:

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