Importing Data.Char causes compiler behaviour to change

I'm learning Haskell, and as an exercise I was asked to write a function nextlet which returns the next letter after the specified parameter (a character). The specification says to assume that 'a' follows 'z' and 'A' follows 'Z', and I made an executive decision that invoking the function on a character outside the ranges A to Z and a to z returns ' '.

My first attempt was:

nextlet c | c == 'Z' = 'A'
          | c == 'z' = 'a'
          | c `elem` ['A'..'Y'] = succ c
          | c `elem` ['a'..'y'] = succ c
          | otherwise = ' '

and this worked.

However, I then thought it would be nice not to have to duplicate the processing for upper and lower case, so I found out how to convert a character to upper case, and coded:

nextlet c | c == 'Z' = 'A'
          | c `elem` ['A'..'Y'] = succ c
          | c `elem` ['a'..'z'] = nextlet toUpper c
          | otherwise = ' '

This failed to compile with the error message "Variable not in scope: toUpper :: Char"; which made perfect sense because I hadn't imported Data.Char.

But when I included import Data.Char at the top of my script, it still failed to compile. The very first line of the function definition fails with

Couldn't match type ‘Char’ with ‘Char -> Char’
    Expected type: (Char -> Char) -> Char -> Char
      Actual type: Char -> Char
     
     nextlet c | c == 'Z' = 'A'
     ^^^^^^^^^^^^^^^^^^^^^^^^^^

What is wrong with this, and why does importing Data.Char make the line fail to compile when it succeeded before?


Solution 1:

You should use nextlet (toUpper c), otherwise you pass toUpper as parameter, which is a function Char -> Char, not a Char.

You thus implement this as:

nextlet c | c == 'Z' = 'A'
          | c `elem` ['A'..'Y'] = succ c
          | c `elem` ['a'..'z'] = nextlet (toUpper c)
          | otherwise = ' '

Solution 2:

Willem Van Onsem's answer explains how to fix your problem, but not why the compiler's behavior seemed to change. GHC has multiple passes. The two important ones in this case are the renamer and the typechecker. When you use a name that can't be resolved, the renamer fails and compilation stops, so you won't see any type errors even if you have them. When you made the name resolve by importing Data.Char, you revealed a type error that was there all along.