Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

A default DefaultFromFields via a type family? #553

Open
kindaro opened this issue Jul 7, 2022 · 0 comments
Open

A default DefaultFromFields via a type family? #553

kindaro opened this issue Jul 7, 2022 · 0 comments

Comments

@kindaro
Copy link

kindaro commented Jul 7, 2022

A default DefaultFromFields via a type family?

problem

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.

Generally, we can have:

runSelectDefault = runSelect  _  _  Select fields  IO [DefaultRow fields]

— 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 ….

  • We can embed this category into Haskell via the functor Opaleye: ΛSQLHaskell 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: ΛSQLHaskell 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: ΛSQLHaskell.

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ΛSQLHaskell) 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:

    data CustomTuple α β = CustomTuple α β
    
    makeAdaptorAndInstance "pCustomTuple" ''CustomTuple
    
    selectExampleWithCustomTuple  Select (CustomTuple (Field SqlInt4) (Field SqlText))
    selectExampleWithCustomTuple = _
    
    example = runSelectI _ selectExampleWithCustomTuple

    — 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

    SqlInt4 refers 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant