问题
In Haskell we have a function readFile :: FilePath -> IO String
. My question while understanding monad is why wrap it in IO
? Couldn't we just have written function like these:
(lines.readFile) path
Rather than
(readFile >>= lines) path
What benefit does the IO wrapper provide?
回答1:
Haskell expressions are referentially transparent. This means that if readFile
would really have a type of FilePath -> String
, then expression readFile "a.txt"
would always yield the same result. Even if you read the file, then change it, and then read again, you will get the contents in its first state.
Thus, we need to distingush between values and actions, and this is what IO
is for. It doesn't let you use the result readFile "a.exe"
in other expressions until you perform an action associated with it. As a consequence, after changing your file you have to perform the reading action again, to get file contents, and because of that you will able to see the changes.
回答2:
We write functions that create computer programs
It should be noted that Haskell is a functional programming language. Functions, in the mathematical sense, always produce the same values for the same inputs.
Now this requirement to always produce the same result constrains things quite a bit, since a function to read a file would somehow have to produce the same result every time, even if the file was later changed. That's obviously not what we really want.
There is, however, a way to make a functional programming language that can handle reading a changing file. What you do is to write a function that produces some action the computer should perform. So you might perform an action composed of the following steps:
Read the file
Break it into lines
Change the even-numbered lines to uppercase
Output the lines to the screen
These four actions aren't performed yet. They're just a sequence of actions that we might perform. A function can return that exact same sequence of potential actions every time it's called, which makes it a proper mathematical function.
The main :: IO a
function in Haskell returns the action that the program should perform. It always returns the same action, making it a proper mathematical function. When the program is run, the computer evaluates the main
function, producing the action the computer should perform, and the computer then executes the action.
Do notation
Do notation takes the strangeness out of the process, giving you the feel of a much more standard programming language. You have three options:
- Perform an action and do nothing with its results
- Perform an action and store its results
- Process data using only functions (no actions)
These are done in the following ways, respectively:
action args
result <- action args
let result = f . g . h . whateverCalculation $ value
This is similar to an imperative language like C where you do, respectively:
action(args);
result = action(args);
result = f(g(h(whateverCalculation(value))));
回答3:
For (lines.readFile) path
to work, the type of readFile
would need to be FilePath -> String
. That, however, doesn't make sense in Haskell. A Haskell function is supposed to always produce the same results when given the same arguments. If the result type readFile
was String
, however, that would not happen, as readFile "foo.txt"
would have to, for any useful implementation of such a readFile
, produce different strings depending on the contents of the foo.txt file.
The Haskell solution to this issue is giving readFile
the type FilePath -> IO String
. An IO String
is not a string, but a program that can be executed by the computer and that, when executed, somehow materialises a String
into memory. While the String
thus produced might be different each time the program is executed, the program itself remains the same, and therefore readFile
always returns the same results when given the same arguments (and so, for instance, readFile "foo.txt"
is always the same program).
This trick of manipulating a program that produces an I/O-dependent result instead of the result itself only works if the I/O-dependent result is kept opaque; that is, if there is no way of directly extracting it. In other words, there cannot be, for instance, an IO String -> String
function -- for one, it would allow us to implement a readFile
with the inappropriate type FilePath -> String
that we have discussed above. There are, however, indirect ways of using the I/O-dependent result that do not lead to trouble. One of them is using it to create a second program, whose I/O-dependent result is just as opaque as the first one was. The Monad
interface allows us to express this usage pattern:
(>>=) :: Monad m => m a -> (a -> m b) -> m b
Specialising (>>=)
to IO
, we get:
(>>=) @IO :: IO a -> (a -> IO b) -> IO b
The first program has type IO a
, and the function that produces the second program using the I/O-dependent result of the first one has type a -> IO b
. The result of (>>=)
is a program which executes the first program and the second, newly generated, one in sequence. For instance...
readFile "foo.txt" >>= putStrLn
... is a program which reads the contents of foo.txt and then displays these contents.
P.S.: With respect to your example involving lines
, it is worth noting that both (readFile >>= lines) path
, as you have written it, and (\p -> readFile p >>= lines) path
are rejected by the type checker. Something that does work is:
(fmap lines . readFile) path
In it, we are making indirect use of the file contents in a different way. If we have a program which produces an I/O-dependent result, we can turn it into a program which produces a modified version of this result. That is done through fmap
, from the Functor
class:
fmap :: Functor f => (a -> b) -> f a -> f b
Or, specialising to IO
:
fmap @IO :: (a -> b) -> IO a -> IO b
来源:https://stackoverflow.com/questions/41096040/why-wrap-an-io-result-in-io-monad