Packing a value in an existential type knowing only a type class

102 views Asked by At

Further to my goal of reducing the boilerplate required to use Haxl with a relational database, I am trying to package up the result of a raw SQL request via Persistent in an existentially quantified type. However the type checker won't allow it:

data SomeRawSql where
  SomeRawSql :: forall b. RawSql b => [b] -> SomeRawSql

packedVal = let res = runDB $ rawSql "SELECT * FROM ..." [toPersistValue (pack "ABC")]
             in fmap SomeRawSql res

This results in a type error on the line with fmap: Ambiguous type variable ‘b0’ arising from a use of ‘SomeRawSql’ prevents the constraint ‘(RawSql b0)’ from being solved.

The type of rawSql from persistent is:

rawSql :: (RawSql a, MonadIO m)
       => Text             -- ^ SQL statement, possibly with placeholders.
       -> [PersistValue]   -- ^ Values to fill the placeholders.
       -> ReaderT SqlBackend m [a]

runDB is a helper function that connections to the database and returns IO [a]. Based on the definition of rawSql I would expect the RawSql constraint to be satisfied. I don't understand why this error arises.

1

There are 1 answers

5
leftaroundabout On

rawSql is universally quantified. That means, it does not “extract a RawSql instance from the database”, which would be what the existential type SomeRawSql expresses. Instead it can extract values from the database provided they have a RawSql instance. What type this is is chosen by the caller.

You could also wrap the universal quantification in a parameterless type:

data SomeRawSql where
  SomeRawSql :: (forall b. RawSql b => [b]) -> SomeRawSql

but I don't think that would be sensible, it just kicks the burden of choosing a type down the road. Parametricity is a good thing, it allows you to actually keep track of what types are going where. Don't circumvent it without a real reason!

An entirely different subject is if you want to retrieve a value whose type you really do not know. That's not covered by rawSql, you'd need to implement it yourself with wrappers like Dynamic.