Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How concatenate tuples in phantom types in Haskell?

Tags:

haskell

hlist

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?

like image 984
mb14 Avatar asked Jun 04 '14 17:06

mb14


1 Answers

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.

like image 79
aavogt Avatar answered Sep 25 '22 15:09

aavogt