0

I'm using hspec to do some basic testing.

I have a argsParser function which given some arguments, returns or rather prints their validity.

argsParser :: [String] -> IO ()
argsParser args | null args = print "no args provided"
                | not $ null args && length args < 2 = print "no file name provided"
                | length args > 2 == print "too many arguments"
                | otherwise = goAhead args

The problem is I'm not sure how I'd compare IO () with another IO ().

I thought maybe liftIO could help but

x <- liftIO $ print "something"
y <- liftIO $ print "anything"

I get

x == y = True

which I suspect is because both are actions.

distro.obs
  • 181
  • 9
  • 4
    `x==y` is `True` because both `x` and `y` are `()`. They're not actions - that would be the case if you defined `x = ...` instead of `x <- ...`, in which case you'd get an error because the type `IO ()` can't be checked for equality. – sepp2k Mar 06 '19 at 09:30
  • You could use a free monad to gain equality and testability but they have a cost: see https://stackoverflow.com/questions/13352205/what-are-free-monads (you can read about caveats and alternatives here: https://markkarpov.com/post/free-monad-considered-harmful.html) – jneira Mar 06 '19 at 09:50
  • 2
    `argsParser` is doing too many things. Something like `argsParser :: [String] -> Either ArgsError [String]` returns either the valid strings or an error (`data ArgsError = NoArgs | NoFilename | TooManyArgs`) that some other function can act on. – chepner Mar 06 '19 at 13:37

2 Answers2

11

You can't compare an IO action to another one. Computability theory states that there is no way to decide whether two IO values are equivalent. Consequently, there is no instance Eq (IO a) in Haskell.

At best, you can try to run the two actions, observe their effect from outside, and compare their effects -- this won't always work (e.g. if an action is an infinite loop, if the action requires user input) but it could be close enough. Implementing this check could be done by running the actions as subprocesses, redirecting their standard output/error.

(Why do you want to compare IO actions, though? That's pretty unusual)

chi
  • 111,837
  • 3
  • 133
  • 218
  • 1
    Well, if you *could* compare IO actions it would be really useful. You could start with `print True == print foundFermatCounterExample`. – Paul Johnson Mar 06 '19 at 15:29
  • 1
    @PaulJohnson That sounds misleading to me. I can compare Bools, but where does `True == foundFermatCounterExample` actually get me? It's not `(==)`'s fault if you pass it arguments that may loop forever before producing their value. – Daniel Wagner Mar 06 '19 at 16:06
  • @DanielWagner Determining if two arbitrary `IO ()` values do the same thing in all circumstances is equivalent to solving the halting problem. If you can solve the halting problem then you can prove Fermat's last theorem by testing if a search for a counter-example will halt. You don't actually run the search, you just submit it to your halting test. – Paul Johnson Mar 06 '19 at 18:00
  • @PaulJohnson My point is that determining if two arbitrary `Bool` terms do the same thing in all circumstances is *also* equivalent to solving the halting problem. Since that is not a difference between `Bool` and `IO ()`, it cannot be the explanation for the difference that we have `instance Eq Bool` but not `instance Eq (IO ())`. – Daniel Wagner Mar 06 '19 at 18:10
  • @DanielWagner My guess is that `Bool`'s bottoms are uninteresting (`error "..", undefined, fix id, ...`) so we like to pretend they do not really exist, hence we accept `instance Eq Bool`. Indeed, if we did not do that, no meaningful `Eq` instance could exist for any type. Instead, `IO ()` bottoms can be quite interesting, like `forever (print 42)`. Further, even if we ignored bottoms, to compare `x,y :: IO a` (assuming `Eq a`), we would need to actually run IO actions, but that would only produce an `IO Bool`, and using unsafe ops to get a `Bool` would be a very bad move. – chi Mar 06 '19 at 19:05
  • `foundFermatCounterExample` seems to me to be a `Bool` which might or might not be bottom and, if it is bottom, qualifies as an "interesting" bottom. `forever (print 42)` is not bottom at all, and is no good as an explanation either, since `repeat 42` is analogous and does not prevent us from having an `Eq` instance for lists. I do not see why I must accept a priori that comparing IO actions requires running them. (N.B. I'm not saying we *should* have `instance Eq (IO ())` -- just that these arguments aren't convincing ones!) – Daniel Wagner Mar 06 '19 at 19:43
  • Interesting discussion, but we've known for about 25 years now that `foundFermatCounterExample` will indeed be bottom. Funny how FLT still seems to be the first example people reach for of a difficult "unsolved" problem. Probably should be the Riemann Hypothesis now :) (My knowledge of pure mathematics research is about 10 years out of date though...) – Robin Zigmond Mar 06 '19 at 20:20
4

First, simplify argsParser into something that just checks the number of args; don't do any IO yet.

import System.IO

data ArgsError = NoArgs | NoFilename | TooManyArgs
instance Show ArgsError where
    show NoArgs = "no args specified"
    show NoFilename = "no file name specified"
    show TooManyArgs = "too many arguments"

validateArgs :: [String] -> Either ArgsError [String]
validateArgs args | null args = Left NoArgs
                  | length args < 2 = Left NoFilename
                  | length args > 2 = Left TooManyArgs
                  | otherwise = Right args

goAhead :: [String] -> IO ()
-- as before

Now you just need a handler for the Either ArgsError [String] value, which is a simply application of either :: (a -> c) -> (b -> c) -> Either a b -> c.

main = do
    args <- getArgs
    either (hPrint stderr) goAhead (validatedArgs args)

You can now easily test validateArgs, and arguably either (hPrint stderr) goAhead doesn't need to be tested.

chepner
  • 497,756
  • 71
  • 530
  • 681