2

Original question

LYAH, in For a Few Monads More shows this function,

solveRPN :: String -> Maybe Double  
solveRPN st = do  
    [result] <- foldM foldingFunction [] (words st)  
    return result

which uses pattern-matching in conjunction with do expression to esure that the monad coming out of foldM wraps a singleton list.

In order to truly understand the nature of the do expression as well as of Monad, I have been rewriting most example from that book using >>= and >> instead of the do expression, a practice which is suggested somewhere in Real World Haskell too, but I don't remember which chapter.

As regards the function above, I'm a bit puzzled. What is the most concise way to write it without using the do expression? The best I could come up with is

solveRPN :: String -> Maybe Double
solveRPN s = foldM step [] (words s) >>= \x -> case x of
                                               [y] -> Just y
                                               _   -> Nothing

but I was hoping in something cleaner, as this one is pretty noisy for 2 reasons:

  • it uses a lambda
  • it uses the case expression, which doesn't look much nicer than a if-then-else.

This question is related to the present one.

I asked another question which actually highlights the basic issue in this one:

How do I pull the head out of a list and succeed only for singleton lists?

And this has nothing to do with the Maybe that wraps the result in solveRPN.

Update

The answer I accepted there proposes a clear solution the question above:

func :: [a] -> a
func = foldr1 (const (const undefined))

which can be used to write easily solveRPN in pointfree style:

solveRPN :: String -> Maybe Double
solveRPN st = foldM foldingFunction [] (words st) >>= Just . foldr1 (const (const undefined))

However, this absolutely unsatisfying, as it does not fully take advantage of the Maybe monad, failing at runtime instead of returning Nothing when the output is incorrect.

I think that playing around with that could lead me or someone else to answer my original question without do/case/helpers.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 6
    Not many real options here. You could use a monad comprehension (which is `do` in disguise). Or, for a marginal gain, you could use `\case [y] -> ... ; _ -> ...` Desugaring `do` with patterns leads to a more verbose code: that's why the sugar is nice to have. – chi Jun 07 '20 at 21:47
  • 5
    Regarding your last point, `case` is a lot nicer than if/then/else. It brings the pattern's constituent parts into scope when it succeeds, whereas the `then` part of an `if` just has the knowledge that some Boolean expression is true. – amalloy Jun 07 '20 at 23:26

1 Answers1

1

The desugaring of do notation specified in the Haskell Report actually includes a pattern match in order to handle pattern match failure with fail, now specified in the MonadFail typeclass. It may be written as a case or as a function.

do {e}                 =  e
do {e;stmts}           =  e >> do {stmts}
do {p <- e; stmts}     =  let ok p = do {stmts}
                              ok _ = fail "..."
                            in e >>= ok
do {let decls; stmts}  =  let decls in do {stmts}

So in your example, that could look like this.

solveRPN :: String -> Maybe Double  
solveRPN st = foldM foldingFunction [] (words st) >>= \ x -> case x of
    [result] -> return result
    _ -> fail "Pattern match failure in do expression at file:line:col"

(And of course fail message :: Maybe a is just Nothing.)

You can make this slightly more concise with LambdaCase to avoid the extra variable.

{-# LANGUAGE LambdaCase #-}

solveRPN :: String -> Maybe Double  
solveRPN st = foldM foldingFunction [] (words st) >>= \ case
    [result] -> return result
    _ -> fail "Pattern match failure in do expression at file:line:col"

That’s the standard desugaring, but if you want to golf this further, you can use a helper function. The standard listToMaybe :: [a] -> Maybe a works if you want to allow non-singleton lists.

import Data.Maybe (listToMaybe)

solveRPN :: String -> Maybe Double  
solveRPN st = foldM foldingFunction [] (words st) >>= listToMaybe

There is no standard singletonToMaybe but you can easily write one.

singletonToMaybe :: [a] -> Maybe a
singletonToMaybe [x] = Just x
singletonToMaybe _   = Nothing

-- or:

singletonToMaybe xs = guard (isSingleton xs) *> listToMaybe xs

isSingleton = null . drop 1

Then write the function in point-free style using the monadic composition operators <=< or >=>.

solveRPN :: String -> Maybe Double  
solveRPN = singletonToMaybe <=< foldM foldingFunction [] . words
Jon Purdy
  • 53,300
  • 8
  • 96
  • 166
  • Nice answer, but the last proposed solution is actually faulty, as it allows a non-`Nothing` result even if the list piped through `<=<` contains more than one element, wichi is contrast with what I have just bolded in my question. – Enlico Jun 08 '20 at 20:43
  • @EnricoMariaDeAngelis: Right you are; I’ve added some clarification on that point but I don’t think much improvement is possible without helper functions that, sadly, aren’t defined in `base`. – Jon Purdy Jun 08 '20 at 23:10