3
votes

I am new to Purescript and I am trying to write a function that can take any record value and iterate over the fields and values and build a querystring.

I am thinking something like:

buildQueryString :: forall a. PropertyTraversible r => r -> String

which I want to use like this:

buildQueryString {name: "joe", age: 10}      -- returns: "name=joe&age=10"

Is there a way to write something like that in Purescript with existing idioms or do I have to create my own custom Type Class for this?

2
Not sure how to do this for any record. Sounds like serious metaprogramming. If you want to do this for newtypes around records, you can use purescript-generics, derive Generic, call toSpine and transform the GenericSpine as you wish. Maybe that is an idea worth exploring. Another possibility would be to just use the FFI.stholzm
@stholzm: after posting the question, I did play around with the FFI and I managed to implement this with the FFI. I will look into purescript-generics to see if that will also work. I would prefer a solution that does not use the FFI though.Rouan van Dalen
Ah, I thought you wanted buildQueryString to work on any record, without a nominal type. It is not possible to derive instances for records AFAIK, so that would be tedious. Also, I am not sure whether purescript-generics or purescript-generics-rep is the preferred library nowadays. I just know it is possible with purescript-generics.stholzm
@stholzm: I do want it to work on any record, but as you say, if I want to use something like purescript-generics then there are some restrictions to the records that can be supported. Once I evaluate all the possibilities I will know what the restrictions are and if I can live with them :)Rouan van Dalen
I had a look at purescript-generics-rep today and it is basically the same idea as my answer. But a custom type class would not work on any record as well. So I think the FFI is a valid approach here. I would love to see another solution though!stholzm

2 Answers

2
votes

I'm sure that it can be shorter, but here is my implementation based on purescript-generic-rep (inspired by genericShow). This solution uses typeclasses - it seems to be standard approach with generic-rep:

module Main where

import Prelude

import Control.Monad.Eff (Eff)
import Control.Monad.Eff.Console (CONSOLE, log)
import Data.Foldable (intercalate)
import Data.Generic.Rep (class Generic, Constructor(..), Field(..), Product(..), Rec(..), from)
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)

class EncodeValue a where
  encodeValue ∷ a → String

instance encodeValueString ∷ EncodeValue String where
  encodeValue = id

instance encodeValueInt ∷ EncodeValue Int where
  encodeValue = show

class EncodeFields a where
  encodeFields :: a -> Array String

instance encodeFieldsProduct
  ∷ (EncodeFields a, EncodeFields b)
  ⇒ EncodeFields (Product a b) where

  encodeFields (Product a b) = encodeFields a <> encodeFields b

instance encodeFieldsField
  ∷ (EncodeValue a, IsSymbol name)
  ⇒ EncodeFields (Field name a) where

  encodeFields (Field a) =
    [reflectSymbol (SProxy :: SProxy name) <> "=" <> encodeValue a]

buildQueryString
  ∷ ∀ a l n.
    Generic n (Constructor l (Rec a))
  ⇒ (EncodeFields a)
  ⇒ n
  → String
buildQueryString n =
  build <<< from $ n
 where
  build (Constructor (Rec fields)) = intercalate "&" <<< encodeFields $ fields

newtype Person =
  Person
    { name   ∷ String
    , age    ∷ Int
    }
derive instance genericPerson ∷ Generic Person _

joe ∷ Person
joe = Person { name: "joe", age: 10 }

main :: forall e. Eff (console :: CONSOLE | e) Unit
main = do
  log <<< buildQueryString $ joe

buildQueryString expects value of type with single constructor which contains a record (possibly just newtype) because it is impossible to derive a Generic instance for "unwrapped" Record type.

If you want to handle also Array values etc. then encodeValue should probably return values of type Array String.

0
votes

This is possible with purescript-generics but it only works on nominal types, not on any record. But it saves you boilerplate, since you can just derive the instance for Generic, so it would work with any data or newtype without further modification.

Downside is, you have to make some assumptions about the type: like it only contains one record and the record does not contain arrays or other records.

Here is a hacky demonstration how it would work:

data Person = Person 
            { name   :: String
            , age    :: Int
            }

derive instance genericPerson :: Generic Person

joe = Person { name: "joe", age: 10 }

build :: GenericSpine -> String
build (SRecord arr) = intercalate "&" (map (\x -> x.recLabel <> "=" <> build (x.recValue unit)) arr)
build (SProd _ arr) = fromMaybe "TODO" $ map (\f -> build (f unit)) (head arr)
build (SString s)   = s
build (SInt    i)   = show i
build _             = "TODO"

test = build (toSpine joe)

purescript-generics-rep is newer, so possibly there is a better solution, maybe even on any record. I have not tried that (yet).