LoginSignup
0
0

More than 5 years have passed since last update.

Short: Decoding Product types using Generics-Rep

Posted at

Last time, I wrote about using Generics-Rep to be able to decode JSON into sum types using the Simple-JSON library to actually perform all the actual parsing of elements here.

In that example, I didn't add any handling for product types. To be honest, any time you want to encode a product type, you should probably be using records instead. However, if you really want encoding of product types, then one way you might choose to encode them is to use a heterogeneous array.

Review

Last time, we had this type class for doing generic sum decoding:

class ReadForeignGenericSum a where
  readForeignGenericSum :: Foreign -> F a

So to add a case for handling product types we'll add an instance for Product for this class. But to write that instance, we need to be able to somehow be able to correctly read a value from an index of an array. So, probably yet another type class.

ReadForeignGenericProduct

So this time around, there's nothing more we need other than just to be able to use an offset. So we'll define our class like so:

class ReadForeignGenericProduct a where
  readForeignGenericProduct :: Int -> Foreign -> F a

Another good thing is that the Product reps are right-nested, and so a three-arg constructor will look like Constructor name (Product (Argument a) (Product (Argument b) (Argument c))). So the offset of the left has to be the number, and we increment the number to handle all the right cases.

instance rfgpProduct ::
  ( ReadForeignGenericProduct a
  , ReadForeignGenericProduct b
  ) => ReadForeignGenericProduct (Product a b) where
  readForeignGenericProduct i f
        = Product
      <$> readForeignGenericProduct i f
      <*> readForeignGenericProduct (i + 1) f

Then for reading the arguments, we just use the offset provided.

instance rfgpArg ::
  ( ReadForeign a
  ) => ReadForeignGenericProduct (Argument a) where
  readForeignGenericProduct i f = do
    Argument <$> (read =<< readIndex i f)

With this, we're done defining our instances for this class. Then we can write the instance for readForeignGenericSum, where we start with an initial offset of 0.

instance rfgsProduct ::
  ( ReadForeignGenericProduct (Product a b)
  ) => ReadForeignGenericSum (Product a b) where
  readForeignGenericSum f =
    readForeignGenericProduct 0 =<< readProp "value" f

And that's it!

Usage

Now we can take the same test case we had before and just add one more constructor:

 data Fruit
   = Apple
   | Grapes Int
+  | Bananas String String Int
   | Thing { name :: String, count :: Int, color :: String }

And with no other changes, our original test case will continue to work, and we can add a new one:

    let
      testJSON2 = """
      {
        "type": "Bananas",
        "value": ["Green", "Big", 3]
      }
      """

      b :: Either (NonEmptyList ForeignError) Fruit
      b = readJSON testJSON2

    pending $ show b
    -- (Right (Bananas "Green" "Big" 3))

    it "works with product types" do
      isRight b `shouldEqual` true

Conclusion

So I hope these two posts have shown you some basics on how to use datatype generics and what all you can do to solve problems you might have, like this generic JSON serialization problem.

Links

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0