problems figuring out what the type system is telling me

John Hughes rjmh@cs.chalmers.se
Sat, 8 Jun 2002 09:34:59 +0200 (MET DST)


On Fri, 7 Jun 2002, Chris Moline wrote:
> ...
> two. i have also read what the hell are monads and monads for the working
> haskell programmer. i still do not get what i am doing wrong.
>
> getDepends :: String -> [String]
> getDepends p = do
>         handle <- openFile (portsDir ++ p ++ "/+CONTENTS") ReadMode
>         fetchDepends handle
>
> to my brain this takes a string, concatenates it with portsDir and
> "+/CONTENTS", and passes it to openfile. openFile then returns a handle and
> this handle and passes it to fetchDepends. getDepends will return whatever
> fetchDepends returns, assuming openFile succeeds. however ghc says
>
> Phoebe.hs:19:
>     Couldn't match `[]' against `IO'
>         Expected type: [t]
>         Inferred type: IO Handle
>     In the application `openFile (portsDir ++ (p ++ "/+CONTENTS"))
>                                  ReadMode'
>     In a 'do' expression pattern binding:
>         handle <- openFile (portsDir ++ (p ++ "/+CONTENTS")) ReadMode
>
> i do not know what this [t] is and i do not know why it is expected. my theory
> is it is being caused by something in fetchDepends.

[t] is a list type: GHC is expecting the call of openFile to return a list
(of t, but t is just a type variable so tells us nothing). Why is it
expecting a list? BECAUSE OF YOUR TYPE SIGNATURE!

You wrote a type signature specifying that getDepends (and fetchDepends,
for that matter), returns a list. You should have given the result an IO
type, since these functions do IO. I haven't checked, but probably the
type of getDepends should be

	getDepends :: String -> IO [String]

rather than the type you wrote. So your mistake is just forgetting to
include the monad in your type signature.

Now, the error message you got maybe isn't the very clearest, but it is
logical. Remember that the do syntax that you used in getDepends is
OVERLOADED -- it can be used with any monad, not just with IO. In
particular, it can be used with the list type, which is itself a monad.
For example, we could, if we wished, define the ordinary list map function
like this:

	map :: (a->b) -> [a] -> [b]
	map f xs = do x <- xs
		      return (f x)

That's an example of using do with the list monad. Of course, since the do
is working over lists, then when we write x <- xs, the xs must also have a
list type. We have to be consistent, and use the same monad throughout the
do. This is just like when we use do to write an IO computation: in that
case, when we write x <- f y or whatever, the f y has to have an IO type.

Now, in your code for getDepends, GHC sees your type signature and says
"Aha! This function returns a list. So the do in the body must be working
over the list monad. In that case, when we see

	handle <- openFile...

then the openFile must have a list type. Oh dear, it's type isn't a list,
it's IO! Better complain that [] (the name of the list type, not
the empty list) doesn't match IO!"

Hence the error message you got.

And now to a thorny and controversial question: should one write type
signatures, or not? In particular, what advice should one give less
experienced Haskell programmers?

In this case, your CODE is probably quite correct. If you hadn't written
the type signatures, then GHC would just have inferred the correct types
and everything would have worked. Good advice for you might be to leave
type signatures out, compile your code, and then look at the types that
functions actually get (using ghci). You can always paste the types back
into your code, and this way, they will be correct.

On the other hand, if you omit type signatures, then when you DO get a
type error it will be in terms of type variables and classes, rather than
types such as Int or String which you would probably be expecting. There
is a trade off here.

One way to approach it is to write type signatures, but when a definition
doesn't type check, remove the signature on that one and see whether it
then typechecks. If so, the definition is right, but your type signature
is wrong. Use ghci to find out the correct type signature, and insert it.

You also mentioned layout problems when you used if in a do. I'm assuming
you tried writing something like

	do ....
	   if ... then ...
	   else ...
	   ...

If you write this, then because the else appears in the same column as the
if, it's taken to be the start of a new element in the do -- with a syntax
error as the result. You just have to indent the else further than the if.
I use an indentation like this:

	do ...
	   if ...
	     then ...
	     else ...
	   ...

which lets GHC see that the entire if-then-else makes up just one element
in the enclosing do. This is a classic gotcha of the layout rule: the
first layout is quite natural, but you just can't write it.