Reputation: 1744
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
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
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
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