0

Having a script that looks like this:

main :: IO ()
main = do
  tehfile <- readIniFile "somefile"

  z <- case tehfile of
    Left a -> 42
    Right b -> print "bar"

  x <- case tehfile of
    Left a -> print "foo"
    Right b -> print "bar"

  return ()

I get errors like "No instance for (Num (IO ())) arising from the literal ‘42’". This is super cryptic, but changing the value 42 to different types I've been able to determine that the type of the function main - IO() does not match with the type of 42, Num.

My question is, why, despite having the result of pattern matching stored in variable z AND explicitly returning a (), not z, I still get a type error? In Scala, if there was no return (), x would be returned by convention. This behaviour by Haskell is genuinely puzzling for me as I can only interpret it as all variables in main:IO() need to be of the type IO(). Am I correct?

11
  • It's just because each expression has to have a type, and for a case expression, each case needs the same type. print "bar" has type IO (), but 42 can't have this type (unless you make a Num instance for IO () which would be strange and almost certainly meaningless). Commented Jan 3, 2020 at 13:33
  • Further, you have a do block in the IO monad (since the overall type is IO ()), so anything on the right of a "binding arrow" <- must have type IO a for some a. So it's not valid for a simple numeric literal to appear here (it's not an IO value). You could have used return 42 to get round this (except that 42 does not have () type). Commented Jan 3, 2020 at 13:36
  • By the way, I'm really not clear on what you're trying to do here, so it's hard for me to say exactly how you should write it instead. I'm guessing it's a highly simplified example to illustrate the problem, but if I had an idea of what result you wanted I could certainly try to give you some working code, along with an explanation of how it works and why, as an answer. Commented Jan 3, 2020 at 13:39
  • 1
    Oh, and one last thing: it seems you're one of many to be confused by Haskell's wilfully misleading return function. This is simply a function which "boxes" a value inside a monad - it has almost nothing in common with the identically-named keyword in imperative languages, and in particular has nothing to do with control flow. The "return value" of a function, as you might understand it, is simply the expression that the function body evaluates to. Commented Jan 3, 2020 at 13:43
  • 2
    no, it "returns" the "IO action" that is its content - a value of type IO a is essentially a "program" that, when run (by the Haskell runtime), will have some side effects and yield a value of type a. z and x are the values output by certain intermediate computations (which themselves can involve IO, eg reading a file), which you can then use in the rest of the Main computation (but which you appear not to do). Commented Jan 3, 2020 at 13:47

1 Answer 1

4

This answer is largely based on your comment, which I quote below so it doesn't get lost. (And many apologies for the length of it: I really didn't intend for this to be so mammoth, but there was a lot I felt it would be helpful to say. Hopefully it is, but don't hesitate to tell me if it isn't.)

Read INI file, read section list off it, print it into stdout, ask for user to choose one of them(simple getline is fine) and then set the selected section as [default]. Pretty much. My main struggle is constant type error when I store results of functions perfroming what I want in main.

And here is the code you linked to:

module Main where
import Data.Ini

main :: IO ()
main = do
  tehfile <- readIniFile "/Users/g.reshetniak/.aws/credentials" -- ~/.aws/credentials: openFile: does not exist --> why shell expansion doesn't work?

  s <- case tehfile of
    Left a -> print "woot"
    Right b -> head (map (\x -> print x) (sections b)) --TODO why this prints one element instead of mapping print over all section

  --TODO selected_profile <- getLine
  --TODO why `selected_profile <- getLine` is different from `let selected_profile = getLine` ?

  p <- case tehfile of
    Left c -> print "oops" --TODO why this does not print "oops" on lacking profile, instead prints Left "Couldn't find section: selected_profile"
    Right d -> print (lookupValue "selected_profile" "aws_access_key_id" d)
  return ()

Now I must admit that I've never come across an "INI file" before, so I'm not familiar with the format. So some of this is based on what I can glean from the documentation of the Data.Ini module you're using (which, in all fairness, seems very straightforward). So I can't promise to show you how to do exactly what you want (in particular I really don't know what you mean by "set the selected section as [default]") - but I hope to answer some of the questions you've asked in your code comments, and perhaps correct some misconceptions you may have about performing I/O in Haskell.

One of the first things I notice from this file, as in the code in your original question, is that you're binding variables to values, with the <- operator, but not then making use of them. Doing s <- someValue in a do block in Haskell is quite a bit like doing s = someValue in an imperiative language. It's pointless doing that in an imperative language if you don't then ever use the s variable afterwards, and it's exactly as pointless in Haskell, for exactly the same reason. In other words, this:

main = do
    ....
    s <- someValue
    ....

is, if the second .... doesn't mention s anywhere, completely equivalent to this:

main = do
    ....
    someValue
    ....

And note that someValue has been kept in there, because it may have some side effects that are important to the program - but whatever "result" the action returns, if any, isn't needed.

For example, here's just about the simplest possible console application in Haskell, a program that asks you for your name and then greets you using it:

main = do
    putStrLn "Please tell me your name"
    name <- getLine
    putStrLn $ "Welcome, " ++ name ++ "!

The way this works is that getLine is an "IO action" of type IO String. It is basically equivalent to Python's input function (more accurately, name <- getLine is equivalent to name = input() in Python): it is an "action" that, when executed, reads a line of input from the console, and then "returns" the string that the user entered. If you want to access that string, you have to do as above and bind it to a variable with the <- "operator". (It's not really an operator - it's actually a special kind of syntactic sugar, but don't worry about that for now, you can think of it as an operator if you want.) But, if you just wanted to greet the user the same way every time, then you wouldn't need to bind the result, and could just do:

main = do
    putStrLn "Please tell me your name"
    getLine
    putStrLn "Welcome!"

which still gives the user the pleasure of entering their name, even though the program then totally ignores it.

In general, when you have a do block inside main - or any other IO action - each line has to be an expression of type IO a, where a can be any type whatsoever (only the final line has to have the "correct" type for a - usually IO () in the case of main). And when you do x <- something, then again something has type IO a, and then x will be of type a: it's whatever you get as the result of the IO action something, which is guaranteed to be of type a since something has type IO a.

So when you do, at the start of your program:

tehfile <- readIniFile "somefile"

and we note from the documentation that readIniFile has type FilePath -> IO (Either String Ini), we see that readIniFile "somefile" has type IO (Either String Ini), and therefore tehFile has type Either String Ini.

I think you already understand that, from the fact that you then pattern match on tehfile using Left and Right, which is exactly right given its type. However, what you then go on to do doesn't really make sense, and is causing compiler errors.

Basically, if the file was successfully opened and parsed, tehFile will be a Right value holding a value of type Ini, which seems to be a representation of the data in the file. If anything went wrong, you will get a Left value, holding a String, which I assume will be an error message of some kind explaining what went wrong.

You can certainly then do:

case tehfile of
    Left e -> print $ "an error occurred: " ++ e
    ....

in order to show the user the details of anything that went wrong. But note that a case expression is exactly that, an expression, and therefore must have a type. In particular, the results of all branches - here the Left branch and the Right branch - must have the same type. print takes a value of some printable type and outputs an IO (). So the Right b branch must also output a value of type IO () - in other words it must, like print "woot", be an action which possibly performs some IO, but has no meaningful "result".

You have actually done this, with:

head (map (\x -> print x) (sections b))

which I will write more succinctly and readably (but completely equivalently), as

head $ map print (sections b)

This is perfectly type-correct: sections b is a list of Text values (which are essentially strings), print transforms each of those to a value of type IO () (with the side effect of printing the value), and head takes the first element. So the above expression has type IO (), as you need - it's an IO action which simply prints the first of the "section" values. Your comment indicates you are puzzled that only the first element was printed: well this is a consequence of Haskell's lazy evaluation. In an "eager" language, map print (sections b) would print them all, but here you only ask for the first of those values, so only the first of the sections is actually printed.

Of course, as you probably noticed, it's not type correct to simply do

Right b -> map print (sections b)

because the expression on the right has type [IO ()] and you need a single IO (). This is what the mapM_ function is for:

Right b -> mapM_ print (sections b)

will do exactly the same in terms of observable effect, but the type is now the IO () that you need. [It is a variant of mapM, which runs over a list of values with a function that makes an IO action from each of them, and, rather than returning a list of actions as map would, returns an action whose result is a list of the corresponding results of each individual action. mapM_ is identical but "throws away" the results, so rather than the result being of the form IO [a] it's simply IO (), as you need here. And note also that both these functions are more general than I've explained here, but that's how they work with lists and IO, which is a pretty common use-case for them.]

Putting it together, for this bit of code you will have:

case tehfile of
    Left e -> print $ "an error occurred: " ++ e
    Right b -> mapM_ print (sections b)

and note that I've quite deliberately left out the s <- before it, which is found in your code. It's perfectly valid to do so, but not only do you not use the s anywhere in your later code (making the <- useless as I explained above), there's absolutely no way you even could use it. The case expression has type IO (), so anything bound to the result will have type () - and () is a type with only one value. This is why we use IO () for actions like main which have no sensible result value, because the type system demands we use some type here, and using a type that essentially tells us nothing makes sense if there is nothing sensible to go there.

Right, I've gone on for ages already. Hopefully you've got something from the above. Before I sign off, I'll try to briefly do a couple of things. Firstly, answer the other questions you raise in your comments:

why selected_profile <- getLine` is different from `let selected_profile = getLine` ?

let selected_profile = getLine is just basically a local variable assignment. In this case it makes selected_profile equal to getLine, which is an action of type IO String. It doesn't actually "perform" any IO, but just allows you to use the getLine IO action under a new name - to go back to Python, it's just like doing selected_profile = input in that language; that line in itself doesn't ask the user for anything, that doesn't happen till you execute the function. getLine in Haskell isn't a function, it's an "action" (executed by the runtime, rather than in Haskell code), but the principle is the same. Whereas selected_profile <- getLine is what actually runs the getLine action, to ask the user for input, which is then assigned to the local variable selected_profile.

why this does not print "oops" on lacking profile, instead prints Left "Couldn't find section: selected_profile"

That's because, in the wider context of your code, you succeeded in opening and parsing the INI file, so tehfile holds a Right value. It seems you want to print "ooops" if the user's input for the selected profile can't be found in the file, but in order to do that, you need to run the lookUpValue function first, and pattern match on its result, to print "oops" in case of a Left (error) value.

So to round off, here is a very simple version of a main which will do, very roughly at least, as you seem to intend. Note that you will need to import pack from Data.Text in order for this to compile (this is just a conversion from String to Text, don't get me started on the ridiculous number of string types in Haskell and why the default one, String, is so terrible that no serious application uses it but still most of the standard library functions force you to):

main :: IO ()
main = do
  tehfile <- readIniFile "/Users/g.reshetniak/.aws/credentials" 

  case tehfile of
    Left e -> print $ "Failed to parse file: " ++ e
    Right i -> do
       mapM_ print (sections b)
       putStrLn "Please select which section you would like"
       selected_profile <- getLine
       case lookupValue (pack selected_profile) "aws_access_key_id" i of
           Left e -> putStrLn $ "oops, an error: " ++ e
           Right v -> print v

(Note that I haven't tried to compile or run the above, so please let me know if anything unexpected happens. I always seem to have some stupid but easily-fixed errors whenever I write more than about 4 lines of Haskell code at once...)

Sign up to request clarification or add additional context in comments.

1 Comment

Wow, I am speechless. It's like an entire lecture! Thank you so much for your effort! Brewing up some coffee and digging into this gem right now :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.