(Warning: This section veers into types that are likely of more use to library authors than end users.)

Suppose we don't know much about our data, but we do know that it starts with an identifying column, and then some number of numeric columns. We can structurally peel off the first column, perform a constrained polymorphic operation on the other columns, then glue the first column back on to the result.

addTwoRest :: RecMapMethod Num ElField rs => Record (s :-> a ' : rs) -> Record (s :-> a ' : rs) addTwoRest (h :& t) = h :& aux t where aux = mapFields @ Num ( \ x -> x + 2)

λ> addTwoRest (rcast @'[Occupation, UserId, Age] (frameRow ms 0)) {occupation :-> "technician", user id :-> 3, age :-> 26}

But what if we don't want to rely entirely on ordering of our rows? Here, we know there is an identifying column, Occupation , and we want to shuffle it around to the head of the record while mapping a constrained polymorphic operation over the other columns.

addTwoOccupation :: ( CanDelete Occupation rs, rs' ~ RDelete Occupation rs, RecMapMethod Num ElField rs') => Record rs -> Record ( Occupation ' : RDelete Occupation rs) addTwoOccupation r = rget @ Occupation r :& mapFields @ Num ( + 2) (rdel @ Occupation r)

λ> addTwoOccupation (rcast @'[UserId,Age,Occupation] (frameRow ms 0)) {occupation :-> "technician", user id :-> 3, age :-> 26}

It is a bit clumsy to delete and then add back a particular field, and the dependence on explicit structure is relying a bit more on coincidence than we might like. We could choose, instead, to work with row types that contain a distinguished column somewhere in their midst, but regarding precisely where it is, or how many other fields there are, we care not.

addTwoOccupation' :: forall rs rs' . ( CanDelete Occupation rs, rs' ~ RDelete Occupation rs, RecMapMethod Num ElField rs') => Record rs -> Record rs addTwoOccupation' = rsubset %~ mapFields @ Num @ rs' ( + 2)

λ> addTwoOccupation' (rcast @'[UserId,Age,Occupation] (frameRow ms 0)) {user id :-> 3, age :-> 26, occupation :-> "technician"}

We can unpack this type a bit to understand what is happening. A Frames Record is a record from the Vinyl library, except that each type has phantom column information. This metadata is available to the type checker, but is erased during compilation so that it does not impose any runtime overhead. What we are doing here is saying that we will operate on a Frames row type, Record rs , that has an element Occupation , and that deleting this element works properly (i.e. the leftover fields are a proper subset of the original row type). We further state – with the AsVinyl constraint – that we want to work on the unadorned field values, temporarily discarding their header information, with the mapMethod function that will treat our richly-typed row as a less informative Vinyl record.

We then peer through a lens onto the set of all unadorned fields other than Occupation , apply a function with a Num constraint to each of those fields, then pull back out of the lens reattaching the column header information on our way. All of that manipulation and bookkeeping is managed by the type checker.

Lest we forget we are working in a typed setting, what happens if the constraint on our polymorphic operation can't be satisfied by one of the columns?

λ> addTwoOccupation (rcast @'[Age,Occupation,Gender] (frameRow ms 0)) <interactive>:15:1-16: No instance for (Num Text) arising from a use of ‘addTwoOccupation’ In the expression: addTwoOccupation (rcast @'[Age, Occupation, Gender] (frameRow ms 0)) In an equation for ‘it’: it = addTwoOccupation (rcast @'[Age, Occupation, Gender] (frameRow ms 0))

This error message isn't ideal in that it doesn't tell us which column failed to satisfy the constraint. Hopefully this can be improved in the future!