Reputation: 33991
I occasionally run into a minor issue where I've got a set of nested case statements, which becomes cumbersome to handle. Is there any technique / pattern I can use to possibly have a list of functions (which would be equivalent to the examples's case ...
statements), evaluate all of them, and pick the first that matches a pattern (example: Right x
)?
A specific issue I have with putting them in a list, is they won't necessarily be of the same type (the monomorphism restriction I think). For example the following:
let possibleTags = [
parse (parseFileReference) "file reference" xStr
, parse (parseGitDiffReference) "git diff tag" xStr
]
Produces the following error:
• Couldn't match type ‘GitDiffReference’ with ‘FileReference’
Expected type: Either ParseError FileReference
Actual type: Either ParseError GitDiffReference
• In the expression:
parse (parseGitDiffReference) "git diff tag" xStr
In the expression:
[parse (parseFileReference) "file reference" xStr,
parse (parseGitDiffReference) "git diff tag" xStr]
In an equation for ‘possibleTags’:
possibleTags
= [parse (parseFileReference) "file reference" xStr,
parse (parseGitDiffReference) "git diff tag" xStr]
abc :: Int -> String
abc = undefined
abc2 :: Int -> Float
abc2 = undefined
abc3 :: Int -> Int
abc3 = undefined
example :: Int -> Maybe String
example x = case abc x of
("yes") -> Just "abc"
("no") -> case abc2 x of
1.0 -> Just "abc2"
2.0 -> case abc3 x of
100 -> Just "abc3"
200 -> Nothing
I'm ideally looking for something like the below (not valid code):
-- Psuedo code
example :: Int -> Maybe String
example =
if (abc x && "yes") then Just "abc"
if (abc2 x && 1.0) then Just "abc2"
if (abc3 x && 100) then Just "abc3"
The problem with using if
conditional is I can't(to my knowledge) do a pattern match, for example if (Just x)
.
Upvotes: 5
Views: 1961
Reputation: 116174
Translating the exact example (keeping the non-exhaustive pattern matching as it is):
import Data.Foldable
example2 :: Int -> Maybe String
example2 x = asum tests
where
tests =
[ case abc x of
"yes" -> Just "abc"
"no" -> Nothing
, case abc2 x of
1.0 -> Just "abc2"
2.0 -> Nothing
, case abc3 x of
100 -> Just "abc3"
200 -> Nothing
]
Upvotes: 6
Reputation: 70307
I'm going to work with your original example. A contrived example like your second one is unlikely to simplify nicely, whereas your first one looks like monad transformer code is actually something you might see in production.
Looking for a general "if-then-case" statement like you seem to is not going to end fruitfully. That's because case-statements are really the catch-all in Haskell, and reductions are going to be more specific, not more general, than case
. So with that in mind, let's take a look at your code.
parseLine :: Text -> IO (Either String Text)
parseLine x = do
let xStr = convertString x :: String
case parse (parseFileReference) "file reference" xStr of
Right z -> do
print z
fileReferenceContent z >>= \case
Just v' -> return2x $ "```\n" <> v' <> "```"
Nothing -> return $ Left "Unable to parse"
Left _ -> case parse (parseGitDiffReference) "git diff tag" xStr of
Right z -> do
print z
return2x $ ""
Left _ -> case parse (parsePossibleTag) "possible tag" xStr of
Right _ -> return $ Left $ "Tag that failed to match: " ++ xStr
Left _ -> return2x x
In each case, when you parse, you ignore the contents if it's a Left
. So let's strip out that pointless data that you ignore.
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe (Left _) = Nothing
eitherToMaybe (Right x) = Just x
parse' :: Foo0 -> String -> String -> Maybe Bar -- where Foo0 and Bar are the correct types
parse' foo s0 s1 = eitherToMaybe $ parse foo s0 s1
Then call parse'
instead of parse
. This doesn't, as yet, improve anything. But let's refactor a bit more and then we'll see what it gets us. I want all of the "success" cases (which were Right z
before and are Just z
in the new parse'
code) to be in a where
block.
parseLine x = do
let xStr = convertString x :: String
case parse' (parseFileReference) "file reference" xStr of
Just z -> fileRef z
Nothing -> case parse' (parseGitDiffReference) "git diff tag" xStr of
Just z -> gitDiff z
Nothing -> case parse' (parsePossibleTag) "possible tag" xStr of
Just _ -> tagMatch xStr
Nothing -> return2x x
where fileRef z = do
print z
fileReferenceContent z >>= \case
Just v' -> return2x $ "```\n" <> v' <> "```"
Nothing -> return $ Left "Unable to parse"
gitDiff z = do
print z
return2x ""
tagMatch xStr = do
return (Left $ "Tag that failed to match: " ++ xStr)
Once again, no real benefit yet. But here's the thing: Maybe
has an Alternative
instance that does exactly what you want, whereas Either
does not. Specifically, it will select the first successful match while ignoring the others. Let's pull out the matches and then go from there.
parseLine :: Text -> IO (Either String Text)
parseLine x = do
let xStr = convertString x :: String
case parseFile xStr of
Just z -> fileRef z
Nothing -> case parseDiff xStr of
Just z -> gitDiff z
Nothing -> case parseTag xStr of
Just _ -> tagMatch xStr
Nothing -> return2x x
where parseFile = parse' (parseFileReference) "file reference"
parseDiff = parse' (parseGitDiffReference) "git diff tag"
parseTag = parse' (parsePossibleTag) "possible tag"
fileRef z = do
print z
fileReferenceContent z >>= \case
Just v' -> return2x $ "```\n" <> v' <> "```"
Nothing -> return $ Left "Unable to parse"
gitDiff z = do
print z
return2x ""
tagMatch xStr = do
return (Left $ "Tag that failed to match: " ++ xStr)
Now here's where the magic happens. Alternative
for Maybe
will select the first successful match. So we're going to use asum
, which sums up a list of alternatives using the Alternative
instance.
parseLine :: Text -> IO (Either String Text)
parseLine x = do
let xStr = convertString x :: String
let result = asum [
fileRef <$> parseFile xStr,
gitDiff <$> parseDiff xStr,
tagMatch xStr <$ parseTag xStr -- Not a typo, the operator is (<$) since you ignore the value
]
maybe failure id result
where ...
failure = return2x x
At the end, we're left with result :: Maybe (IO (Either String Text)). If it exists, we want to return it unmodified. If it doesn't, we want to fall back to
failure. So we use
maybe`, which is more compact than a full case statement here.
Finally, in fileRef
we can factor out the pattern of "custom error message on failure".
maybeToEither :: a -> Maybe b -> Either a b
maybeToEither x Nothing = Left x
maybeToEither _ (Just y) = Right y
Then that statement becomes
fileRef z = do
print z
value <- maybeToEither "Unable to parse" <$> fileReferenceContent z
return $ (\v' -> "```\n" <> v' <> "```") <$> value
So the final code looks like
eitherToMaybe :: Either a b -> Maybe b
eitherToMaybe (Left _) = Nothing
eitherToMaybe (Right x) = Just x
parse' :: Foo0 -> String -> String -> Maybe Bar -- where Foo0 and Bar are the correct types
parse' foo s0 s1 = eitherToMaybe $ parse foo s0 s1
maybeToEither :: a -> Maybe b -> Either a b
maybeToEither x Nothing = Left x
maybeToEither _ (Just y) = Right y
parseLine :: Text -> IO (Either String Text)
parseLine x = do
let xStr = convertString x :: String
let result = asum [
fileRef <$> parseFile xStr,
gitDiff <$> parseDiff xStr,
tagMatch xStr <$ parseTag xStr
]
maybe failure id result
where parseFile = parse' (parseFileReference) "file reference"
parseDiff = parse' (parseGitDiffReference) "git diff tag"
parseTag = parse' (parsePossibleTag) "possible tag"
fileRef z = do
print z
value <- maybeToEither "Unable to parse" <$> fileReferenceContent z
return $ (\v' -> "```\n" <> v' <> "```") <$> value
gitDiff z = do
print z
return2x ""
tagMatch xStr = do
return (Left $ "Tag that failed to match: " ++ xStr)
failure = return2x x
It's not shorter than the original code, but it has less nested case alternatives, and it certainly reads more linearly.
Upvotes: 3
Reputation: 120731
At least when the result is Maybe
, this looks like a clear-cut use case for the Alternative
instance:
example x = ("abc" <$ guard (abc x=="yes"))
<|>("abc2" <$ guard (abc2 x==1))
<|>("abc3" <$ guard (abc3 x==100))
Upvotes: 5