0

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.

RichardW
  • 899
  • 10
  • 15
  • 1
    “I don’t understand why this error arises”: it’s exactly the same error as `f x = show (read x)`. *Which type* do you use as the intermediate? The existential wrappers ("`String`" and `SomeRawSql`) can’t hold polymorphic values. @leftaroundabout is right: you do not want existential quantification. Roughly, existential quantification lets the producer choose whatever type they want and make it opaque to the consumer, and universal quantification lets consumer choose the type and make it opaque to the producer. – HTNW Jan 16 '18 at 12:53

1 Answers1

3

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.

leftaroundabout
  • 117,950
  • 5
  • 174
  • 319
  • Thanks. I get the point about what RawSql does -- rawSql is like `read` and uses the dictionary associated with `a` to figure out what to do with the raw bytes returned from the database (right?) – RichardW Jan 16 '18 at 11:10
  • Actually "kicking the burden of choosing a type down the road" is exactly what I'm trying to accomplish here: I want to defer the resolution of a concrete type closer to the site of usage, after it's gone through the Haxl plumbing. The other question I linked would provide more context if you are curious. if I make the change to SomeRawSql that you suggested I get a slightly different error: `Couldn't match type ‘[a0]’ with ‘forall b. RawSql b => [b]` – RichardW Jan 16 '18 at 11:18
  • The type in this answer may have use in some limited number of cases; but this isn't one of them. You can't write `fmap SomeRawSql $ runDB $ rawSql ...` because `rawSql` produces `[a]` for some type `a` s.t. `RawSql a`; this entirely different than a value of type `(forall b. RawSql b => [b])` and there is no way in general to convert the former to the latter. You may have some luck with implementing an `instance RawSql SomeRawSql`. – user2407038 Jan 16 '18 at 13:28
  • @user2407038 no, `rawSql` produces `[a]` for **any** type a s.t. `RawSql a`, which is in fact also what `(∀b. RawSql b => [b])` expresses. The reason `fmap SomeRawSql $ runDB $ rawSql` doesn't work is a bit more technical... – leftaroundabout Jan 16 '18 at 13:44
  • 2
    @RichardW ...namely, what you would need is `ReaderT SqlBackend m (∀a . [a])` rather than `∀a . ReaderT SqlBackend m [a]`. But the former would be an _impredicative type_, which Haskell doesn't support. – leftaroundabout Jan 16 '18 at 13:47