You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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_ (FieldSqlInt4, FieldSqlText)
example = table _ _
Nothing stops me from writing:
howManyPaws∷ (Int32, Text) →String
howManyPaws (int, text) ="A "<>Text.unpack text <>" has "<>show int <>" paws."instanceDefaultFromFields (FieldSqlInt4, FieldSqlText) Stringwhere
def =fmap howManyPaws def
selectExample∷Select (FieldSqlInt4, FieldSqlText)
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.
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:
typeinstanceDefaultRow (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:
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.
The text was updated successfully, but these errors were encountered:
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
.The text was updated successfully, but these errors were encountered: