[Haskell-cafe] Using Template Haskell to make type-safe database access

Bjorn Bringert bjorn at bringert.net
Tue May 13 15:00:01 EDT 2008


On Thu, May 8, 2008 at 5:32 PM, Mads Lindstrøm <mads_lindstroem at yahoo.dk> wrote:
> Hi Wouter,
>
>  Wouter Swierstra wrote:
>
>
> > Nice! I have to admit, it's much nicer than I expected it to be. Just
>  > out of curiousity, what happens when you write:
>  >
>  > selectTupleList :: Connection -> IO [Integer]
>  >
>  > instead of
>  >
>  > selectTupleList :: Connection -> IO [(Integer, String, String)]
>  >
>  > What kind of error message do you get? More specifically, is this
>  > error caught statically or dynamically.
>
>  The type annotation in UseSqlExpr.hs was just for the reader. The
>  compiler can infer the types completely. Thus when I make the suggested
>  change I get a compile time error. It looks like this:
>
>  UseSqlExpr.hs:27:6:
>     Couldn't match expected type `Integer'
>            against inferred type `(Integer, String, String)'
>       Expected type: IO [Integer]
>       Inferred type: IO [(Integer, String, String)]
>     In the expression:
>         (return
>        $ (map
>             (\ [x0[a2ZY], x1[a2ZZ], x2[a300]]
>                  -> (readInteger x0[a2ZY],
>                      readString x1[a2ZZ],
>                      readString x2[a300]))
>             rows[a2ZX]))
>     In the expression:
>         do rows[a2ZX] <- fetchRows
>                            dsn[a2ZW]
>                            ['S', 'E', 'L', 'E', 'C', 'T', ' ', 'u', 's', 'e', 'r', '_', 'i',
>                             'd', ',', ' ', 'u', 's', 'e', 'r', '_', 'n', 'a', 'm', 'e', ',',
>                             ' ', 'u', 's', 'e', 'r', '_', 'r', 'e', 'a', 'l', '_', 'n', 'a',
>                             'm', 'e', ' ', 'F', 'R', 'O', 'M', ' ', 'u', 's', 'e', 'r', ';']
>            (return
>           $ (map
>                (\ [x0[a2ZY], x1[a2ZZ], x2[a300]]
>                     -> (readInteger x0[a2ZY],
>                         readString x1[a2ZZ],
>                         readString x2[a300]))
>                rows[a2ZX]))
>  make: *** [all] Fejl 1
>
>
>  >
>  > The only other limitation I can think of, would be in the situation
>  > where you don't have compile-time access to the database, e.g.
>  > developing software for a client with a database that can only be
>  > accessed from their intranet. I have no idea how much of a limitation
>  > that is.
>
>  True, but this limitation is only relevant when you do not have access
>  to the production database or a database with identical metadata. How
>  often do people develop like that? How are they testing? I have a hard
>  time picturing a setup without a test database with identical metadata
>  to the production database.
>
>
>  > >> Perhaps I should explain my own thoughts on the subject a bit better.
>  > >> I got interested in this problem because I think it makes a nice
>  > >> example of dependent types "in the real world" - you really want to
>  > >
>  > > But won't you end up implementing all the functionality of an SQL
>  > > parser? While possible, it does seem like a huge job. With a TH
>  > > solution
>  > > you will safe a lot of work.
>  >
>  > Yes - but parsing the result of an SQL describe statement is pretty
>  > easy.
>  ok.
>
>
>  >
>  > > A library that
>  > > will be a lot more complex to learn than what I am proposing (assuming
>  > > the developer already knows SQL).
>  >
>  > Hmm. This is a rather sticky point. One might also argue that Haskell
>  > developers have to learn SQL to use the solution you propose. I'm not
>  > particularly convinced. Both approaches have their merits I think.
>
>  Yes. I was _not_ making what you could call a strong argument. I was
>  assuming that most (Haskell) developers knew SQL anyway. I have no data
>  to back it up. Just my gut feeling.
>
>  To be fair I should mention a couple of drawbacks with the TH-based
>  approach. While SQL got static typing, it is not really as powerful as
>  it could be. For example if you do "select sum(...) from ..." the type
>  system will tell you that a set of values are returned. In reality this
>  set will never have more than one member. Your proposal would be able to
>  return a Float in stead of a [Float].
>
>  Another advantage your proposal (and disadvantage of the TH based one)
>  would be that it can abstract over variances in different database
>  implementation. That is, you could translate to SQL depending on SQL
>  backend. This would be really nice. But I guess it would also be a big
>  task.
>
>
>  >
>  > Anyhow - nice work! Have you asked Bjorn Bringert what he thinks? He's
>  > a really clever and approachable guy - and he knows a lot more about
>  > interfacing with databases than I do.

I guess I'll just have to reply then :-)

Mads: Preparing the statement and asking the DB about the type at
compile is a great idea! I've never thought of that. Please consider
completing this and packaging it as a library.

I can't really see any major problems with this approach, other than
the obivious "changing schema" problem that it shares with HaskellDB.
Of course there are some things that it won't catch, like the
singleton results from aggregates, but it does go a long way towards a
statically safe DB interface.

I think that the main disadvantages compared to HaskellDB are that:
- It won't let you write your own query combinators.
- The program becomes dependent on the exact SQL dialect you chose to use.

On the other hand it addresses some of the problems with HaskellDB:
- Poorly optimized queries.
- Missing SQL features (e.g. outer joins).
- Poor support for DB-specific features.
- Difficult to understand type errors.

/Björn


More information about the Haskell-Cafe mailing list