Skip to content

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:
To upload designs, you'll need to enable LFS and have an admin enable hashed storage. More information