tomjaguarpaw / haskell-opaleye

Other
599 stars 115 forks source link

A default `DefaultFromFields` via a type family? #553

Open kindaro opened 1 year ago

kindaro commented 1 year ago

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:

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

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: