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