Michael Pankov
Michael Pankov

Reputation: 3701

Why can't I compare result of lookup to Nothing in Haskell?

I have the following code:

import System.Environment
import System.Directory
import System.IO
import Data.List

dispatch :: [(String, [String] -> IO ())]
dispatch =  [ ("add", add)
            , ("view", view)
            , ("remove", remove)
            , ("bump", bump)
            ]

main = do
    (command:args) <- getArgs
    let result = lookup command dispatch
    if result == Nothing then
        errorExit
    else do
        let (Just action) = result
        action args

errorExit :: IO ()
errorExit = do
    putStrLn "Incorrect command"

add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ "\n")

view :: [String] -> IO ()
view [fileName] = do
    contents <- readFile fileName
    let todoTasks = lines contents
        numberedTasks = zipWith (\n line -> show n ++ " - " ++ line) [0..] todoTasks
    putStr $ unlines numberedTasks

remove :: [String] -> IO ()
remove [fileName, numberString] = do
    handle <- openFile fileName ReadMode
    (tempName, tempHandle) <- openTempFile "." "temp"
    contents <- hGetContents handle
    let number = read numberString
        todoTasks = lines contents
        newTodoItems = delete (todoTasks !! number) todoTasks
    hPutStr tempHandle $ unlines newTodoItems
    hClose handle
    hClose tempHandle
    removeFile fileName
    renameFile tempName fileName

bump :: [String] -> IO ()
bump [fileName, numberString] = do
    handle <- openFile fileName ReadMode
    (tempName, tempHandle) <- openTempFile "." "temp"
    contents <- hGetContents handle
    let number = read numberString
        todoTasks = lines contents
        bumpedItem = todoTasks !! number
        newTodoItems = [bumpedItem] ++ delete bumpedItem todoTasks
    hPutStr tempHandle $ unlines newTodoItems
    hClose handle
    hClose tempHandle
    removeFile fileName
    renameFile tempName fileName

Trying to compile it gives me the following error:

$ ghc --make todo
[1 of 1] Compiling Main             ( todo.hs, todo.o )

todo.hs:16:15:
    No instance for (Eq ([[Char]] -> IO ()))
      arising from a use of `=='
    Possible fix:
      add an instance declaration for (Eq ([[Char]] -> IO ()))
    In the expression: result == Nothing
    In a stmt of a 'do' block:
      if result == Nothing then
          errorExit
      else
          do { let (Just action) = ...;
               action args }
    In the expression:
      do { (command : args) <- getArgs;
           let result = lookup command dispatch;
           if result == Nothing then
               errorExit
           else
               do { let ...;
                    .... } }

I don't get why is that since lookup returns Maybe a, which I'm surely can compare to Nothing.

Upvotes: 2

Views: 3190

Answers (2)

Antal Spector-Zabusky
Antal Spector-Zabusky

Reputation: 36622

The type of the (==) operator is Eq a => a -> a -> Bool. What this means is that you can only compare objects for equality if they're of a type which is an instance of Eq. And functions aren't comparable for equality: how would you write (==) :: (a -> b) -> (a -> b) -> Bool? There's no way to do it.1 And while clearly Nothing == Nothing and Just x /= Nothing, it's the case that Just x == Just y if and only if x == y; thus, there's no way to write (==) for Maybe a unless you can write (==) for a.

There best solution here is to use pattern matching. In general, I don't find myself using that many if statements in my Haskell code. You can instead write:

main = do (command:args) <- getArgs
          case lookup command dispatch of
            Just action -> action args
            Nothing     -> errorExit

This is better code for a couple of reasons. First, it's shorter, which is always nice. Second, while you simply can't use (==) here, suppose that dispatch instead held lists. The case statement remains just as efficient (constant time), but comparing Just x and Just y becomes very expensive. Second, you don't have to rebind result with let (Just action) = result; this makes the code shorter and doesn't introduce a potential pattern-match failure (which is bad, although you do know it can't fail here).


1:: In fact, it's impossible to write (==) while preserving referential transparency. In Haskell, f = (\x -> x + x) :: Integer -> Integer and g = (* 2) :: Integer -> Integer ought to be considered equal because f x = g x for all x :: Integer; however, proving that two functions are equal in this way is in general undecidable (since it requires enumerating an infinite number of inputs). And you can't just say that \x -> x + x only equals syntactically identical functions, because then you could distinguish f and g even though they do the same thing.

Upvotes: 10

legoscia
legoscia

Reputation: 41618

The Maybe a type has an Eq instance only if a has one - that's why you get No instance for (Eq ([[Char]] -> IO ())) (a function can't be compared to another function).

Maybe the maybe function is what you're looking for. I can't test this at the moment, but it should be something like this:

maybe errorExit (\action -> action args) result

That is, if result is Nothing, return errorExit, but if result is Just action, apply the lambda function on action.

Upvotes: 8

Related Questions