Skip to content

Hard-to-read/misleading errors for types that are missing an import and interpreted as variables

This is something that has tripped me up a few times recently, so I thought it might be worth filing a report for it. Even though the frontend technically exhibits the correct behavior, the error messages are highly misleading (and for larger programs often hard-to-read too).

Consider the following program:

import Data.IORef (newIORef)

f :: Int -> IO (IORef Int)
f n = do
  x <- newIORef 42
  return x

The programmer forgot to import the IORef type, otherwise the program should be correct. GHC outputs a helpful note here:

src/MissingImport.hs:3:17: error: [GHC-76037]
    Not in scope: type constructor or class ‘IORef’
    Suggested fix:
      Perhaps you want to add ‘IORef’ to the import list in the import of
      ‘Data.IORef’ (src/MissingImport.hs:1:1-28).
  |
3 | f :: Int -> IO (IORef Int)
  |                 ^^^^^

Curry, on the other hand, emits this:

src/MissingImport.curry:3:6-3:26 Error:
    Type signature too general
    Function: f
    Inferred type: Prelude.Int ->
                   Prelude.IO (Data.IORef.IORef Prelude.Int)
    Type signature: Int -> IO (IORef Int)
   | 
 3 | f :: Int -> IO (IORef Int)
   |      ^^^^^^^^^^^^^^^^^^^^^
ERROR occurred during parsing!

To the untrained eye, this error message is highly confusing, since it looks like the inferred and expected type are the same. The problem here is the interaction with a legacy feature (that may or may not be considered deprecated) that Curry supports different case modes and, by default, interprets even capitalized type identifiers that aren't otherwise imported as type variables. Therefore the second/expected IORef is interpreted as a type variable here, in other words, the Curry compiler reads the program as

f :: Int -> IO (a Int)
f n = do
  x <- newIORef 42
  return x

Now we do have a warning for this and if we update the function to

f :: Int -> IO (IORef Int)
f = failed

the warning is emitted correctly too (since it now typechecks as Int -> IO (a Int) too):

src/MissingImport.curry:3:17-3:21 Warning:
    Symbol `IORef' is a variable name, but the selected case mode is `curry`, try renaming to iORef instead
   | 
 3 | f :: Int -> IO (IORef Int)
   |                 ^^^^^

The immediate fix I see here is to therefore move the case mode check before the type checker (or even earlier) and to find some way to emit this warning even when errors are emitted. In the long term, we might consider improving the typechecker's error messages too (maybe clarify which types are interpreted as variables?).

Edited by Fredrik Wieczerkowski