Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Right way to forcibly convert Maybe a to a in Elm, failing clearly for Nothings

Okay, what I really wanted to do is, I have an Array and I want to choose a random element from it. The obvious thing to do is get an integer from a random number generator between 0 and the length minus 1, which I have working already, and then applying Array.get, but that returns a Maybe a. (It appears there's also a package function that does the same thing.) Coming from Haskell, I get the type significance that it's protecting me from the case where my index was out of range, but I have control over the index and don't expect that to happen, so I'd just like to assume I got a Just something and somewhat forcibly convert to a. In Haskell this would be fromJust or, if I was feeling verbose, fromMaybe (error "some message"). How should I do this in Elm?

I found a discussion on the mailing list that seems to be discussing this, but it's been a while and I don't see the function I want in the standard library where the discussion suggests it would be.

Here are some pretty unsatisfying potential solutions I found so far:

  • Just use withDefault. I do have a default value of a available, but I don't like this as it gives the completely wrong meaning to my code and will probably make debugging harder down the road.
  • Do some fiddling with ports to interface with Javascript and get an exception thrown there if it's Nothing. I haven't carefully investigated how this works yet, but apparently it's possible. But this just seems to mix up too many dependencies for what would otherwise be simple pure Elm.
like image 777
betaveros Avatar asked Feb 24 '15 15:02

betaveros


2 Answers

Solution

The existence or use of a fromJust or equivalent function is actually code smell and tells you that the API has not been designed correctly. The problem is that you're attempting to make a decision on what to do before you have the information to do it. You can think of this in two cases:

  1. If you know what you're supposed to do with Nothing, then the solution is simple: use withDefault. This will become obvious when you're looking at the right point in your code.

  2. If you don't know what you're supposed to do in the case where you have Nothing, but you still want to make a change, then you need a different way of doing so. Instead of pulling the value out of the Maybe use Maybe.map to change the value while keeping the Maybe. As an example, let's say you're doing the following:

    foo : Maybe Int -> Int
    foo maybeVal =
      let
        innerVal = fromJust maybeVal
      in
        innerVal + 2
    

    Instead, you'll want this:

    foo : Maybe Int -> Maybe Int
    foo maybeVal =
        Maybe.map (\innerVal -> innerVal + 2) maybeVal
    

    Notice that the change you wanted is still done in this case, you've simply not handled the case where you have a Nothing. You can now pass this value up and down the call chain until you've hit a place where it's natural to use withDefault to get rid of the Maybe.

What's happened is that we've separated the concerns of "How do I change this value" and "What do I do when it doesn't exist?". We deal with the former using Maybe.map and the latter with Maybe.withDefault.

Caveat

There are a small number of cases where you simply know that you have a Just value and need to eliminate it using fromJust as you described, but those cases should be few and far between. There's quite a few that actually have a simpler alternative.

Example: Attempting to filter a list and get the value out.

Let's say you have a list of Maybes that you want the values of. A common strategy might be:

foo : List (Maybe a) -> List a
foo hasAnything =
  let
    onlyHasJustValues = List.filter Maybe.isJust hasAnything
    onlyHasRealValues = List.map fromJust onlyHasJustValues
  in
    onlyHasRealValues

Turns out that even in this case, there are clean ways to avoid fromJust. Most languages with a collection that has a map and a filter have a method to filter using a Maybe built in. Haskell has Maybe.mapMaybe, Scala has flatMap, and Elm has List.filterMap. This transforms your code into:

foo : List (Maybe a) -> List a
foo hasAnything =
  let
    onlyHasRealValues = List.filterMap (\x -> x) hasAnything
  in
    onlyHasRealValues
like image 71
Mezuzza Avatar answered Oct 22 '22 13:10

Mezuzza


(answering my own question)

I found two more-satisfying solutions:

  • Roll my own partially defined function, which was referenced elsewhere in the linked discussion. But the code kind of feels incomplete this way (I'd hope the compiler would warn me about incomplete pattern matches some day) and the error message is still unclear.
  • Pattern-match and use Debug.crash if it's a Nothing. This appears similar to Haskell's error and is the solution I'm leaning towards right now.

    import Debug
    
    fromJust : Maybe a -> a
    fromJust x = case x of
        Just y -> y
        Nothing -> Debug.crash "error: fromJust Nothing"
    

    (Still, the module name and description also make me hesitate because it doesn't seem like the "right" method intended for my purposes; I want to indicate true programmer error instead of mere debugging.)

like image 28
betaveros Avatar answered Oct 22 '22 14:10

betaveros