Implementing Cond in Elm

Implementing Cond in Elm

I’'m a great fan of the case statement, but there are some times when I miss the cond function found in Lisp, Racket, etc. This occasionally comes up when there is a series of conditions that have to be checked, which of course you can do with a nested if-then-else construct. But that leads, in my experience, to fragile code that can be hard to read and maintain. Besides, as a new and enthusiastic user of elm-review, I am trying to reduce the cyclomatic complexity of my code, and nested if-then-elses can boost it way up.

A good solution is to use cond, but Elm doesn’t have this. No big deal — as shown below, it is easy enough to hack a simple version together using Step and loop, which are also user-defined.

Note that if a condition is satisfied, no further conditions will be evaluated.

Example.

    > conditions = [  \x -> x >= 0 && x < 1
                    , \x -> x >= 1 && x < 2
                    , \x -> x >= 2 && x < 3  ] 

    > outputs = ["red", "green", "blue"]

    > cond conditions outputs 1.5
    Just "green"

Code.

cond : List (a -> Bool) -> List b -> a -> Maybe b
cond conditions outputs input =
  loop {  conditions = conditions 
        , outputs = outputs
        , input = input} nextStep

type alias State a b = { conditions : List (a -> Bool)
                       , outputs : List b
                       , input : a}

nextStep : (State a b) -> Step (State a b) (Maybe b)
nextStep state =
    case (List.head state.conditions, List.head state.outputs) of
        (Nothing, _)                  -> Done Nothing
        (_, Nothing)                  -> Done Nothing
        (Just condition, Just output) -> if condition state.input
           then Done (Just output)
           else Loop {state | conditions = List.drop 1 state.conditions
                            , outputs = List.drop 1 state.outputs}

type Step state x
    = Loop state
    | Done x


loop : state -> (state -> Step state a) -> a
loop s f =
    case f s of
        Loop s_ ->
            loop s_ f

        Done b ->
            b

Hello,

nice one - you can simplify this a bit with map2 if you don’t mind iterating the lists completely:

cond : List (a -> Bool) -> List b -> a -> Maybe b
cond conditions outputs input =
    List.map2
        (\condition output ->
            if condition input then
                Just output

            else
                Nothing
        )
        conditions
        outputs
        |> List.filterMap identity
        |> List.head

Isn’t this asking for you to get the conditions mixed up with incorrect results? Wouldn’t it be better to have the conditions with the associated results:

conditions : List ( Float -> Bool, String)
conditions =
    [ ( \x -> x >= 0 && x < 1, "red" )
    , ( \x -> x >= 1 && x < 2, "green")
    , ( \x -> x >= 2 && x < 3 , "blue" )
    ]

Then your cond function is just basically a version of List.Extra.find:

cond : List (a -> Bool, b) -> a -> Maybe b
cond candidates arg =
    case candidates of
        [] ->
             Nothing
        ((predicate, answer) :: rest ) ->
              case predicate arg of
                    True -> Just answer
                    False -> cond rest arg

Or am I missing something?

3 Likes

Oops, see the edit below …

I prefer not iterating the lists completely — though upon reflection, it likely makes little difference if the conditions are inexpensive to evaluate. Good alternative!

Thinking about the suggestions above and also using cond in “real life,” I’ve rewritten cond to
take as its first argument data of type

List ( a -> Bool, a -> b) 

that is, a list of pairs (predicate, action) is as in @allenderek’s code to avoid misalignment. In the toy example,

data : List (number -> Bool, number -> String)
data = [
              (\x -> x >= 0 && x < 1,  \x -> "red")
            , (\x -> x >= 1 && x < 2,  \x -> "green")
            , (\x -> x >= 2 && x < 3,  \x -> "blue")
        ]

In a real example, the data List (predicate, action) is

schedulerConfig : List ((Model -> Bool), Model -> (Model, "Cmd.msg"))
schedulerConfig = [    (  noCurrentUser     , noCurrentUserAction      )
                     , ( automaticSignOutOK  ,  automaticSignoutAction  )
                     , ( timeToGetStatistics , getStatistics) 
                   ]

It is easy to add new (predicate, action) pairs…

Here is the new code:

cond : List ( a -> Bool, a -> b) ->  b -> a -> b
cond data_  default input  =
  let 
   initialState = { conditions = List.map Tuple.first data_
                  , input = input
                  , actions =  List.map Tuple.second data_
                  , default = default }
   in
   loop initialState nextCondStep

type alias CondState a  b = { conditions : List (a -> Bool)
                            , input: a
                            , actions : List (a -> b)
                            , default: b }

nextCondStep : (CondState a b) -> Step (CondState a b) b
nextCondStep state =
    case (List.head state.conditions, List.head state.actions) of
        (Just condition, Just action) ->
          if (condition state.input)
             then Done  (action state.input)
             else Loop {state | conditions = List.drop 1 state.conditions
                              , actions = List.drop 1 state.actions}
        _ -> Done state.default


1 Like

Hi. I’m still unsure why you are using loop (I don’t know where the definition of loop is).
Essentially can you explain why you are translating from an aligned data structure into an unaligned one, rather than just working with the aligned structure? If I’m honest, I think my code is somewhat more readable (it’s shorter for a start and it doesn’t require someone to know what ‘loop’ does). It trivially generalises to your a -> b extension:

cond : List (a -> Bool, a -> b) -> a -> Maybe b
cond candidates arg =
    case candidates of
        [] ->
             Nothing
        ((predicate, transform) :: rest ) ->
              case predicate arg of
                    True -> Just (transform arg)
                    False -> cond rest arg

I guess I just don’t know why you’re avoiding this?

3 Likes

Yes, your code is quite nice (I like it).