Haskell – 92 – input e output – 1

Continuo da qui, copio qui.

We’ve mentioned that Haskell is a purely functional language. Whereas in imperative languages you usually get things done by giving the computer a series of steps to execute, functional programming is more of defining what stuff is. In Haskell, a function can’t change some state, like changing the contents of a variable (when a function changes state, we say that the function has side-effects). The only thing a function can do in Haskell is give us back some result based on the parameters we gave it. If a function is called two times with the same parameters, it has to return the same result. While this may seem a bit limiting when you’re coming from an imperative world, we’ve seen that it’s actually really cool. In an imperative language, you have no guarantee that a simple function that should just crunch some numbers won’t burn down your house, kidnap your dog and scratch your car with a potato while crunching those numbers. For instance, when we were making a binary search tree, we didn’t insert an element into a tree by modifying some tree in place. Our function for inserting into a binary search tree actually returned a new tree, because it can’t change the old one.

While functions being unable to change state is good because it helps us reason about our programs, there’s one problem with that. If a function can’t change anything in the world, how is it supposed to tell us what it calculated? In order to tell us what it calculated, it has to change the state of an output device (usually the state of the screen), which then emits photons that travel to our brain and change the state of our mind, man. 😉 😋

Do not despair, all is not lost. It turns out that Haskell actually has a really clever system for dealing with functions that have side-effects that neatly separates the part of our program that is pure and the part of our program that is impure, which does all the dirty work like talking to the keyboard and the screen. With those two parts separated, we can still reason about our pure program and take advantage of all the things that purity offers, like laziness, robustness and modularity while efficiently communicating with the outside world.

Hello, world!
Up until now, we’ve always loaded our functions into GHCI to test them out and play with them. We’ve also explored the standard library functions that way. But now, after eight or so chapters, we’re finally going to write our first real Haskell program! Yay! And sure enough, we’re going to do the good old “hello, world” schtick.

Hey! For the purposes of this chapter, I’m going to assume you’re using a unix-y environment for learning Haskell. If you’re in Windows, I’d suggest you download Cygwin, which is a Linux-like environment for Windows, A.K.A. just what you need.

So, for starters, punch in the following in your favorite text editor:

main = putStrLn "hello, world"

We just defined a name called main and in it we call a function called putStrLn with the parameter "hello, world". Looks pretty much run of the mill, but it isn’t, as we’ll see in just a few moments. Save that file as helloworld.hs.

And now, we’re going to do something we’ve never done before. We’re actually going to compile our program! I’m so excited! Open up your terminal and navigate to the directory where helloworld.hs is located and do the following:

Okay! With any luck, you got something like this and now you can run your program by doing ./helloworld.

And there we go, our first compiled program that printed out something to the terminal. How extraordinarily boring!

Let’s examine what we wrote. First, let’s look at the type of the function putStrLn.

Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> :t putStrLn "hello, world"
putStrLn "hello, world" :: IO ()

We can read the type of putStrLn like this: putStrLn takes a string and returns an I/O action that has a result type of () (i.e. the empty tuple, also know as unit). An I/O action is something that, when performed, will carry out an action with a side-effect (that’s usually either reading from the input or printing stuff to the screen) and will also contain some kind of return value inside it. Printing a string to the terminal doesn’t really have any kind of meaningful return value, so a dummy value of () is used.

The empty tuple is a value of () and it also has a type of ().

So, when will an I/O action be performed? Well, this is where main comes in. An I/O action will be performed when we give it a name of main and then run our program.

Having your whole program be just one I/O action seems kind of limiting. That’s why we can use do syntax to glue together several I/O actions into one. Take a look at the following example:

main = do
  putStrLn "Hello, what's your name?"
  name <- getLine
  putStrLn ("Hey " ++ name ++ ", you rock!")

Ah, interesting, new syntax! And this reads pretty much like an imperative program. If you compile it and try it out, it will probably behave just like you expect it to. Notice that we said do and then we laid out a series of steps, like we would in an imperative program. Each of these steps is an I/O action. By putting them together with do syntax, we glued them into one I/O action. The action that we got has a type of IO (), because that’s the type of the last I/O action inside.

Because of that, main always has a type signature of main :: IO something, where something is some concrete type. By convention, we don’t usually specify a type declaration for main.

An interesting thing that we haven’t met before is the third line, which states name <- getLine. It looks like it reads a line from the input and stores it into a variable called name. Does it really? Well, let’s examine the type of getLine.

Prelude> :t getLine
getLine :: IO String

Aha, o-kay. getLine is an I/O action that contains a result type of String. That makes sense, because it will wait for the user to input something at the terminal and then that something will be represented as a string. So what’s up with name <- getLine then? You can read that piece of code like this: perform the I/O action getLine and then bind its result value to name. getLine has a type of IO String, so name will have a type of String. You can think of an I/O action as a box with little feet that will go out into the real world and do something there (like write some graffiti on a wall) and maybe bring back some data. Once it’s fetched that data for you, the only way to open the box and get the data inside it is to use the <- construct. And if we’re taking data out of an I/O action, we can only take it out when we’re inside another I/O action. This is how Haskell manages to neatly separate the pure and impure parts of our code. getLine is in a sense impure because its result value is not guaranteed to be the same when performed twice. That’s why it’s sort of tainted with the IO type constructor and we can only get that data out in I/O code. And because I/O code is tainted too, any computation that depends on tainted I/O data will have a tainted result.

When I say tainted, I don’t mean tainted in such a way that we can never use the result contained in an I/O action ever again in pure code. No, we temporarily un-taint the data inside an I/O action when we bind it to a name. When we do name <- getLine, name is just a normal string, because it represents what’s inside the box. We can have a really complicated function that, say, takes your name (a normal string) as a parameter and tells you your fortune and your whole life’s future based on your name. We can do this:

main = do
    putStrLn "Hello, what's your name?"
    name <- getLine
    putStrLn $ "Read this carefully, because this is your future: " ++ tellFortune name

and tellFortune (or any of the functions it passes name to) doesn’t have to know anything about I/O, it’s just a normal String -> String function!

Take a look at this piece of code. Is it valid?

nameTag = "Hello, my name is " ++ getLine

If you said no, go eat a cookie. If you said yes, drink a bowl of molten lava. Just kidding, don’t! The reason that this doesn’t work is that ++ requires both its parameters to be lists over the same type. The left parameter has a type of String (or [Char] if you will), whilst getLine has a type of IO String. You can’t concatenate a string and an I/O action. We first have to get the result out of the I/O action to get a value of type String and the only way to do that is to say something like name <- getLine inside some other I/O action. If we want to deal with impure data, we have to do it in an impure environment. So the taint of impurity spreads around much like the undead scourge and it’s in our best interest to keep the I/O parts of our code as small as possible.

Every I/O action that gets performed has a result encapsulated within it. That’s why our previous example program could also have been written like this:

main = do
  foo <- putStrLn "Hello, what's your name?"
  name <- getLine
  putStrLn ("Hey " ++ name ++ ", you rock!")

However, foo would just have a value of (), so doing that would be kind of moot. Notice that we didn’t bind the last putStrLn to anything. That’s because in a do block, the last action cannot be bound to a name like the first two were. We’ll see exactly why that is so a bit later when we venture off into the world of monads. For now, you can think of it in the way that the do block automatically extracts the value from the last action and binds it to its own result.

Except for the last line, every line in a do block that doesn’t bind can also be written with a bind. So putStrLn "BLAH" can be written as _ <- putStrLn "BLAH". But that’s useless, so we leave out the <- for I/O actions that don’t contain an important result, like putStrLn something.

Beginners sometimes think that doing name = getLine will read from the input and then bind the value of that to name. Well, it won’t, all this does is give the getLine I/O action a different name called, well, name. Remember, to get the value out of an I/O action, you have to perform it inside another I/O action by binding it to a name with <-.

I/O actions will only be performed when they are given a name of main or when they’re inside a bigger I/O action that we composed with a do block. We can also use a do block to glue together a few I/O actions and then we can use that I/O action in another do block and so on. Either way, they’ll be performed only if they eventually fall into main.

Oh, right, there’s also one more case when I/O actions will be performed. When we type out an I/O action in GHCI and press return, it will be performed.

Prelude> putStrLn "HEEY"
HEEY

Even when we just punch out a number or call a function in GHCI and press return, it will evaluate it (as much as it needs) and then call show on it and then it will print that string to the terminal using putStrLn implicitly.

Remember let bindings? If you don’t, refresh your memory on them by reading this section. They have to be in the form of let bindings in expression, where bindings are names to be given to expressions and expression is the expression that is to be evaluated that sees them. We also said that in list comprehensions, the in part isn’t needed. Well, you can use them in do blocks pretty much like you use them in list comprehensions. Check this out:

import Data.Char

main = do
    putStrLn "What's your first name?"
    firstName <- getLine
    putStrLn "What's your last name?"
    lastName <- getLine
    let bigFirstName = map toUpper firstName
        bigLastName = map toUpper lastName
    putStrLn $ "hey " ++ bigFirstName ++ " " ++ bigLastName ++ ", how are you?"

See how the I/O actions in the do block are lined up? Also notice how the let is lined up with the I/O actions and the names of the let are lined up with each other? That’s good practice, because indentation is important in Haskell. Now, we did map toUpper firstName, which turns something like “John” into a much cooler string like “JOHN”. We bound that uppercased string to a name and then used it in a string later on that we printed to the terminal.

You may be wondering when to use <- and when to use let bindings? Well, remember, <- is (for now) for performing I/O actions and binding their results to names. map toUpper firstName, however, isn’t an I/O action. It’s a pure expression in Haskell. So use <- when you want to bind results of I/O actions to names and you can use let bindings to bind pure expressions to names. Had we done something like let firstName = getLine, we would have just called the getLine I/O action a different name and we’d still have to run it through a <- to perform it.

Now we’re going to make a program that continuously reads a line and prints out the same line with the words reversed. The program’s execution will stop when we input a blank line. This is the program:

pres.hs

main = do
  line <- getLine 
  if null line then return () 
  else do 
    putStrLn $ reverseWords line 
    main 

reverseWords :: String -> String
reverseWords = unwords . map reverse . words
Prelude> :l pres
[1 of 1] Compiling Main             ( pres.hs, interpreted )
Ok, modules loaded: Main.
*Main> main
abc
cba
Juhan
nahuJ

ovviamente avrei potuto compilarlo.

To get a feel of what it does, you can run it before we go over the code. Aehmmm… già fatto.

Protip: To run a program you can either compile it and then run the produced executable file by doing ghc --make helloworld and then ./helloworld or you can use the runhaskell command like so: runhaskell helloworld.hs and your program will be executed on the fly. Uh! runhaskell, devo ricordarmelo:

First, let’s take a look at the reverseWords function. It’s just a normal function that takes a string like “hey there man” and then calls words with it to produce a list of words like ["hey","there","man"]. Then we map reverse on the list, getting [“yeh”,”ereht”,”nam”] and then we put that back into one string by using unwords and the final result is “yeh ereht nam”. See how we used function composition here. Without function composition, we’d have to write something like reverseWords st = unwords (map reverse (words st)).

What about main? First, we get a line from the terminal by performing getLine call that line line. And now, we have a conditional expression. Remember that in Haskell, every if must have a corresponding else because every expression has to have some sort of value. We make the if so that when a condition is true (in our case, the line that we entered is blank), we perform one I/O action and when it isn’t, the I/O action under the else is performed. That’s why in an I/O do block, ifs have to have a form of if condition then I/O action else I/O action.

Let’s first take a look at what happens under the else clause. Because, we have to have exactly one I/O action after the else, we use a do block to glue together two I/O actions into one. You could also write that part out as:

else (do
  putStrLn $ reverseWords line
  main)

This makes it more explicit that the do block can be viewed as one I/O action, but it’s uglier. Anyway, inside the do block, we call reverseWords on the line that we got from getLine and then print that out to the terminal. After that, we just perform main. It’s called recursively and that’s okay, because main is itself an I/O action. So in a sense, we go back to the start of the program.

Now what happens when null line holds true? What’s after the then is performed in that case. If we look up, we’ll see that it says then return (). If you’ve done imperative languages like C, Java or Python, you’re probably thinking that you know what this return does and chances are you’ve already skipped this really long paragraph. Well, here’s the thing: the return in Haskell is really nothing like the return in most other languages! It has the same name, which confuses a lot of people, but in reality it’s quite different. In imperative languages, return usually ends the execution of a method or subroutine and makes it report some sort of value to whoever called it. In Haskell (in I/O actions specifically), it makes an I/O action out of a pure value. If you think about the box analogy from before, it takes a value and wraps it up in a box. The resulting I/O action doesn’t actually do anything, it just has that value encapsulated as its result. So in an I/O context, return "haha" will have a type of IO String. What’s the point of just transforming a pure value into an I/O action that doesn’t do anything? Why taint our program with IO more than it has to be? Well, we needed some I/O action to carry out in the case of an empty input line. That’s why we just made a bogus I/O action that doesn’t do anything by writing return ().

Using return doesn’t cause the I/O do block to end in execution or anything like that. For instance, this program will quite happily carry out all the way to the last line:

main = do
  return ()
  return "HAHAHA"
  line <- getLine
  return "BLAH BLAH BLAH"
  return 4
  putStrLn line

All these returns do is that they make I/O actions that don’t really do anything except have an encapsulated result and that result is thrown away because it isn’t bound to a name. We can use return in combination with <- to bind stuff to names.

main = do
  a <- return "hell"
  b <- return "yeah!"
  putStrLn $ a ++ " " ++ b

So you see, return is sort of the opposite to <-. While return takes a value and wraps it up in a box, <- takes a box (and performs it) and takes the value out of it, binding it to a name. But doing this is kind of redundant, especially since you can use let bindings in do blocks to bind to names, like so:

main = do
  let a = "hell"
      b = "yeah"
  putStrLn $ a ++ " " ++ b

When dealing with I/O do blocks, we mostly use return either because we need to create an I/O action that doesn’t do anything or because we don’t want the I/O action that’s made up from a do block to have the result value of its last action, but we want it to have a different result value, so we use return to make an I/O action that always has our desired result contained and we put it at the end.

A do block can also have just one I/O action. In that case, it’s the same as just writing the I/O action. Some people would prefer writing then do return () in this case because the else also has a do.

 Before we move on to files, let’s take a look at some functions that are useful when dealing with I/O.  NO! il post diventerebbe troppo lungo! Pausa 😋

🤩

Annunci
Post a comment or leave a trackback: Trackback URL.

Trackbacks

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...

%d blogger hanno fatto clic su Mi Piace per questo: