runSelect connection is a transformation Select x → IO [y] that can do anything, depending on DefaultFromFields x y. There is no guidance. For example, suppose we have this table:
example ∷ Table _ (Field SqlInt4, Field SqlText)
example = table _ _
Nothing stops me from writing:
howManyPaws ∷ (Int32, Text) → String
howManyPaws (int, text) = "A " <> Text.unpack text <> " has " <> show int <> " paws."
instance Default FromFields (Field SqlInt4, Field SqlText) String where
def = fmap howManyPaws def
selectExample ∷ Select (Field SqlInt4, Field SqlText)
selectExample = selectTable example
exampleRows ∷ IO [String]
exampleRows = runSelect _ selectExample
Someone else will write other instances like that. The compiler could not infer the type of exampleRows. The programmer will also have a hard time figuring out what type is the most suitable. In a complicated code base this effort will be taxing.
The source of the problem is that runSelect does two things:
Change the functor from Select to IO ∘ [ ].
Change the underlying type from (Int32, Text) to String.
solution
We can have:
type family DefaultRow fields where
DefaultRow (α, β) = (DefaultRow α, DefaultRow β)
DefaultRow (Field SqlInt4) = Int32
DefaultRow (Field SqlText) = Text
exampleRowsDefault ∷ IO [DefaultRow (Field SqlInt4, Field SqlText)]
exampleRowsDefault = runSelect _ selectExample
exampleRowsRevisited ∷ IO [String]
exampleRowsRevisited = (fmap . fmap) howManyPaws exampleRowsDefault
All the types are inferred and the programmer can see how the row is processed at the use site. If there is any doubt as to what the appropriate target type for runSelect is, DefaultRow x is always appropriate for a row selected by Select x, and the programmer may choose to convert it into anything at will.
There is a category ΛSQL of table headers α, β, … and lambda abstractions of form λ table, select … from table ….
We can embed this category into Haskell via the functor Opaleye: ΛSQL → Haskell that sends:
A table with header α to the Haskell type Select (Opaleye α) with Opaleye α a type arbitrarily derived from the table header α. Opaleye α does not even have to be inhabited.
A lambda abstraction λ table, select β from table … to an appropriate Opaleye expression of type Select (Opaleye α) → Select (Opaleye β).
We can embed this category into Haskell via the functor Haskell: ΛSQL → Haskell that sends:
A table with header α to the Haskell type IO [Haskell α] with Haskell α a type derived from the table header α in such a way that Haskell α is inhabited by exactly as many total values as there may be distinct rows in a table with header α.
A lambda abstraction λ table, select β from table … to an appropriate expression of type IO [Haskell α] → IO [Haskell β] built from filter, sort and other familiar Haskell functions.
Thus we have two parallel functors Opaleye, Haskell: ΛSQL → Haskell.
Now runSelect connection is a bunch of arrows from which we can variously pick transformations between Opaleye and Haskell. Among these transformations some are natural. Among these natural transformations, some are such that every component runSelect @(Opaleye α) @(Haskell α) connection is initial in the slice category (Select α ↓ Haskell) — that is to say, any arrow of type Opaleye α → x splits one way as f ∘ runSelect @(Opaleye α) @(Haskell α) connection. Since all initial objects of a category are essentially the same, all thus defined natural transformations are essentially the same as well — they are the initial object in the category (Opaleye ↓ ΛSQL → Haskell) of natural transformations from Opaleye.
In the example above, the arrow runSelect connection ∷ Select (Field SqlInt4, Field SqlText) → IO [String] splits as howManyPaws ∘ runSelect @(Field SqlInt4, Field SqlText) @(Int32, Text) connection.
alternatives
I am aware of runSelectI and Inferrable FromFields. There are several problems with this solution:
The inference does not work well with product profunctors and custom tuples. See:
— Here GHC will give you an error «Ambiguous type variable ‘haskells0’ arising from a use of ‘runSelectI’». It is not clear how to solve this problem with runSelectI. With the type family DefaultRow the solution is straightforward:
type instance DefaultRow (CustomTuple α β) = CustomTuple (DefaultRow α) (DefaultRow β)
example = runSelectDefault _ selectExampleWithCustomTuple
Even if the result X of runSelectI is determined by the constraint β ~ X ⇒ Default (Inferrable FromFields) A β, it is not clear how to mention it in client code. Once you call runSelectI, you will want to do something with the result, but you cannot even refer to its type!
The choices made for the default instances are not always universal. For example:
instance int ~ Int => D.Default (Inferrable FromField) T.SqlInt4 int where
SqlInt4refers to 32 bit wide numbers in PostgreSQL, but Int is 64 bit wide on my computer. Thus, there is no initial natural transformation with a component going from Select (Column SqlInt4) to IO [Int]. SqlInt4 should refer to Int32, as SqlInt8 already correctly refers to Int64.
A default
DefaultFromFields
via a type family?problem
runSelect connection
is a transformationSelect x → IO [y]
that can do anything, depending onDefaultFromFields x y
. There is no guidance. For example, suppose we have this table:Nothing stops me from writing:
Someone else will write other instances like that. The compiler could not infer the type of
exampleRows
. The programmer will also have a hard time figuring out what type is the most suitable. In a complicated code base this effort will be taxing.The source of the problem is that
runSelect
does two things:Select
toIO ∘ [ ]
.(Int32, Text)
toString
.solution
We can have:
All the types are inferred and the programmer can see how the row is processed at the use site. If there is any doubt as to what the appropriate target type for
runSelect
is,DefaultRow x
is always appropriate for a row selected bySelect x
, and the programmer may choose to convert it into anything at will.Generally, we can have:
— It will always do the right thing.
categorially
There is a category ΛSQL of table headers α, β, … and lambda abstractions of form λ table, select … from table ….
Opaleye
: ΛSQL → Haskell that sends:Select (Opaleye α)
withOpaleye α
a type arbitrarily derived from the table header α.Opaleye α
does not even have to be inhabited.Select (Opaleye α) → Select (Opaleye β)
.Haskell
: ΛSQL → Haskell that sends:IO [Haskell α]
withHaskell α
a type derived from the table header α in such a way thatHaskell α
is inhabited by exactly as many total values as there may be distinct rows in a table with header α.IO [Haskell α] → IO [Haskell β]
built fromfilter
,sort
and other familiar Haskell functions.Thus we have two parallel functors
Opaleye
,Haskell
: ΛSQL → Haskell.Now
runSelect connection
is a bunch of arrows from which we can variously pick transformations betweenOpaleye
andHaskell
. Among these transformations some are natural. Among these natural transformations, some are such that every componentrunSelect @(Opaleye α) @(Haskell α) connection
is initial in the slice category (Select α ↓ Haskell) — that is to say, any arrow of typeOpaleye α → x
splits one way asf ∘ runSelect @(Opaleye α) @(Haskell α) connection
. Since all initial objects of a category are essentially the same, all thus defined natural transformations are essentially the same as well — they are the initial object in the category (Opaleye
↓ ΛSQL → Haskell) of natural transformations fromOpaleye
.In the example above, the arrow
runSelect connection ∷ Select (Field SqlInt4, Field SqlText) → IO [String]
splits ashowManyPaws ∘ runSelect @(Field SqlInt4, Field SqlText) @(Int32, Text) connection
.alternatives
I am aware of
runSelectI
andInferrable FromFields
. There are several problems with this solution:The inference does not work well with product profunctors and custom tuples. See:
— Here GHC will give you an error «Ambiguous type variable ‘haskells0’ arising from a use of ‘runSelectI’». It is not clear how to solve this problem with
runSelectI
. With the type familyDefaultRow
the solution is straightforward:Even if the result
X
ofrunSelectI
is determined by the constraintβ ~ X ⇒ Default (Inferrable FromFields) A β
, it is not clear how to mention it in client code. Once you callrunSelectI
, you will want to do something with the result, but you cannot even refer to its type!The choices made for the default instances are not always universal. For example:
SqlInt4
refers to 32 bit wide numbers in PostgreSQL, butInt
is 64 bit wide on my computer. Thus, there is no initial natural transformation with a component going fromSelect (Column SqlInt4)
toIO [Int]
.SqlInt4
should refer toInt32
, asSqlInt8
already correctly refers toInt64
.