Odd interaction with TypeError, ImplicitParams, and constraints in scope
Summary
The require-callstack
library provides a variant of HasCallStack
that gets propagated like a normal constraint. This makes it easy to find callsites of a function and update them to also provide a callstack. A custom TypeError
is used to provide a better error message for the end user.
However, there's some odd behavior. Consider this:
someFunc :: RequireCallStack => IO ()
someFunc = do
errorRequireCallStack "asdf"
let bar = errorRequireCallStack "qwer"
bar
The first errorRequireCallStack
picks up the RequireCallStackImpl
in the constraint of someFunc
, and is fine. However, the let bar = errorRequireCallStack
triggers the TypeError
.
If I remove the TypeError
instance, then the code works fine, but missing RequireCallStack
constraints only show the usual error message about a missing implicit parameter.
Steps to reproduce
This repository contains the code, in full.
Clone the repository and run cabal build
.
The code is short enough that I'll include it inline too:
{-# language RankNTypes, TypeFamilies, KindSignatures, MultiParamTypeClasses, DataKinds, ConstraintKinds, ImplicitParams, UndecidableInstances, FlexibleContexts, FlexibleInstances #-}
{-# OPTIONS_GHC -Wno-orphans -Wno-missing-methods #-}
module Lib where
import Data.Kind
import GHC.Stack (HasCallStack)
import GHC.Classes (IP(..))
import GHC.TypeLits (TypeError, ErrorMessage(..))
-- The Problem:
--
-- GHC complains about `bar` with the `TypeError` in the `instance IP
-- "provideCallStack" ProvideCallStack`.
--
-- Removing the type error allows the program to compile without error.
--
-- Adding a type signature to `bar` that specifies `RequireCallStack` also
-- fixes the issue.
someFunc :: RequireCallStack => IO ()
someFunc = do
errorRequireCallStack "asdf"
let bar = errorRequireCallStack "qwer"
bar
alsoWeird :: IO ()
alsoWeird = provideCallStack $ do
-- `RequireCallStack` should be a satisfied constraint here, as
-- evidenced by this building:
someFunc
-- But we get an error in this let binding.
let bar = errorRequireCallStack "qwer"
bar
errorRequireCallStack :: RequireCallStack => String -> x
errorRequireCallStack = error
instance TypeError ('Text "Add RequireCallStack to your function context or use provideCallStack") => IP "provideCallStack" ProvideCallStack
type RequireCallStack = (HasCallStack, RequireCallStackImpl)
type RequireCallStackImpl = ?provideCallStack :: ProvideCallStack
data ProvideCallStack = ProvideCallStack
provideCallStack :: (RequireCallStackImpl => r) -> r
provideCallStack r = r
where
?provideCallStack = ProvideCallStack
You can work around the behavior by providing a type signature for bar
or using provideCallStack
:
someFunc :: RequireCallStack => IO ()
someFunc = do
errorRequireCallStack "asdf"
let bar :: RequireCallStack => a
bar = errorRequireCallStack "qwer"
-- or,
bar = provideCallStack errorRequireCallStack "qwer"
bar
But both of these feel redundant with RequireCallStack
already present in the constraint.
Expected behavior
I would expect that a constraint like RequireCallStack
is satisfied even in let
bindings.
Environment
- GHC version used: 8.10.7, 9.2.7, 9.6.2
Optional:
- Operating System: Ubuntu Unity
- System Architecture: