packages feed

bidirectional (empty) → 0.1.0.0

raw patch · 7 files changed

+312/−0 lines, 7 filesdep +basedep +bidirectionaldep +hedgehogsetup-changed

Dependencies added: base, bidirectional, hedgehog, mtl, profunctors

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for bidirectional++## 0.1.0.0 -- 2020-10-07++* First version. Released on an unsuspecting world.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2020, Mats Rauhala++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Mats Rauhala nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,63 @@+# Bidirectional - bidirectional parsing++This library is based on the [blog](https://blog.poisson.chat/posts/2017-01-01-monadic-profunctors.html) by Lysxia.++In it they define a bidirectional parser that can both generate and consume+values.++Imagine that you have a record `Person` and you want to serialize it into a+list and deserialize it back.++``` haskell+data Person = Person { name :: String, age :: Int }+```+++The parser is parameterized over the parsing and generation contexts, these+needs to be selected carefully when implementing the parsers. It could be for+example a `RowParser` `Writer [SQLData]` combination for sqlite-simple, or a+simple state and writer for our running example.++So, let's figure out the context for our example. We want to encode into a list+and then decode the list back into a value.++``` haskell+type SimpleParser a = IParser (StateT [String] Maybe) (Writer [String]) a a+```++Then we create the parsers using the `parser` function.++``` haskell+int :: SimpleParser Int+int =+  parser+    (StateT $ \(x:xs) -> (,xs) <$> readMaybe x)+    (\x -> x <$ tell [show x])++string :: SimpleParser String+string =+  parser+    (StateT $ \(x:xs) -> Just (x,xs))+    (\x -> x <$ tell [x])+```++One small detail for creating records, is that the encoder gets the full record+structure by default, so you need to focus in on a specific part of the record+using the `(.=)` function.++``` haskell+person :: SimpleParser Person+person =+  Person <$> name .= string+         <*> age .= int+```++And now you can use your encoders and decoders++```+> execWriter (encode person (Person "foo" 30))+["foo", "30"]++> evalStateT (decode person ["foo", "30"])+Just (Person { name = "foo", age = 30 })+```
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ bidirectional.cabal view
@@ -0,0 +1,42 @@+cabal-version:       2.0+-- Initial package description 'bidirectional.cabal' generated by 'cabal+-- init'.  For further documentation, see+-- http://haskell.org/cabal/users-guide/++name:                bidirectional+version:             0.1.0.0+synopsis:            Simple bidirectional serialization and deserialization+description:         Bidirectional serialization based on https://blog.poisson.chat/posts/2017-01-01-monadic-profunctors.html+bug-reports:         https://github.com/MasseR/bidirectional/issues+license:             BSD3+license-file:        LICENSE+author:              Mats Rauhala+maintainer:          mats.rauhala@iki.fi+-- copyright:+category:            Codec+build-type:          Simple+extra-source-files:  CHANGELOG.md+                     README.md++source-repository head+  type: git+  location: https://github.com/MasseR/bidirectional++library+  exposed-modules:     Data.IParser+  -- other-modules:+  -- other-extensions:+  build-depends:       base >=4.10.0.0 && < 4.14+                     , profunctors >= 5 && < 5.6+  hs-source-dirs:      src+  default-language:    Haskell2010++test-suite bidirectional-test+  default-language:    Haskell2010+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Main.hs+  build-depends:       base >=4.10.0.0 && < 4.14+                     , hedgehog+                     , bidirectional+                     , mtl
+ src/Data/IParser.hs view
@@ -0,0 +1,94 @@+{-# LANGUAGE Safe #-}+{-|+Module      : Data.IParser+Description : Bidirectional parsing+Copyright   : (c) Mats Rauhala, 2020+License     : BSD-3+Maintainer  : mats.rauhala@iki.fi+Stability   : experimental+Portability : POSIX++Let's assume we have a parser like the following++@+int :: Parser (ReaderT String Maybe) (Writer [Int]) Int Int+int = parser (ReaderT readMaybe) (\x -> x <$ tell [show x])+@++Then you can use the parser for parsing:++@+> runReaderT (decode int) "3"+Just 3+@++Or for encoding:++@+> execWriter (encode int 3)+["3"]+@++Or combine both of them++@+> runReaderT (decode int) $ head $ execWriter $ encode int 3+Just 3+@+-}+module Data.IParser where++import Data.Profunctor++-- | The core bidirectional parser type+--+-- See the module for usage example+--+data IParser r w c a =+  IParser { decoder :: r a -- ^ Return a value "a" in context "r".+          , encoder :: Star w c a -- ^ Generate a value "a" from the initial object "c" in the context "w"+          }++-- | Smart constructor for the parser+parser+  :: r a -- ^ The parser+  -> (c -> w a) -- ^ The encoder+  -> IParser r w c a+parser dec enc = IParser { decoder = dec, encoder = Star enc }++-- | Extract the decoder from the parser+decode :: IParser r w c a -> r a+decode = decoder++-- | Extract the encoder from the parser+encode :: IParser r w c a -> c -> w a+encode = runStar . encoder++-- | Record accessor helper+--+-- Due to the nature of the parser, the encoder gets the full record type, when+-- it should only focus on a specific part.+--+-- @+-- data Person = Person { name :: String, age :: Int }+--+-- person :: IParser r w Person Person+-- person =+--  Person <$> name .= string+--         <*> age .= int+-- @+(.=) :: (Functor r, Functor w) => (c -> c') -> IParser r w c' a -> IParser r w c a+(.=) = lmap++instance (Functor w, Functor r) => Functor (IParser r w c) where+  fmap f i = IParser { decoder = f <$> decoder i+                     , encoder = f <$> encoder i+                     }++instance (Applicative w, Applicative r) => Applicative (IParser r w c) where+  pure x = IParser { decoder = pure x, encoder = pure x }+  fx <*> x = IParser { decoder = decoder fx <*> decoder x, encoder = encoder fx <*> encoder x }++instance (Functor r, Functor w) => Profunctor (IParser r w) where+  dimap f g i = IParser { decoder = g <$> decoder i, encoder = dimap f g (encoder i) }+
+ test/Main.hs view
@@ -0,0 +1,76 @@+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TupleSections #-}+module Main (main) where++import System.Exit+       (exitFailure, exitSuccess)++import Data.Functor.Identity++import Hedgehog++import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++import Control.Monad.Reader+       (ReaderT(..), runReaderT)+import Control.Monad.State+       (StateT(..), evalStateT)+import Control.Monad.Writer+       (Writer, execWriter, tell)+import Text.Read+       (readMaybe)++import Data.IParser++-- fmap id == id+prop_functor_identity :: Property+prop_functor_identity = property $ do+  x <- forAll $ Gen.integral (Range.linear 0 100)+  let p = parser (pure x) pure :: IParser Identity Identity Int Int+  decode (fmap id p) === id (decode p)+  encode (fmap id p) x === id (encode p x)++-- fmap (f . g) == fmap f . fmap g+prop_functor_composition :: Property+prop_functor_composition = property $ do+  x <- forAll $ Gen.integral (Range.linear 0 100)+  y <- forAll $ Gen.integral (Range.linear 0 100)+  z <- forAll $ Gen.integral (Range.linear 0 100)+  let p = parser (pure val) pure :: IParser Identity Identity (Int, (Int, Int)) (Int, (Int, Int))+      f = fst+      g = snd+      val = (x,(y,z))+  decode (fmap (f . g) p) === decode (fmap f . fmap g $ p)+  encode (fmap (f . g) p) val === encode (fmap f . fmap g $ p) val++-- pure id <*> v+prop_applicative_identity :: Property+prop_applicative_identity = property $ do+  x <- forAll $ Gen.integral (Range.linear 0 100)+  let p = pure x :: IParser Identity Identity Int Int+  decode (pure id <*> p) === pure x++prop_bidirectional :: Property+prop_bidirectional = property $ do+  x <- forAll $ Gen.integral (Range.linear 0 100)+  let int = parser (ReaderT readMaybe) (\x -> x <$ tell [show x])+  runReaderT (decode int) (head (execWriter (encode int x))) === Just x++data Person+  = Person { name :: String, age :: Int }+  deriving (Show, Eq)++prop_compose :: Property+prop_compose = property $ do+  person <- forAll (Person <$> Gen.string (Range.linear 0 10) Gen.unicode <*> Gen.integral (Range.linear 10 30))+  let int = parser (StateT $ \(x:xs) -> (,xs) <$> readMaybe x) (\x -> x <$ tell [show x])+      string = parser (StateT $ \(x:xs) -> Just (x,xs)) (\x -> x <$ tell [x])+      p = Person <$> name .= string <*> age .= int+      encoded = execWriter (encode p person)+  evalStateT (decode p) encoded === Just person++main :: IO ()+main = do+  result <- checkParallel $$(discover)+  if result then exitSuccess else exitFailure