14

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.
glennsl
  • 28,186
  • 12
  • 57
  • 75
betaveros
  • 1,360
  • 12
  • 23
  • "I have control over the index and don't expect [index out of range] to happen" -> What if the array is empty? – mgold Nov 02 '15 at 15:01
  • 1
    I updated my [nonempty list library](http://package.elm-lang.org/packages/mgold/elm-nonempty-list/1.7.0/List-Nonempty) with a `sample` function. – mgold Nov 02 '15 at 18:12
  • The two arrays I was considering when asking this question were both initialized to arrays with a fixed number of elements. They are never empty (modulo programmer error). – betaveros Nov 03 '15 at 15:29

2 Answers2

8

(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.)

betaveros
  • 1,360
  • 12
  • 23
  • 1
    I think `Debug.crash` is the right approach. You may want to give a better message than "oh no!". You can always start a new thread on the mailing list about changing the name of `Debug.crash`. – Apanatshka Feb 24 '15 at 16:26
  • In 0.16, the compiler will not allow incomplete pattern matching like `(\(Just x) -> x)`. `Debug.crash` is the way to go. – mgold Nov 23 '15 at 15:19
  • I think `Debug.crash` is a great name. It is basically saying to only use this during debugging and don't put it in production. – intrepion Apr 07 '16 at 23:28
  • 2
    That is exactly what the name suggests to me as well, but if so, how am I supposed to define `fromJust` in production? – betaveros Apr 08 '16 at 01:55
  • Honestly, neither of these is the intended use of `Maybe` or `Option` and there are other ways around this. See my answer: http://stackoverflow.com/a/41656722/2895548. – Mezuzza Jan 15 '17 at 01:11
  • Since Elm 0.19, `Debug.crash` is no more and you'll have to find another way to deal with this. – damd Mar 03 '20 at 15:41
5

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
Mezuzza
  • 419
  • 4
  • 14
  • 1
    I get the general sentiment, but I explicitly stated my use case: I have an `Array a`, which I know to be non-empty (because it is a constant), and I want to sample a random element, but the functions in Elm's API I found for this give me a `Maybe a` (because they were designed for general `Array a`s, which could be empty and/or have some indexes be out-of-bounds for them). Do you have suggestions for this case? The nonempty list type in a comment is nice, but no longer O(1), and in general I can't expect to be able to encode the impossibility of every similar failure mode in the type system. – betaveros Jan 17 '17 at 03:03
  • So couple thoughts: 1) You can select a random element from a list which you correctly point out is not O(1)...Unless you know that the size of the list is a small constant, in which case who cares, use the list. List has a better interface in Elm. 2) If this is for a small toy project, then don't worry about it. Use your Debug.crash implementation. 3) If you're doing this for a larger production database, then attempt to implement this as cleanly as possible. Finally, you can expect to encode impossibility in a lot of cases. I've been bitten by not doing so before. – Mezuzza Jan 25 '17 at 05:11
  • 1
    If this specific use case were hypothetically for a larger production database, you still haven't given me any concrete details about how to "implement this as cleanly as possible"... – betaveros Jan 26 '17 at 13:23