みなさん、ご無沙汰しております〜 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 Symbol
s, 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 Symbol
s 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 Symbol
s with Symbol
s
First, we'll define a function so we can format Symbol
s. 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 Symbol
s 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
- Repo: https://github.com/justinwoo/purescript-shoronpo
- Previous post about using record-format to have typed path params: https://qiita.com/kimagure/items/3273d20c4c5ad74dbe26
- Previous post about parsing
Symbol
s for type annotations: https://qiita.com/kimagure/items/6729a5d55ab99bcee8ec