The Prairie
library for working with record types has been occupying a lot of my brain space recently. I work on a Web interface for a database and I frequently have to create new HTML forms and SQL queries to work with structured data. We write the SQL directly and parse Haskell records from each row, and write forms so that users can change the data in a particular row. We can model this with a higher-kinded record type.
data OrderF b = OrderF
{ orderId :: b OrderId
, quantity :: b Int
, rate :: b Double
, unitId :: b UnitId
}
type OrderSelect = OrderF (Const Sql)
type Order = OrderF Identity
-- type OrderFromUser = OrderF FormResult
-- type OrderDisplay = OrderF (Const (WidgetFor app ()))
This form doesn’t let us derive Prairie
's Record
instance. We’d have to “lower” the type to do so:
data Order = Order
{ orderId :: OrderId
, quantity :: Int
, rate :: Double
, unitId :: Shaped Need UnitId -- ~ Identity UnitId, but for a <select required>
}
mkRecord ''Order
-- generates:
instance Record Order where
data Field Order ty where
OrderOrderId :: Field Order OrderId
OrderQuantity :: Field Order Int
OrderRate :: Field Order Double
OrderUnitId :: Field Order (Shaped Need UnitId)
-- These let us define an `Order` via pattern match on `Field Order ty`
tabulateRecordA :: (Applicative f) => (forall ty. Field Order ty -> f ty) -> f Order
tabulateRecord :: (forall ty. Field Order ty -> ty) -> Order
The function tabulateRecordA
is a powerhouse. Look closely at the type of its argument: f
is a type constructor that applies linearly across all of the fields of a record. For records with a finite number of fields, this is isomorphic to our higher-kinded data type. Examples here and here.
What use do they have in industry? I am also daydreaming of an alternate Yesod.Form
implementation that, among other things, moves the “required” and “multiple” attributes to the type level, and using Prairie
would allow extremely concise definitions for what it means to “run” a form for a particular record. Writing a form is as easy as writing a value:
orderForm ::
(RenderMessage app FormMessage) =>
(Prairie Order Identity -> Prairie Order (FormInput app))
orderForm (Prairie order) = Prairie \field -> case field of
OrderOrderId -> FormInput numberPortal [("id", "orderid")] (order field).runIdentity
OrderQuantity -> FormInput numberPortal [("min", "0")] (order field).runIdentity
OrderRate -> FormInput numberPortal [("step", "0.01")] (order field).runIdentity
OrderUnitId -> FormInput (selectPortal _) [] (order field).runIdentity
-- for use with
prairieForm ::
(Record rec, RenderMessage app FormMessage) =>
(Prairie rec (FormInput app) -> FormFor app (Prairie rec (FormOutput app)))
Tailor-made interfaces to delicate sections of business logic enable greater separation of concerns. Higher-kinded data gives you the power to use types as templates, and Prairie
only needs to generate one GADT to do it. Prairie
also gives you literal superpowers for combining records with the same Field
s:
components ::
(Applicative f, Applicative g, Record rec) =>
(forall x. f x -> g x) ->
(Prairie rec f -> Prairie rec g)
components nat (Prairie f) = Prairie (nat . f)
zipWith ::
(Applicative f, Applicative g, Record rec) =>
(forall x. f x -> g x -> h x) ->
(Prairie rec f -> Prairie rec g -> Prairie rec h)
zipWith nat (Prairie f) (Prairie g) = Prairie (liftA2 nat f g)
In the language of barbies
, components
is a way to change clothes, and zipWith
is a way to wear two sets of clothes at once.