• People like to put whatever in it, valid or not • Needs to be extracted from and validated • We’re stuck with it • But at least it’s kind of easy to work with
String -> a ▪ How is the result always a? • It isn’t ▪ Very error prone • Cannot read property ‘apple’ of undefined ▪ Some approaches use schemas for validation • But you don’t get to use this information to automatically check the rest of your program!
E.g. Java + Jackson ◦ Annotations can get out of sync quickly, but kind of works otherwise • Datatype generics ◦ E.g. via Generics-Rep, GHC Generics, Shapeless, etc. ◦ Uses the actual type information! ◦ Labels from the type information are used for reading records ▪ E.g. { a :: String } reads using property “a” • Metaprogramming and/or codegen ◦ More clumsy to use, but usually at least uses the type information or the AST
No more schema boilerplate e.g. schema, joi • No more manual boilerplate e.g. readString • No more generics boilerplate e.g. derive Generic, instance ReadJSON • Just use the type definition ◦ E.g. { a :: String, b :: Int } ◦ There’s already enough type information here!
in context, by annotation or usage) type MyType = { a :: String, b :: Int } result <- readJSON <$> readTextFile UTF8 "./config.json" case result of Right myType -> myFunction myType -- myFunction :: MyType -> ... • Works with int, string, boolean, arrays, nullables, records, and more • Records enabled by using row type information and RowToList (more on this later) Record ( a :: String, b :: Int ) RowToList, (Cons "a" String (Cons "b" Int Nil))
can return the correct type and modify the type used to parse • E.g. we can rename fields using purescript-record type MyThing = { "fieldA" :: String, "fieldB" :: Int } decodeMyThingFromDirtyJSON :: String -> Either _ MyThing decodeMyThingFromDirtyJSON s = do parsed <- readJSON s pure $ rename (SProxy :: SProxy "MY_FIELD_A") (SProxy :: SProxy "fieldA") parsed -- { MY_FIELD_A :: String, fieldB :: Int }
MyThingy = { a :: String , b :: Array String } parseMyThingyJsonFromDirtyJSON :: String -> Either _ MyThingy parseMyThingyJsonFromDirtyJSON str = do json <- readJSON str -- json read in as { a :: String, b :: Nullable (Array String) } let b = fromMaybe [] <<< Nullable.toMaybe $ json.b -- this value is Array String, and we can replace the existing field like so: pure $ json { b = b }
can be useful to learn • Implemented using a type class defined for concrete types class ReadForeign a where readImpl :: Foreign -> F a instance readString :: ReadForeign String where readImpl = readString instance readArray :: ReadForeign a => ReadForeign (Array a) where readImpl = traverse readImpl <=< readArray
} ~ Record ( a :: String, b :: Int ) RowToList ( a :: String, b :: Int ) (Cons "a" String (Cons "b" Int Nil)) instance readRecord :: ( RowToList fields fieldList , ReadForeignFields fieldList () fields ) => ReadForeign (Record fields) where readImpl o = do steps <- getFields (RLProxy :: RLProxy fieldList) o pure $ Builder.build steps {} Uses purescript-record Builder to build the whole record from an empty record
(to :: # Type) | xs -> from to where getFields :: RLProxy xs -> Foreign -> F (Builder (Record from) (Record to)) Uses the RowList to match instances of the type class and has the input and output row types as params The returned Builder in F (i.e. Except MultipleErrors) ultimately provides empty row to the full record
IsSymbol name -- need to do Symbol operations, like getting the value level string , ReadForeign ty -- reading the value from Foreign , ReadForeignFields tail from from' -- the rest of the fields, with the intermediate result , RowLacks name from' -- the current label does not exist in the intermediate result , RowCons name ty from' to -- adding the label with the type to the intermediate result -- gives us the final result we want ) => ReadForeignFields (Cons name ty tail) from to where getFields _ obj = do ...
do value :: ty <- withExcept' $ readImpl =<< readProp name obj -- read the field rest <- getFields tailP obj -- get the Builder for the rest of the fields let first :: Builder (Record from') (Record to) first = Builder.insert nameP value -- make the builder for the current field pure $ first <<< rest -- compose the builders to get the result builder where nameP = SProxy :: SProxy name tailP = RLProxy :: RLProxy tail name = reflectSymbol nameP withExcept' = withExcept <<< map $ ErrorAtProperty name
gives you tools to do it • Simple-JSON is fun • You shouldn’t have to manually do anything unless you have very specific needs ◦ We’ve already shown renaming is no big deal ◦ Neither is changing the type ◦ Removing nesting would be application of the two in various ways • Row types are fun • RowToList is fun • Type level programming is fun