Skip to content

Error messages for functional dependencies are missing critical details

Summary

GHC's functional dependency errors are quite bad when a downstream usage of a variable resolves to one type, and the definition expects another: they only list the definition site's type. This leads to spooky action at a distance.

This is felt especially badly in libraries like Esqueleto which use functional dependencies in such a way that type errors in usages of anything defined inside a from will appear as a functional dependency error on that builder, rather than where they came from inside, or at the other usage causing the conflicting inference.

I have provided a reproducer of this issue with a tiny query, but this is significantly worse in industry code with hundred-line queries. This error is significantly worse when you have a query with 25 different Text values in it, and the only way to figure out which one was accidentally not Maybe is to replace stuff with undefined or otherwise bisect.

The actual issue with the provided code below is that:

    pure $ li ?. #name

should be

    pure $ li ^. #name

where:

λ> import Database.Esqueleto.Experimental
λ> :t (^.)
(^.)
  :: (PersistEntity val, PersistField typ) =>
     SqlExpr (Entity val) -> EntityField val typ -> SqlExpr (Value typ)
λ> :t (?.)
(?.)
  :: (PersistEntity val, PersistField typ) =>
     SqlExpr (Maybe (Entity val))
     -> EntityField val typ -> SqlExpr (Value (Maybe typ))

Steps to reproduce

Please provide a set of concrete steps to reproduce the issue.

Clone the reproducer from https://gitlab.haskell.org/lf-/fundep-repro

The interesting code is reproduced below for convenience:

typeError :: MonadIO m => SqlPersistT m (Maybe (Value Text))
typeError = selectOne $ do
    (li :& sg) <- from $ table @LineItem
        `leftJoin` table @Something
        `on` (\(li :& sg) -> just (li ^. #id) E.==. sg ?. #lineItemId)
    pure $ li ^. #name
dev/ghcrepro - [main●] » cabal build
Build profile: -w ghc-9.5.20220714 -O1
In order, the following will be built (use -v for more details):
 - lunchline-0.0.0 (exe:lunchline) (file app/Main.hs changed)
Preprocessing executable 'lunchline' for lunchline-0.0.0..
Building executable 'lunchline' for lunchline-0.0.0..
[1 of 3] Compiling Main             ( app/Main.hs, /Users/jade/dev/ghcrepro/dist-newstyle/build/aarch64-osx/ghc-9.5.20220714/lunch
line-0.0.0/x/lunchline/build/lunchline/lunchline-tmp/Main.o, /Users/jade/dev/ghcrepro/dist-newstyle/build/aarch64-osx/ghc-9.5.2022
0714/lunchline-0.0.0/x/lunchline/build/lunchline/lunchline-tmp/Main.dyn_o ) [Source file changed]

app/Main.hs:40:13: error:
    • Couldn't match type ‘Text’ with ‘Maybe typ0’
        arising from a functional dependency between:
          constraint ‘Database.Esqueleto.Internal.Internal.SqlSelect
                        (SqlExpr (Value (Maybe typ0))) (Value Text)’
            arising from a use of ‘selectOne’
          instance ‘Database.Esqueleto.Internal.Internal.SqlSelect
                      (SqlExpr (Value a)) (Value a)’
            at <no location info>
    • In the first argument of ‘($)’, namely ‘selectOne’
      In the expression:
        selectOne
          $ do (li :& sg) <- from
                               $ table @LineItem
                                   `leftJoin`
                                     table @Something
                                       `on`
                                         (\ (li :& sg) -> just (li ^. #id) E.==. sg ?. #lineItemId)
               pure $ li ?. #name
      In an equation for ‘typeError’:
          typeError
            = selectOne
                $ do (li :& sg) <- from
                                     $ table @LineItem
                                         `leftJoin`
                                           table @Something
                                             `on`
                                               (\ (li :& sg)
                                                  -> just (li ^. #id) E.==. sg ?. #lineItemId)
                     pure $ li ?. #name
   |
40 | typeError = selectOne $ do
   |             ^^^^^^^^^

app/Main.hs:41:19: error:
    • Couldn't match type: Entity LineItem
                     with: Maybe (Entity val0)
        arising from a functional dependency between:
          constraint ‘Database.Esqueleto.Experimental.From.ToFrom
                        (From
                           (SqlExpr (Entity LineItem) :& SqlExpr (Maybe (Entity Something))))
                        (SqlExpr (Maybe (Entity val0))
                         :& SqlExpr (Maybe (Entity Something)))’
            arising from a use of ‘from’
          instance ‘Database.Esqueleto.Experimental.From.ToFrom (From a) a’
            at <no location info>
    • In the first argument of ‘($)’, namely ‘from’
      In a stmt of a 'do' block:
        (li :& sg) <- from
                        $ table @LineItem
                            `leftJoin`
                              table @Something
                                `on` (\ (li :& sg) -> just (li ^. #id) E.==. sg ?. #lineItemId)
      In the second argument of ‘($)’, namely
        ‘do (li :& sg) <- from
                            $ table @LineItem
                                `leftJoin`
                                  table @Something
                                    `on` (\ (li :& sg) -> just (li ^. #id) E.==. sg ?. #lineItemId)
            pure $ li ?. #name’
   |
41 |     (li :& sg) <- from $ table @LineItem
   |                   ^^^^

Expected behavior

What do you expect the reproducer described above to do?

I would like to know where the conflicting usage is. For instance, I would like to see that that the other direction of bad inference came from li ^. #name.

Environment

  • GHC version used:

I grabbed HEAD to confirm this still is a bug, but it also happens on 9.0, 9.2.

ghc --version
The Glorious Glasgow Haskell Compilation System, version 9.5.20220714

Optional:

  • Operating System: macOS
  • System Architecture: aarch64
Edited by Jade Lovelace
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information