haskell / haskell/attoparsec

`satisfyMaybe` parser

Open
#234 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Haskell
Stars
531
Forks
98
PR merge metrics
No merged PRs in 30d

Description

While working on a parser recently, I thought that it would be useful to have a function like `satisfy` but that also returned more information about the character in question. I figured the best idiom would be a variant of `satisfy` that took a `Char/Word8 -> Maybe a` "predicate" instead of a standard `Char/Word8 -> Bool` predicate.

These functions can implemented very similarly to how `satisfy` is implemented:

```haskell
-- | The parser @satisfyMaybe f@ succeeds on any character @c@ for which
-- @f c@ returns (`Just` @x@). This parser returns both the parsed character
-- and the result of @f c@.
satisfyMaybe :: (Char -> Maybe a) -> Parser (Char, a)
satisfyMaybe p = do
(k,c) <- ensure 1
let !h = T.unsafeHead c
case p h of
Just x -> advance k >> return (h,x)
Nothing -> fail "satisfyMaybe"

-- | Like `satisfyMaybe`, but doesn't return
-- the parsed character.
satisfyMaybe_ :: (Char -> Maybe a) -> Parser a
satisfyMaybe_ p = do
(k,c) <- ensure 1
let !h = T.unsafeHead c
case p h of
Just x -> advance k >> return x
Nothing -> fail "satisfyMaybe_"
```

Currently, there's a few ways of mimicking this behaviour, but they have their shortcomings and wouldn't be as efficient (I assume) as a dedicated low-level parser.

```haskell
-- Uses an incomplete pattern match.
satisfyMaybe :: (Char -> Maybe a) -> Parser (Char, a)
satisfyMaybe p = do
c <- satisfy (isJust . p)
let (Just x) = p c
return (c,x)

-- Does backtracking on failure.
satisfyMaybe p = do
c <- anyChar
maybe (fail "satisfyMaybe") (return . (c,)) (p c)

-- Uses a partial function.
satisfyMaybe_ :: (Char -> Maybe a) -> Parser a
satisfyMaybe_ f = fromJust <$> satisfyWith p isJust
```

However, one use case for `satisfyMaybe` could also be obtained by just breaking up the predicate of `satisfyMaybe` and using a chain of `<|>` and `satisfy`.

```haskell
data CharClass = Class1 | Class2 | Class3 deriving (Show,Eq)

pred1, pred2, pred3 :: Char -> Bool
pred1 = ...
pred2 = ...
pred3 = ...

classifyChar :: Char -> Maybe CharClass
classifyChar c
| pred1 c = Just Class1
| pred2 c = Just Class2
| pred3 c = Just Class3
| otherwise = Nothing

parserV1 :: Parser (Char, CharClass)
parserV1 = satisfyMaybe classifyChar

parserV2 :: Parser (Char, CharClass)
parserV2
= ((,Class1) <$> satisfy pred1)
<|> ((,Class2) <$> satisfy pred2)
<|> ((,Class3) <$> satisfy pred3)
```

However, using `satisfyMaybe` would give more control over the overall predicate and allow short-circuiting of certain failstates. This is especially useful if some of the predicates are more expensive than others. e.g.

```haskell
-- So long as pred0, pred1, or pred2 holds,
-- this operation is cheap.
classifyChar2 :: Char -> Maybe CharClass
classifyChar2 c
| pred0 c = Nothing -- cheap
| pred1 c = Just Class1 -- cheap
| pred2 c = Just Class2 -- cheap
| pred3 c = Just Class3 -- expensive
| otherwise = Nothing
```

This can actually be quite common if all members of `Class1/Class2/Class3` all fall within a certain range of Unicode, where `pred0` can just test whether a character falls outside that range.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.