LoginSignup
4
1

More than 5 years have passed since last update.

Formatting type-level Strings with row type labels

Last updated at Posted at 2018-07-22

みなさん、ご無沙汰しております〜 It's been a while since I wrote anything here, so this time I'll write about some more Symbol formatting using the labels of a row type.

What

Since PureScript 0.12, we're now able to do a lot of things with Symbols, where we might use them to parse out parameters to use with SQL queries like I've demonstrated in this post. While this post simply writes out the entire query, we can see some potential here where we might even construct Symbols by taking the labels out of a record type that we want to parse the results to when forming a select query. And so, in this post we'll look at how we write some code to do so.

Formatting Symbols with Symbols

First, we'll define a function so we can format Symbols. We use the record-format library that we talked about previously here.

formatSymbol
  :: forall string flist row out proxyOrRec
   . RF.Parse string flist
  => FormatSymbolParsed flist row "" out
  => SProxy string
  -> proxyOrRec row
  -> SProxy out
formatSymbol _ _ = SProxy

Like last time, using the Parse class from record-format gives us a type-level list of tokens, which might be a parameter variable Var or a literal string Lit. With this information, we can define our class FormatSymbolParsed which will accumulate a formatted symbol:

class FormatSymbolParsed
  (flist :: RF.FList)
  (row :: # Type)
  (acc :: Symbol)
  (out :: Symbol)
  | flist -> row acc out

For this class, we have our functional dependencies set to say that the parameters can all be determined by the flist, as we only need to match instances based on it. For the empty case, we simply say that the accumulate and output are the same:

instance nilFormatSymbolParsed :: FormatSymbolParsed RF.FNil row out out

For the variable case, we can take the correct Symbol that is carried by an SProxy in our row type for the variable name using Row.Cons:

instance consVarFormatSymbolParsed ::
  ( Symbol.Append acc sym acc'
  , Row.Cons var (SProxy sym) row' row
  , FormatSymbolParsed tail row acc' out
  ) => FormatSymbolParsed (RF.FCons (RF.Var var) tail) row acc out

So we take the Symbol parameter carried in the row parameter and use that to append to the accumulate. In the case of the literal, we can just append the symbol as-is:

instance consLitFormatSymbolParsed ::
  ( Symbol.Append acc lit acc'
  , FormatSymbolParsed tail row acc' out
  ) => FormatSymbolParsed (RF.FCons (RF.Lit lit) tail) row acc out

Intercalating row labels

To prepare the Symbols that we will format with, we need to intercalate labels of a row such that given (a :: _, b :: _, c :: _), we should be able to intercalate this with , to get a, b, c. First, let's prepare the top-level function:

intercalateRowLabels
  :: forall row x out proxyOrRecord
   . IntercalateRowLabels row x out
  => proxyOrRecord row
  -> SProxy x
  -> SProxy out
intercalateRowLabels _ _ = SProxy

intercalateRecordLabels
  :: forall row x out
   . IntercalateRowLabels row x out
  => Proxy { | row }
  -> SProxy x
  -> SProxy out
intercalateRecordLabels _ _ = SProxy

Here I've defined two functions that do the same thing, but where one can take a RProxy, Record, or some other type with kind # Type -> Type and one that takes a Proxy of the record. Then we can define the IntercalateRowLabels class used here:

class IntercalateRowLabels (row :: # Type) (x :: Symbol) (out :: Symbol)

instance intercalateRowLabelsInstance ::
  ( RL.RowToList row rl
  , IntercalateRowLabelsImpl rl x "" out
  ) => IntercalateRowLabels row x out

So this is a type class with a single instance which will kick off into the implementation IntercalateRowLabelsImpl with an empty string for the accumulate.

class IntercalateRowLabelsImpl
  (rl :: RL.RowList)
  (x :: Symbol)
  (acc :: Symbol)
  (out :: Symbol)
  | rl -> x out

Like before, our accumulator function will match instances on the RowList parameter, and the empty case will match the accumulator to the output:

instance nilIntercalateRowLabelsImpl :: IntercalateRowLabelsImpl RL.Nil x out out

Then there are two cases to handle for intercalation: the last and Nth elements. For the last element, we can just add the last label directly:

instance consNilIntercalateRowLabelsImpl ::
  ( Symbol.Append acc name acc'
  ) => IntercalateRowLabelsImpl (RL.Cons name ty RL.Nil) x acc acc'

Then for the Nth element, we can write a chained instance that first appends the label and delimiter to use in appending to the accumulate:

else instance consIntercalateRowLabelsImpl ::
  ( Symbol.Append name x s
  , Symbol.Append acc s acc'
  , IntercalateRowLabelsImpl tail x acc' out
  ) => IntercalateRowLabelsImpl (RL.Cons name ty tail) x acc out

And that's all we need here.

Usage

We can see our intercalateRecordLabels function at work:

type MyRecord =
  { a :: SProxy "A"
  , b :: SProxy "B"
  , c :: SProxy "C"
  }

-- inferred type:
labels :: SProxy "a, b, c"
labels =
  S.intercalateRecordLabels
    (Proxy :: Proxy MyRecord)
    (SProxy :: SProxy ", ")

The type signature here for labels is inferred, so we can change other types and have our IDE plugin generate the type signature again.

We can also see an inferred example in action:

main = do
  let
    myLabels =
      S.intercalateRecordLabels
        (Proxy :: Proxy { apple :: Int, banana :: String })
        (SProxy :: SProxy ", ")
    myFormatted =
      S.formatSymbol
        (SProxy :: SProxy "myLabels: {myLabels}")
        { myLabels }

  assertEqual
    { actual: reflectSymbol myFormatted
    , expected: "myLabels: apple, banana"
    }

Works like a charm!

Conclusion

This time, we mostly just reused methods that we've seen in my earlier posts about `Symbol.Cons** and instance chains in PureScript 0.12. Hopefully this has shown you that you can also come up with many of your solutions to these problems, once you know how to work with the constraints and functional dependencies.

Links

4
1
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
4
1