• type Config = { url :: String, id :: Int } • All of the static information we need is right here: ◦ “url” field of String ◦ “id” field of Int • { “url”: “google.com”, “id”: 1 } ◦ Right Config • { } ◦ Left Error • What if you could get JSON parsing for free?
types • type Config = { filePath :: FilePath, id :: Id } • type Config = Record ( filePath :: FilePath, id :: Id ) • data Record :: # Type -> Type ◦ # indicates row type where field is of kind provided ◦ E.g. data Eff :: # Effect -> Type -> Type • Record instance takes this row (unordered set) and converts it into a type-level RowList (ordered list) via RowToList RowToList (filePath :: FilePath, id :: Id) (Cons "filePath" FilePath (Cons "id" Id Nil))
values provided is wrong? ◦ The name of a field is unpleasant/wrong? • These are solved problems • We can use the Record library for generic operations on records
most languages • Remember { | row } ~ Record row • PureScript-Record ◦ E.g. what is “get” for records? ▪ For all types row row’ l a ▪ If l is a Symbol (type-level string) ▪ And there exists a structural subtype row’ where a field has been added with label l and type a to form row ▪ Then there is a function for a proxy carrying l to a record of type { | row } to a get :: forall r r' l a . IsSymbol l => RowCons l a r' r => SProxy l -> { | r } -> a
you want an Array, where nulls are turned into empty arrays • No problem, we can simply modify the field modify :: forall r1 r2 r l a b . IsSymbol l => RowCons l a r r1 => RowCons l b r r2 => SProxy l -> (a -> b) -> { | r1 } -> { | r2 }
b :: Array String } modify :: forall r1 (a :: String, b :: Array String) r "b" (Nullable (Array String)) (Array String) . RowCons "b" (Nullable (Array String)) r r1 => RowCons "b" (Array String) r (a :: String, b :: Array String) class RowCons (l :: Symbol) (a :: Type) (i :: # Type) (o :: # Type) | l a i -> o , l o -> a i We have l ("b") and o ((a :: String, b :: Array String)), so a and i can be solved for. We already know a, so it acts as an extra constraint, and then i is produced from the substitution. We know from the second constraint that r will then be (a :: String) without the field (b :: Array String) Then with r we can use the first constraint to insert (b :: Nullable (Array String)) So r1 is (a :: String, b :: Nullable (Array String)) And the input is parsed as { a :: String, b :: Nullable (Array String) } by ReadForeign instances
ForeignError) MyThingy parseMyThingyJsonFromImperfectJsonButConvertTheDirtyProperty str = do json <- readJSON str let b = fromMaybe [] <<< Nullable.toMaybe $ json.b pure $ json { b = b } Using normal record update syntax
next ty input inter output . IsSymbol prev -- previous name Symbol => IsSymbol next -- next name Symbol => RowCons prev ty inter input -- (prev, ty) + inter = input => RowLacks prev inter -- inter does not have a field with label prev => RowCons next ty inter output -- (next, ty) + inter = output => RowLacks next inter -- inter does not have a field with label next => SProxy prev -> SProxy next -> Record input -> Record output
row type information is cool ◦ Records are not Hashmaps and vice versa ◦ Always better to have stronger guarantees and not have invalid branches ▪ “Make illegal states impossible” • Nobody should have to decode JSON by hand ◦ You should only need to do small tweaks using generic operations • None of these techniques are specific to JSON ◦ E.g. https://github.com/justinwoo/purescript-tortellini ◦ Weird argument made by people who should know better • People sometimes get really offended by this conclusion slide ◦ “My favorite language can’t do this so this offends me” ◦ You are not your tool
has encoding them not been a giant mess • Build your own solution with datatype generics using Generics-Rep ◦ See my Simple-JSON-Generic-Sums repo ▪ Sums encoded as { type, value } ▪ Products encoded as heterogeneous array ▪ Pre 0.12: converting reps record fields into fields and vice versa :barf: ▪ Alas, this is not a talk about datatype generics • Obvious solution: use Variants for sums, Records for products