Returning values in Haskell IO() main function 在Haskell中IO()主函数中返回值

huangapple go评论72阅读模式
英文:

Returning values in Haskell IO() main function

问题

以下是翻译好的内容:

拥有以下形式的脚本:

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 ()

我收到类似于““No instance for (Num (IO ())) arising from the literal ‘42’””的错误信息。这看起来很晦涩,但通过将值42更改为不同类型,我已经确定了函数main的类型 - IO()与类型42,Num不匹配。

我的问题是,为什么尽管将模式匹配的结果存储在变量z中,并明确返回了()而不是z,我仍然收到类型错误?在Scala中,如果没有return (),通常会返回x。对我来说,Haskell的这种行为真的很令人费解,因为我只能将其解释为main:IO()中的所有变量都需要是类型IO()。我理解得对吗?

英文:

Having a script that looks like this:

main :: IO ()
main = do
  tehfile &lt;- readIniFile &quot;somefile&quot;

  z &lt;- case tehfile of
    Left a -&gt; 42
    Right b -&gt; print &quot;bar&quot;

  x &lt;- case tehfile of
    Left a -&gt; print &quot;foo&quot;
    Right b -&gt; print &quot;bar&quot;
  
  return ()

I get errors like &quot;No instance for (Num (IO ())) arising from the literal ‘42’&quot;.
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?

答案1

得分: 4

这是您要翻译的部分:

这个答案在很大程度上基于您的评论,我会引用以下部分以防丢失。(对于这么长的答案,我深感抱歉,我真的没有打算写得这么长,但我觉得有很多东西我觉得有必要说。希望这对你有帮助,但如果不是的话,请不要犹豫告诉我。)

以下是您提供的代码:

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 ()

以下是翻译的文本:

这个答案在很大程度上基于您的评论,我会引用以下部分以防丢失。

以下是您提供的代码:

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 ()

希望这对您有所帮助。如果您需要更多的翻译或有其他问题,请随时告诉我。

英文:

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 &lt;- readIniFile &quot;/Users/g.reshetniak/.aws/credentials&quot; -- ~/.aws/credentials: openFile: does not exist --&gt; why shell expansion doesn&#39;t work?

  s &lt;- case tehfile of
    Left a -&gt; print &quot;woot&quot;
    Right b -&gt; head (map (\x -&gt; print x) (sections b)) --TODO why this prints one element instead of mapping print over all section
    
  --TODO selected_profile &lt;- getLine
  --TODO why `selected_profile &lt;- getLine` is different from `let selected_profile = getLine` ?

  p &lt;- case tehfile of
    Left c -&gt; print &quot;oops&quot; --TODO why this does not print &quot;oops&quot; on lacking profile, instead prints Left &quot;Couldn&#39;t find section: selected_profile&quot;
    Right d -&gt; print (lookupValue &quot;selected_profile&quot; &quot;aws_access_key_id&quot; 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 &lt;- operator, but not then making use of them. Doing s &lt;- 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 &lt;- 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 &quot;Please tell me your name&quot;
    name &lt;- getLine
    putStrLn $ &quot;Welcome, &quot; ++ name ++ &quot;!

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 &lt;- 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 &lt;- "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 &quot;Please tell me your name&quot;
    getLine
    putStrLn &quot;Welcome!&quot;

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 &lt;- 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 &lt;- readIniFile &quot;somefile&quot;

and we note from the documentation that readIniFile has type FilePath -&gt; IO (Either String Ini), we see that readIniFile &quot;somefile&quot; 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 -&gt; print $ &quot;an error occurred: &quot; ++ 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 &quot;woot&quot;, be an action which possibly performs some IO, but has no meaningful "result".

You have actually done this, with:

head (map (\x -&gt; 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 -&gt; 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 -&gt; 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 -&gt; print $ &quot;an error occurred: &quot; ++ e
    Right b -&gt; mapM_ print (sections b)

and note that I've quite deliberately left out the s &lt;- 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 &lt;- 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 &lt;- 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 &lt;- 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 &quot;oops&quot; on lacking profile, instead prints Left &quot;Couldn&#39;t find section: selected_profile&quot;

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 &lt;- readIniFile &quot;/Users/g.reshetniak/.aws/credentials&quot; 

  case tehfile of
    Left e -&gt; print $ &quot;Failed to parse file: &quot; ++ e
    Right i -&gt; do
       mapM_ print (sections b)
       putStrLn &quot;Please select which section you would like&quot;
       selected_profile &lt;- getLine
       case lookupValue (pack selected_profile) &quot;aws_access_key_id&quot; i of
           Left e -&gt; putStrLn $ &quot;oops, an error: &quot; ++ e
           Right v -&gt; 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...)

huangapple
  • 本文由 发表于 2020年1月3日 21:31:06
  • 转载请务必保留本文链接:https://go.coder-hub.com/59579481.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定