How concatenate tuples in phantom types in Haskell?
Asked Answered
C

1

6

I'm writing a SQL combinator which allows SQL fragments to be composed as a Monoid. I have roughly a type like this (this is a simplified implementation) :

data SQLFragment = { selects :: [String], froms :[String], wheres :: [String]}

instance Monoid SQL Fragment where ...

This allows to me to combine easily bits of SQL I use often and do things like :

email = select "email" <> from "user" 
name  = select "name" <> from "user"
administrators = from "user" <> where_ "isAdmin = 1"

toSql $ email <> name <> administrators
=> "SELECT email, name FROM user WHERE isAdmin = 1"

That works very well and I'm happy with it. Now I use MySQL.Simple and to be executed it needs to know the type of a row.

main = do
       conn <- SQL.connect connectInfo
       rows <- SQL.query_ conn $ toSql (email <> name <> administrators)
       forM_ (rows :: [(String, String)]) print

Which is why I need the

 rows :: [(String, String)]

To avoid to have add manually this explicit (and useless) type signature I had the following idea : I add a phantom type to my SQLFragment and use it to force the type of the query_ function. So I could have something like this

email = select "email" <> from "user" :: SQLFragment String
name  = select "name" <> from "user" :: SQLFragment String
administrators = from "user" <> where_ "isAdmin = 1" :: SQLFragment ()

etc ...

Then I can do

query_ :: SQL.Connection -> SQLFragment a -> IO [a]
query_ con q = SQL.query_ conn (toSql q)

My first problem is I can't use <> anymore because SQLFragment a is not a Monoid anymore. The second is how do I implement my new <> to compute correctly the phantom type ?

I found a way which I think is ugly and I hope there is a much better solution. I created a typed version of SQLFragment and use phantom attribute which is a HList.

data TQuery e = TQuery 
               { fragment :: SQLFragment
               , doNotUse :: e
               }

then I create a new typed operator : !<>! which I don't undestand the type signature so I don't write it

(TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q') (e.*.e')

Now I can't combine my typed fragment and keep track of the type (even though it's not yet a tuple but something really weird).

To convert this weird type to a tuple I create a type family :

type family Result e :: *

and instantiate it for some tuples

Another solution would be probably to use a type family and write manually every combination of tuples

type instance Result (HList '[a])  = (SQL.Only a)
type instance Result (HList '[HList '[a], b])  = (a, b)
type instance Result (HList '[HList '[HList '[a], b], c])  = (a, b, c)
type instance Result (HList '[HList '[HList '[HList '[a], b], c], d])  = (a, b, c, d)
type instance Result (HList '[HList '[HList '[HList '[HList '[a], b], c], d], e])  = (a, b, c,d, e)

etc ...

And that works. I can write my function using the Result family

execute :: (SQL.QueryResults (Result e)) => 
        SQL.Connection -> TQuery e -> SQL.Connection -> IO [Result e]
execute conn (TQuery q _ ) = SQL.query_ conn (toSql q)

My main program looks like :

email = TQuery (select "email" <> from "user") ((undefined :: String ) .*. HNil)
name  = TQuery (select "name" <> from "user" ) ((undefined :: String ) .*. HNil)
administrators = TQuery (from "user" <> where_ "isAdmin = 1") (HNil)

main = do
       conn <- SQL.connect connectInfo
       rows <- execute conn $ email !<>! name !<>! administrators
       forM_ rows print

and it works!

However is there a better way to do it , especially without using HList and if possible less extensions as possible ?

If I "hide" somehow the phantom type (so I can have a real Monoid and be able to use <> instead of !<>!) is there a way to get the type back?

Contour answered 4/6, 2014 at 17:18 Comment(5)
What's wrong with writing a secondary function strQuery :: Connection -> Query -> IO [(String, String)]; strQuery = SQL.query_, much like writing a function readInt :: String -> Int; readInt = read? If you're always getting the same return type back, or just one of a handful of types, then this approach should be pretty manageable and doesn't require any fancy plumbing to work. Otherwise I see no problem with having to specify the type inline.Liberal
in (TQuery q e) !<>! (TQuery q' e') = TQuery (q<>q) (e.*.e') perhaps you meant q<>q'?Macintosh
@bheklir: What's wrong in adding a type signature is that it's redundant and in theory unnecessary, each time I modify the query, I need to modify the type signature (If I need to modify something at 2 places it's redundant). Moreover, any type signature will type check but might fail at runtime. With my plumbery, once a column has been declared properly (which is done once and that I generated from the schema), every query combinations which type check will works. So it's basically "type" safer.Contour
I am very confused by the TQuery type, the !<>! operator, and the .*. one, which I cannot find with hoogle. Could you explain?Macintosh
@didierc: Hoogle doesn't work but Hayoo find it. .*. comes from HList , it's the heterogeneous version of :. Prepend something to a HList.Contour
E
2

Consider using haskelldb which has the typed database query problem figured out. The records in haskelldb work fine, but they don't provide many operations, and the types are longer since they don't use -XDataKinds.

I have some suggestions for your current code:

newtype TQuery (e :: [*]) = TQuery SQLFragment

is better because the e is actually a phantom type. Then your append operation can look like:

(!<>!) :: TQuery a -> TQuery b -> TQuery (HAppendR a b)
TQuery a !<>! TQuery b = TQuery (a <> b)

Result then looks much cleaner:

type family Result (a :: [*])
type instance Result '[a])  = (SQL.Only a)
type instance Result '[a, b]  = (a, b)
type instance Result '[a, b, c]  = (a, b, c)
type instance Result '[a, b, c, d]  = (a, b, c, d)
type instance Result '[a, b, c, d, e]  = (a, b, c,d, e)
-- so you might match the 10-tuple mysql-simple offers

If you want to stay with HList+mysql-simple and duplicate parts of haskelldb, an instance QueryResults (Record r) is probably appropriate. An unreleased Read instance solves a very similar problem and might be worth looking at.

Etty answered 5/6, 2014 at 7:32 Comment(1)
That's much cleaner indeed and pretty much what I'm looking for, althought I would prefer to get ridoff HList entirely. I've been considering HaskellDB as well as Esqueletto but none of them were quite what I need for what I'm trying to do, which is basically not have to specify the "from" clause and do auto-join.Contour

© 2022 - 2024 — McMap. All rights reserved.