The code doesn't compile because the types don't match. Let's load up a GHCI session and look at the types of the functions you're using -
> :t writeFile
writeFile :: FilePath -> String -> IO ()
>
> :t readFile
readFile :: FilePath -> IO String
So writeFile wants a FilePath and a String. You want to get the String from readFile - but readFile returns IO String instead of String.
Haskell is a very principled language. It has a distinction between pure functions (which give the same outputs every time they are called with the same arguments) and impure code (which may give different results, e.g. if the function depends on some user input). Functions that deal with input/output (IO) always have a return type which is marked with IO. The type system ensures that you cannot use impure IO code inside pure functions - for example, instead of returning a String the function readFile returns an IO String.
This is where the <- notation is important. It allows you to get at the String inside the IO and it ensures that whatever you do with that string, the function you are defining will always be marked with IO. Compare the following -
> let x = readFile "tmp.txt"
> :t x
x :: IO String
which isn't what we want, to this
> y <- readFile "tmp.txt"
> :t y
y :: String
which is what we want. If you ever have a function that returns an IO a and you want to access the a , you need to use <- to assign the result to a name. If your function doesn't return IO a , or if you don't want to get at the a inside the IO then you can just use =.