Evan Sebastian
Evan Sebastian

Reputation: 1744

Change this to do notation

I am doing a project with Hspec and Parsec, and stumbled upon this following code.

stringLiteralSpec :: Spec
stringLiteralSpec =
    describe "SimpleExpression:StringLiteral" $
        it "Is able to parse correct string literals" $
            stringLiteral `shouldParse` [
                "\"Hello World\"" `to` StringLiteral "Hello World",
                "\'Hello World\'" `to` StringLiteral "Hello World"]

shouldParse :: (Show a, Eq a) => Parser a -> [(String, a)] -> Expectation

to = (,)

Is it possible to somehow come up with another definition of to such that the list notation could be written in a prettier way like this?

            stringLiteral `shouldParse` $ do
                "\"Hello World\"" `to` StringLiteral "Hello World"
                "\'Hello World\'" `to` StringLiteral "Hello World"

Upvotes: 0

Views: 109

Answers (3)

David Young
David Young

Reputation: 10783

If we use the Writer Monad, we can collect singleton lists together. Writer keeps track of a Monoid that you can mappend things to (in this case, the [a] Monoid) as it interprets the Monad/Applicative actions. It would look something like this

to :: a -> b -> Writer [(a, b)] ()
x `to` y = tell [(x, y)]

Now we can write:

stringLiteral `shouldParse` execWriter (do
            "\"Hello World\"" `to` StringLiteral "Hello World"
            "\'Hello World\'" `to` StringLiteral "Hello World")

Here is an example of how this to implementation works, w.r.t. its Monad instance

λ> execWriter $ do { 1 `to` 2; 10 `to` 100 }
[(1,2),(10,100)]

Notice you have to remove the Writer "wrapping" from the value we actually want to get at.

Also, we don't actually make full use of the Monad since we never bind anything to a name, we just ignore the result. Note that

do
  a
  b

is the same as

a >> b

which is required to result in the same value as

a >>= (\_ -> b)

which is the default implementation of (>>).

This is also the same value as

a *> b

from the corresponding Applicative instance. So, this would only be used to take advantage of the notational convenience of do notation, but we lose some of that due to the extra Writer wrapping. Internally, Writer is just a pair so we would still have to extract the list from the first element of the pair. There isn't really a way around that.

The [] Monad doesn't work for this because it doesn't append the results of the actions in this way. It's not possible to implement a thin newtype wrapper around [] that does this either, because the (>>=) :: [a] -> (a -> [b]) -> [b] method (or, more to the point, the (>>) :: [a] -> [b] -> [b] method) can't behave in this way, essentially because it doesn't know if a and b are the same type so it can't just append those two lists (I've specialized the types here to the [] instance for readability).

I would stick with your original list notation personally, since its less verbose and easier to immediately understand.

Upvotes: 4

dsemi
dsemi

Reputation: 640

stringLiteral `shouldParse` [
    "\"Hello World\"" `to` StringLiteral "Hello World"
  , "'Hello World'" `to` StringLiteral "Hello World" ]

Is that significantly uglier? The only way I can think of to write this in do-notation would be to bind a list to a value and return it, you would still need the explicit brackets.

On the other hand if you have a function that can map each quoted word to its bare counterpart, you could do something along the lines of:

-- f is your quote stripping function
map (\str -> (str, f str)) ["\"Hello World\"", "'Hello World'"]

-- or

[(str, f str) | str <- ["\"Hello World\"", "'Hello World'"]]

-- which can be rewritten as

do
  str <- ["\"Hello World\"", "'Hello World'"]
  return (str, f str)

Upvotes: 0

Alexander Vieth
Alexander Vieth

Reputation: 876

In that snippet, a String is paired with the datum to which it should parse directly via the function to. Another option is to pair a list of Strings with the data to which each element should parse, via a monadic bind:

-- Implement this function which, for instance, maps "\"Hello World\""
-- to StringLiteral "Hello World" 
stringToDatum :: String -> a

-- With stringToDatum in hand, we can coerce it into a bind by pairing its
-- output with its input, and using the list return.
["\"Hello World\"", "\'Hello World\'"] >>= (\str -> return (str, stringToDatum str))

You could then do something like

stringLiteral `shouldParse` $ do
  str <- listOfStrings
  return (str, stringToDatum str)

To get something like

stringLiteral `shouldParse` $ do
  "\"Hello World\"" `to` StringLiteral "Hello World"
  "\'Hello World\'" `to` StringLiteral "Hello World"

the output of to would have to be a list, and under the standard list monad the value of the expression on the second line of the do block would be duplicated for each value of the list given by the first line of the do block, which is definitely not what we want.

Upvotes: 0

Related Questions