replace-megaparsec 1.0.1.0 → 1.1.0.0
raw patch · 7 files changed
+379/−134 lines, 7 filesdep +criterionPVP ok
version bump matches the API change (PVP)
Dependencies added: criterion
API changes (from Hackage documentation)
Files
- README.md +44/−11
- bench/BenchUnit.hs +156/−0
- replace-megaparsec.cabal +77/−28
- src/Replace/Megaparsec.hs +23/−12
- tests/TestByteString.hs +30/−31
- tests/TestString.hs +23/−24
- tests/TestText.hs +26/−28
README.md view
@@ -8,7 +8,7 @@ replacing the found patterns. This activity is traditionally done with regular expressions, but __replace-megaparsec__ uses-[__Megaparsec__](http://hackage.haskell.org/package/megaparsec)+[__megaparsec__](http://hackage.haskell.org/package/megaparsec) parsers instead for the pattern matching. __replace-megaparsec__ can be used in the same sort of “pattern capture”@@ -57,7 +57,7 @@ [regular](https://en.wikipedia.org/wiki/Chomsky_hierarchy#The_hierarchy) grammers. Parsers are able pattern-match with context-free grammers, and- even context-sensitive or Turing grammers, if needed. See below for+ even context-sensitive grammers, if needed. See below for an example of lifting a `Parser` into a `State` monad for context-sensitive pattern-matching. @@ -88,7 +88,7 @@ and deconstruct the string of text by separating it into sections which match the pattern, and sections which don't match. -#### Pattern match, capture only the parsed result+#### Pattern match, capture only the parsed result with `sepCap` Separate the input string into sections which can be parsed as a hexadecimal number with a prefix `"0x"`, and sections which can't.@@ -101,7 +101,7 @@ [Right 10,Left " 000 ",Right 65535] ``` -#### Pattern match, capture only the matched text+#### Pattern match, capture only the matched text with `findAll` Just get the strings sections which match the hexadecimal parser, throw away the parsed number.@@ -114,7 +114,7 @@ [Right "0xA",Left " 000 ",Right "0xFFFF"] ``` -#### Pattern match, capture the matched text and the parsed result+#### Pattern match, capture the matched text and the parsed result with `findAllCap` Capture the parsed hexadecimal number, as well as the string section which parses as a hexadecimal number.@@ -227,10 +227,43 @@ <http://hackage.haskell.org/package/template> -## Motivation+<http://hackage.haskell.org/package/regex-applicative> -I wanted to scan a Markdown document and find tokens inside backticks that-look like a Haskell identifier, then look up the identifier in Hoogle to-see if it has a definition in __base__, and if so, insert a Hackage link-for the identifier into the Markdown. I couldn't find a simple and-obvious way to do that with any existing technology.+## Hypothetically Asked Questions++1. *Is it fast?*++ lol not really. `sepCap` is fundamentally about consuming the stream one+ token at a time while we try and fail to run a parser and then+ backtrack each time. That's+ [a slow activity](https://markkarpov.com/megaparsec/megaparsec.html#writing-efficient-parsers).++ Consider a 1 megabyte file that consists of `"foo"` every ten bytes:++ ```+ foo foo foo foo foo foo ...+ ```++ We want to replace all the `"foo"` with `"bar"`. We would expect `sed`+ to be about at the upper bound of speed for this task, so here+ are the `perf` results when we compare `sed`+ to __replace-megaparsec__ with some different stream types.++ | Method | `perf task-clock` |+ | :--- | ---: |+ | `sed s/foo/bar/g` | 39 msec |+ | `streamEdit String` | 793 msec |+ | `streamEdit ByteString` | 513 msec |+ | `streamEdit Text` | 428 msec |++2. *Could we write this library for __parsec__?*++ No, because the+ [`match`](https://hackage.haskell.org/package/megaparsec/docs/Text-Megaparsec.html#v:match)+ combinator doesn't exist for __parsec__. (I can't find it anywhere.+ [Can it be written?](http://www.serpentine.com/blog/2014/05/31/attoparsec/#from-strings-to-buffers-and-cursors))++3. *Could we write this library for __attoparsec__?*++ I think so, but I wouldn't expect much of a speed improvement, because+ again, `sepCap` is a fundamentally slow activity.
+ bench/BenchUnit.hs view
@@ -0,0 +1,156 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE BangPatterns #-}++import Text.Megaparsec+import Replace.Megaparsec+import Criterion.Main+import Criterion.Types+import qualified Data.ByteString as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.ByteString.Builder as B.Builder+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import Data.Void++fooStringM :: String+fooStringM = take 1000000 $ cycle " foo" -- a million bytes of foos+fooString10K :: String+fooString10K = take 10000 fooStringM+fooString100K :: String+fooString100K = take 100000 fooStringM++fooByteStringM :: B.ByteString+fooByteStringM = BL.toStrict fooByteStringLM+fooByteString10K :: B.ByteString+fooByteString10K = B.take 10000 fooByteStringM+fooByteString100K :: B.ByteString+fooByteString100K = B.take 100000 fooByteStringM++fooByteStringLM :: BL.ByteString+fooByteStringLM = B.Builder.toLazyByteString $ B.Builder.string8 fooStringM+fooByteStringL10K :: BL.ByteString+fooByteStringL10K = BL.take 10000 fooByteStringLM+fooByteStringL100K :: BL.ByteString+fooByteStringL100K = BL.take 100000 fooByteStringLM++fooTextM :: T.Text+fooTextM = T.pack fooStringM+fooText10K :: T.Text+fooText10K = T.take 10000 fooTextM+fooText100K :: T.Text+fooText100K = T.take 100000 fooTextM++fooTextLM :: TL.Text+fooTextLM = TL.pack fooStringM+fooTextL10K :: TL.Text+fooTextL10K = TL.take 10000 fooTextLM+fooTextL100K :: TL.Text+fooTextL100K = TL.take 100000 fooTextLM++main :: IO ()+main = defaultMainWith (defaultConfig+ { reportFile = Just "criterion-report.html"+ , resamples = 100+ })+ [ bgroup "String"+ [ bench "sepCap 10,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+ fooString10K+ , bench "streamEdit 10,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+ fooString10K+ , bench "sepCap 100,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+ fooString100K+ , bench "streamEdit 100,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+ fooString100K+ , bench "sepCap 1,000,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void String String)))+ fooStringM+ , bench "streamEdit 1,000,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void String String) (const "bar"))+ fooStringM+ ]+ , bgroup "ByteString.Strict"+ [ bench "sepCap 10,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+ fooByteString10K+ , bench "streamEdit 10,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+ fooByteString10K+ , bench "sepCap 100,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+ fooByteString100K+ , bench "streamEdit 100,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+ fooByteString100K+ , bench "sepCap 1,000,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void B.ByteString B.ByteString)))+ fooByteStringM+ , bench "streamEdit 1,000,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void B.ByteString B.ByteString) (const "bar"))+ fooByteStringM+ ]+ , bgroup "ByteString.Lazy"+ [ bench "sepCap 10,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+ fooByteStringL10K+ , bench "streamEdit 10,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+ fooByteStringL10K+ , bench "sepCap 100,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+ fooByteStringL100K+ , bench "streamEdit 100,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+ fooByteStringL100K+ , bench "sepCap 1,000,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString)))+ fooByteStringLM+ , bench "streamEdit 1,000,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void BL.ByteString BL.ByteString) (const "bar"))+ fooByteStringLM+ ]+ , bgroup "Text.Strict"+ [ bench "sepCap 10,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+ fooText10K+ , bench "streamEdit 10,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+ fooText10K+ , bench "sepCap 100,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+ fooText100K+ , bench "streamEdit 100,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+ fooText100K+ , bench "sepCap 1,000,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void T.Text T.Text)))+ fooTextM+ , bench "streamEdit 1,000,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void T.Text T.Text) (const "bar"))+ fooTextM+ ]+ , bgroup "Text.Lazy"+ [ bench "sepCap 10,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+ fooTextL10K+ , bench "streamEdit 10,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+ fooTextL10K+ , bench "sepCap 100,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+ fooTextL100K+ , bench "streamEdit 100,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+ fooTextL100K+ , bench "sepCap 1,000,000" $ whnf+ (parseMaybe (sepCap (chunk "foo" :: Parsec Void TL.Text TL.Text)))+ fooTextLM+ , bench "streamEdit 1,000,000" $ whnf+ (streamEdit (chunk "foo" :: Parsec Void TL.Text TL.Text) (const "bar"))+ fooTextLM+ ]+ ]+
replace-megaparsec.cabal view
@@ -1,5 +1,5 @@ name: replace-megaparsec-version: 1.0.1.0+version: 1.1.0.0 cabal-version: 1.18 synopsis: Stream editing with parsers homepage: https://github.com/jamesdbrock/replace-megaparsec@@ -24,7 +24,6 @@ library hs-source-dirs: src- -- rely on megaparsec for version bounds build-depends: base >= 4.0 && < 5.0 , megaparsec default-language: Haskell2010@@ -32,33 +31,83 @@ ghc-options: -O2 -Wall test-suite test-string- type: detailed-0.9- test-module: TestString- hs-source-dirs: tests- default-language: Haskell2010- build-depends: base >= 4.0 && < 5.0- , replace-megaparsec- , megaparsec- , Cabal+ type: detailed-0.9+ test-module: TestString+ hs-source-dirs: tests+ default-language: Haskell2010+ build-depends: base >= 4.0 && < 5.0+ , replace-megaparsec+ , megaparsec+ , Cabal+ ghc-options: -Wall test-suite test-text- type: detailed-0.9- test-module: TestText- hs-source-dirs: tests- default-language: Haskell2010- build-depends: base >= 4.0 && < 5.0- , replace-megaparsec- , megaparsec- , Cabal- , text+ type: detailed-0.9+ test-module: TestText+ hs-source-dirs: tests+ default-language: Haskell2010+ build-depends: base >= 4.0 && < 5.0+ , replace-megaparsec+ , megaparsec+ , Cabal+ , text+ ghc-options: -Wall test-suite test-bytestring- type: detailed-0.9- test-module: TestByteString- hs-source-dirs: tests- default-language: Haskell2010- build-depends: base >= 4.0 && < 5.0- , replace-megaparsec- , megaparsec- , Cabal- , bytestring+ type: detailed-0.9+ test-module: TestByteString+ hs-source-dirs: tests+ default-language: Haskell2010+ build-depends: base >= 4.0 && < 5.0+ , replace-megaparsec+ , megaparsec+ , Cabal+ , bytestring+ ghc-options: -Wall+++-- Disable these executables for now, they clutter up+-- the dependency list and `cabal v2-build`.+--+-- executable bench-string+-- main-is: BenchString.hs+-- hs-source-dirs: bench+-- default-language: Haskell2010+-- build-depends: base >= 4.0 && < 5.0+-- , megaparsec+-- , replace-megaparsec+-- ghc-options: -O2 -Wall+--+-- executable bench-text+-- main-is: BenchText.hs+-- hs-source-dirs: bench+-- default-language: Haskell2010+-- build-depends: base >= 4.0 && < 5.0+-- , megaparsec+-- , replace-megaparsec+-- , text+-- ghc-options: -O2 -Wall+--+-- executable bench-bytestring+-- main-is: BenchByteString.hs+-- hs-source-dirs: bench+-- default-language: Haskell2010+-- build-depends: base >= 4.0 && < 5.0+-- , megaparsec+-- , replace-megaparsec+-- , bytestring+-- ghc-options: -O2 -Wall++benchmark bench-unit+ main-is: BenchUnit.hs+ hs-source-dirs: bench+ type: exitcode-stdio-1.0+ default-language: Haskell2010+ build-depends: base >= 4.0 && < 5.0+ , megaparsec+ , replace-megaparsec+ , text+ , bytestring+ , criterion+ ghc-options: -O2 -Wall+
src/Replace/Megaparsec.hs view
@@ -41,6 +41,8 @@ , findAllCap -- * Running parser+ --+ -- Ways to run a parser , streamEditT , streamEdit )@@ -51,11 +53,9 @@ import Data.Bifunctor import Data.Functor.Identity import Data.Proxy-import Data.Foldable import Control.Exception (throw) import Data.Typeable import Control.Monad- import Text.Megaparsec -- |@@ -90,6 +90,7 @@ -- but, importantly, it returns the parsed result of the @sep@ parser instead -- of throwing it away. --+{-# INLINABLE sepCap #-} sepCap :: forall e s m a. (MonadParsec e s m) => m a -- ^ The pattern matching parser @sep@@@ -126,6 +127,7 @@ -- @ -- findAllCap sep = 'sepCap' ('Text.Megaparsec.match' sep) -- @+{-# INLINABLE findAllCap #-} findAllCap :: MonadParsec e s m => m a -- ^ The pattern matching parser @sep@@@ -144,6 +146,7 @@ -- @ -- findAll sep = (fmap.fmap) ('Data.Bifunctor.second' fst) $ 'sepCap' ('Text.Megaparsec.match' sep) -- @+{-# INLINABLE findAll #-} findAll :: MonadParsec e s m => m a -- ^ The pattern matching parser @sep@@@ -154,7 +157,7 @@ -- | -- == Stream editor ----- Also can be considered “find-and-replace”. Finds all+-- Also known as “find-and-replace”, or “match-and-substitute”. Finds all -- of the sections of the stream which match the pattern @sep@, and replaces -- them with the result of the @editor@ function. --@@ -162,18 +165,26 @@ -- a “way to run a parser”, like 'Text.Megaparsec.parse' -- or 'Text.Megaparsec.runParserT'. ----- === Access the matched section of text in the `editor`+-- === Access the matched section of text in the @editor@ -- -- If you want access to the matched string in the @editor@ function, -- then combine the pattern parser @sep@ with 'Text.Megaparsec.match'.--- This will effectively change the type of the `editor`--- to `(s,a) -> m s`, and then we can write `editor` like:+-- This will effectively change the type of the @editor@+-- to @(s,a) -> m s@. --+-- This allows us to write an @editor@ function which can choose to not+-- edit the match and just leave it as it is. We can write an+-- @editorId@ function such that @streamEditT@ changes nothing.+-- -- @--- let editor (matchString,parseResult) = return matchString+-- editorId (matchString, parseResult) = return matchString+-- @ ----- streamEditT ('Text.Megaparsec.match' sep) editor inputString+-- implies that+-- -- @+-- streamEditT ('Text.Megaparsec.match' sep) editorId ≡ 'Control.Monad.return'+-- @ -- -- === Type constraints --@@ -188,7 +199,7 @@ -- "Data.Bytestring.Lazy", -- and "Data.String". ----- We need the @Monoid s@ instance so that we can @mappend@ the output+-- We need the @Monoid s@ instance so that we can @mconcat@ the output -- stream. -- -- We need @Typeable s@ and @Show s@ for 'Control.Exception.throw'. In theory@@ -206,6 +217,7 @@ -- -- If you want the @editor@ function or the parser @sep@ to remember some state, -- then run this in a stateful monad.+{-# INLINABLE streamEditT #-} streamEditT :: forall s m a. (Stream s, Monad m, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s) => ParsecT Void s m a@@ -219,14 +231,13 @@ streamEditT sep editor input = do runParserT (sepCap sep) "" input >>= \case (Left err) -> throw err -- sepCap can never fail, but if it does, throw- (Right r) -> fmap fold $ traverse (either return editor) r--+ (Right r) -> fmap mconcat $ traverse (either return editor) r -- | -- == Pure stream editor -- -- Pure version of 'streamEditT'.+{-# INLINABLE streamEdit #-} streamEdit :: forall s a. (Stream s, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s) => Parsec Void s a
tests/TestByteString.hs view
@@ -9,24 +9,23 @@ import Text.Megaparsec.Byte import Text.Megaparsec.Byte.Lexer import Data.Void-import qualified Data.ByteString as BS-import Data.ByteString (ByteString)+import qualified Data.ByteString as B import Data.ByteString.Internal (c2w) import GHC.Word -type Parser = Parsec Void ByteString+type Parser = Parsec Void B.ByteString tests :: IO [Test] tests = return [ Test $ runParserTest "findAll upperChar" (findAllCap (upperChar :: Parser Word8))- ("aBcD" :: ByteString)+ ("aBcD" :: B.ByteString) [Left "a", Right ("B", c2w 'B'), Left "c", Right ("D", c2w 'D')] -- check that sepCap can progress even when parser consumes nothing -- and succeeds. , Test $ runParserTest "zero-consumption parser" (sepCap (many (upperChar :: Parser Word8)))- ("aBcD" :: ByteString)+ ("aBcD" :: B.ByteString) [Left "a", Right [c2w 'B'], Left "c", Right [c2w 'D']] , Test $ runParserTest "scinum" (sepCap scinum)@@ -45,33 +44,33 @@ ("a") ([Left "a"]) ]--runParserTest name p input expected = TestInstance- { run = do- case runParser p "" input of- Left e -> return (Finished $ Fail $ show e)- Right output ->- if (output == expected)- then return (Finished Pass)- else return (Finished $ Fail- $ show output ++ " ≠ " ++ show expected)- , name = name- , tags = []- , options = []- , setOption = \_ _ -> Left "no options supported"- }+ where+ runParserTest nam p input expected = TestInstance+ { run = do+ case runParser p "" input of+ Left e -> return (Finished $ Fail $ show e)+ Right output ->+ if (output == expected)+ then return (Finished Pass)+ else return (Finished $ Fail+ $ show output ++ " ≠ " ++ show expected)+ , name = nam+ , tags = []+ , options = []+ , setOption = \_ _ -> Left "no options supported"+ } -scinum :: Parser (Double, Integer)-scinum = do- -- This won't parse mantissas that contain a decimal point,- -- but if we use the Text.Megaparsec.Byte.Lexer.float, then it consumes- -- the "E" and the exponent. Whatever, doesn't really matter for this test.- m <- fromIntegral <$> decimal- chunk "E"- e <- decimal- return (m, e)+ scinum :: Parser (Double, Integer)+ scinum = do+ -- This won't parse mantissas that contain a decimal point,+ -- but if we use the Text.Megaparsec.Byte.Lexer.float, then it consumes+ -- the "E" and the exponent. Whatever, doesn't really matter for this test.+ m <- (fromIntegral :: Integer -> Double) <$> decimal+ _ <- chunk "E"+ e <- decimal+ return (m, e) -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+ offsetA :: Parser Int+ offsetA = getOffset <* chunk "A"
tests/TestString.hs view
@@ -6,7 +6,6 @@ import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char-import Text.Megaparsec.Char.Lexer import Data.Void type Parser = Parsec Void String@@ -40,30 +39,30 @@ ("a") ([Left "a"]) ]--runParserTest name p input expected = TestInstance- { run = do- case runParser p "" input of- Left e -> return (Finished $ Fail $ show e)- Right output ->- if (output == expected)- then return (Finished Pass)- else return (Finished $ Fail- $ show output ++ " ≠ " ++ show expected)- , name = name- , tags = []- , options = []- , setOption = \_ _ -> Left "no options supported"- }+ where+ runParserTest nam p input expected = TestInstance+ { run = do+ case runParser p "" input of+ Left e -> return (Finished $ Fail $ show e)+ Right output ->+ if (output == expected)+ then return (Finished Pass)+ else return (Finished $ Fail+ $ show output ++ " ≠ " ++ show expected)+ , name = nam+ , tags = []+ , options = []+ , setOption = \_ _ -> Left "no options supported"+ } -scinum :: Parser (Double, Integer)-scinum = do- m <- some digitChar- chunk "E"- e <- some digitChar- return (read m, read e)+ scinum :: Parser (Double, Integer)+ scinum = do+ m <- some digitChar+ _ <- chunk "E"+ e <- some digitChar+ return (read m, read e) -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+ offsetA :: Parser Int+ offsetA = getOffset <* chunk "A"
tests/TestText.hs view
@@ -7,24 +7,22 @@ import Replace.Megaparsec import Text.Megaparsec import Text.Megaparsec.Char-import Text.Megaparsec.Char.Lexer import Data.Void import qualified Data.Text as T-import Data.Text (Text) -type Parser = Parsec Void Text+type Parser = Parsec Void T.Text tests :: IO [Test] tests = return [ Test $ runParserTest "findAll upperChar" (findAllCap (upperChar :: Parser Char))- ("aBcD" :: Text)+ ("aBcD" :: T.Text) [Left "a", Right ("B", 'B'), Left "c", Right ("D", 'D')] -- check that sepCap can progress even when parser consumes nothing -- and succeeds. , Test $ runParserTest "zero-consumption parser" (sepCap (many (upperChar :: Parser Char)))- ("aBcD" :: Text)+ ("aBcD" :: T.Text) [Left "a", Right "B", Left "c", Right "D"] , Test $ runParserTest "scinum" (sepCap scinum)@@ -43,30 +41,30 @@ ("a") ([Left "a"]) ]--runParserTest name p input expected = TestInstance- { run = do- case runParser p "" input of- Left e -> return (Finished $ Fail $ show e)- Right output ->- if (output == expected)- then return (Finished Pass)- else return (Finished $ Fail- $ show output ++ " ≠ " ++ show expected)- , name = name- , tags = []- , options = []- , setOption = \_ _ -> Left "no options supported"- }+ where+ runParserTest nam p input expected = TestInstance+ { run = do+ case runParser p "" input of+ Left e -> return (Finished $ Fail $ show e)+ Right output ->+ if (output == expected)+ then return (Finished Pass)+ else return (Finished $ Fail+ $ show output ++ " ≠ " ++ show expected)+ , name = nam+ , tags = []+ , options = []+ , setOption = \_ _ -> Left "no options supported"+ } -scinum :: Parser (Double, Integer)-scinum = do- m <- some digitChar- chunk "E"- e <- some digitChar- return (read m, read e)+ scinum :: Parser (Double, Integer)+ scinum = do+ m <- some digitChar+ _ <- chunk "E"+ e <- some digitChar+ return (read m, read e) -offsetA :: Parser Int-offsetA = getOffset <* chunk "A"+ offsetA :: Parser Int+ offsetA = getOffset <* chunk "A"