Most likely, this is the last part, published just in time. My vacation is almost over, and now it will be very difficult to write on the article per week. Thanks to everyone who was interested in the Haskell Quest Tutorial!Living room
You are in the living room. There is a doorway to the west, it is a trophy case, and a large oriental rug in the middle of the room.
Above the trophy case hangs an elvish sword of great antiquity.
A battery-powered brass lantern is on the trophy case.
Content:
GreetingPart 1 - The ThresholdPart 2 - ForestPart 3 - PolyanaPart 4 - View of the canyonPart 5 - Hall')
Part 5,
in which we derive significant consequences from a small error, and then add objects to the game.
First, let's refresh our memory. Let's run through the run function, which we got in the fourth part. As you can see, I worked for you, added the Look action. The run function now looks impressive:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Look -> do
putStrLn ( describeLocation curLoc )
run curLoc
Go dir -> do
putStrLn ( " \ n You walking to" ++ show dir ++ ". \ n " )
run ( walk curLoc dir )
convertResult -> do
putStrLn ( evalAction convertResult )
putStrLn “End of turn. \ n "
run curLoc
In assignment number 2 to the last part there was a proposal to think about how to avoid two calls "(describeLocation curLoc)". We, of course, do not solve the gravitational problem for N bodies in order to fight for resources using the example of one small function, but several important consequences follow from this insignificance. If we had an imperative language, we would simply assign the result to a variable. In Haskell, data is considered immutable, so the assignment itself is not. But there are other mechanisms that, as it turns out, not only generalize the assignment, but also, taking into account the immutable state, produce interesting effects. For example, determinism of execution. In fact, if a global state does not affect a function, and inside it, the data is obviously immutable, it means that it will return the same arguments to the same arguments. By calling a function once, we can memorize its parameters and the result obtained — and if we need to call it a second time with the same parameters, we simply return the result calculated earlier, because we are sure of it.
But let's get down to business. There are several solutions to our problem. First, let's try, for example, to associate the result with some variable, in the same way as we connected the string from getLine and the variable x. This is like an assignment, and the idea here is simple: you can use the variable as much as you like. From the first call you get approximately the following code:
run curLoc = do
locDescr <- describeLocation curLoc
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
... - remaining code
But this code is a trick because it is no longer compiled.
* Main > : r
[ 2 of 2 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 58 : 17 :
Couldn't match expected type ' [ a0 ] ' with actual type ' IO ( ) '
In the return type of a call of ' putStrLn '
In a stmt of a ' do ' expression: putStrLn locDescr
In the expression:
...
Failed , modules loaded: Types .
What is the difference between "x <- getLine" and "locDescr <- describeLocation curLoc"? It seems to be the same: the getLine function returns a string that is associated with the variable x; and the function (describeLocation curLoc) also returns a string that is associated with another locDescr variable. But the interpreter says that some types do not match. Let's understand the situation - and for this you need to understand what is actually happening in the run function.
Returning to the working code, commenting out the erroneous lines.
run curLoc = do
- locDescr <- describeLocation curLoc
- putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
... - remaining code
* Types > : r
[ 2 of 2 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )
Ok , modules loaded: Main , Types .
* Main >
We intentionally did not write the definition of the run function in order not to go into the subtleties of its type. But she has the type, and the compiler dutifully outputs it himself. Check:
* Main > : t run
run :: Location -> IO ( )
And here we are waiting for a surprise. It is clear that Location is a type of a single parameter, and for a strange type of IO () there is nothing left but to be a type of return value. Very interesting! After all, we do not use any IO () anywhere, where did it come from? If you have already set out to blame Haskell for its dark deeds and self-righteousness, then do not rush: I very much use it. The fact is that, according to Haskell, any I / O action (Input-Output, I / O) is potential errors and failures, also known as “side effects”. Consequently, input / output functions (putStrLn, putStr, putChar, getLine, getChar, readFile, writeFile) are dangerous, they can return different results for the same arguments, and in some cases can even throw an error. Therefore, they have type IO, which, in the opinion of the language, should alarm us. In addition, all other functions in which the type IO arises inside also become non-deterministic and are required to adopt it. Our run function chose unsafe actions, which caused IO () to get infected. From here it follows, by the way, that the entry point to the program - the main function - did not avoid this fate, since run is called in it.
* Main > : t main
main :: IO ( )
It is fair to say that in the explanations it was assumed that functions with IO-actions are non-deterministic. In fact this is not true; and such functions in a sense can be deterministic and even pure. This question, generally speaking, is a stumbling block in the debate about the purity of Haskell, and its discussion can be found on the Internet.
How does this relate to our problem? Let's look again (without compilation) on the wrong code, assigning to it, for order, the definition of the run function.
run :: Location -> IO ( )
run curLoc = do
locDescr <- describeLocation curLoc
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
... - remaining code
The keyword do, as we know, links the actions in a chain, but not any ones. The do-notation requires that all actions in a chain return the same type. Since we are working with I / O, the type of the functions must contain IO.
* Main > : t putStrLn
putStrLn :: String -> IO ( )
* Main > : t putStr
putStr :: String -> IO ( )
In addition to the String argument, these two functions have the same type of return value - IO (). Empty brackets here show that nothing else is returned to the useful function. In fact, it doesn't matter to us what putStrLn returns there, so long as it prints its argument to the console. On the contrary, we expect something “useful” from the getLine function, and this is reflected in its own way in its type:
* Main > : t getLine
getLine :: IO String
getLine receives a string from the user and packs it, as a gift, into an IO type. And do with a gift what you want. We, for example, pass it to the convertStringToAction function:
...
x <- getLine
case ( convertStringToAction x ) of
...
But the convertStringToAction function has a different type! - exclaim you and you will be right. She is waiting for a String at the input, not an IO String! What is the focus? Well, somewhere in these two lines, the gift unfolds, the IO box is thrown away, and the blank line already goes further. And this is the arrow. It takes the packed result from the right side and unpacks it into the variable on the left. Therefore, by the way, this operation is called binding, and not assignment. And if you think about it, our expression “locDescr <- describeLocation curLoc” is erroneous, because in the right side nothing is packed anywhere - read, the result is not put into the IO box.
* Main > : t ( describeLocation Home )
( describeLocation Home ) :: String
Well, we arrived ... They wanted to be as simple as that, but they got what kind of effect even a few pages. But I have good news for you: there is a solution! We ourselves will pack what is right on the IO type, and let the arrow choke. For this there is a function return. The following code will work as intended:
run :: Location -> IO ( )
run curLoc = do
locDescr <- return ( describeLocation curLoc )
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Look -> do
putStrLn locDescr
run curLoc
... - remaining code
Yes, the return function will pack the string into the do (IO) block type, and the arrow will unpack it and bind it to the locDescr variable. And no longer need to calculate the location description several times. The logic, of course, is unusual, but you must agree: there is some inner beauty in it. It's worth the drink!
Behind the do-notation and type of IO, there are even more consequences and pitfalls. If we drop deeper, we will see a universal way of taming not only side effects, but also many other calculations, which can be related to each other by different laws than the actions of IO. Making calculations using the so-called monads , we end up with a surprisingly simple and convenient logic, and the code becomes concise, understandable and expressive. It is important that such code is strictly mathematical and very convenient. It is very suitable for describing specific tasks such as parsing, DSL, mutable states, and more. Maybe someday we will return to this topic - but not before it really is needed.
Well, we solved the puzzle, which turned out to be unexpectedly capacious. So why do we need to strain and come up with some other solutions? Yes, in general, for the same: to learn something else about Haskell.
Our next solution will use the where construct. This is a special construction where you can describe functions that are available only here and now, like nested functions in some other programming languages. Within the where-block, variables of the parent function are available, and all laws that apply to normal functions apply. This means that you can create several variants of the same function, use pattern matching, security expressions, and even create nested where constructs.
The where-construct does not require any special explanation, just look at the example:
run :: Location -> IO ( )
run curLoc = do
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Look -> do
putStrLn locDescr
run curLoc
... - remaining code
where
locDescr = describeLocation curLoc
We made the same indent before the where-construct as the do block. In essence, we defined where for this block. It may seem that we assign the result to the variable locDescr, but this is not the case. There are no variables and assignments; locDescr is a function that returns a location description. We call this function from the parent code, and two times; however, it is most likely calculated only once. Haskell compilers can save past results for reuse. When data is no longer needed, it is deleted by the built-in garbage collector. Remember the frame from the last part, where the Fibonacci numbers are calculated. How would the program work if at each step of the recursion the values ​​were calculated from the very beginning?
Finally, the last solution - the so-called let-expressions - is the simplest and, perhaps, the most suitable here. Although the let-expressions are very similar to where, they define expression aliases, and not functions with such names. Inside the do block, a let-expression looks like this:
run :: Location -> IO ( )
run curLoc = do
let locDescr = describeLocation curLoc
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Look -> do
putStrLn locDescr
run curLoc
... - remaining code
If we use a let-expression outside the do block, —for example, within other expressions — the record will change a little. The in keyword is added:
run :: Location -> IO ( )
run curLoc =
let locDescr = describeLocation curLoc in
do
putStrLn locDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Look -> do
putStrLn locDescr
run curLoc
... - remaining code
In fact, you can define as many such expressions as you want between in and in, you will see how easy it is when we move on to adding objects. For now we will leave the previous option where the let-expression is inside the do-block.
Let-expressions are good not only because they are shorter, but also because they can be used in some special cases. So, for example, let can be inserted into list generators (list comprehensions) for the same purpose: to define an expression with a limited scope. However, in comparison with where there are drawbacks: you cannot use security expressions and pattern matching. Of course, since we set pseudonyms, they should not be repeated within their scope.
You ask when we go to the objects? I have already passed. Are you not yet, or what? Then catch up. Create an ADT type for objects, like this:
data Object = Table
| Umbrella
| Drawer
| Phone
| Mailbox
| Friend'sKey
deriving ( Eq , Show , Read )
In the image of locations, add a function with a description of the objects:
describeObject :: Object -> String
describeObject Umbrella = "Nice red mechanic Umbrella."
describeObject Table = "Good wooden table with drawer."
describeObject Phone = "The Phone has some voice messages for you."
describeObject MailBox = "The MailBox is closed."
describeObject obj = "There is nothing special about" ++ show obj
There are no dirty tricks, all that we already know and can do. New starts when we need to define objects for locations. We would like, of course, that everything was ready, Take and Drop actions worked, there was an inventory, composite objects ... But this does not happen. Now we will make a draft of the objects in the locations where they can neither be taken nor thrown, and on the basis of it we will come up with other, more advanced and suitable mechanisms.
Suppose, in the Home location where the game begins, there are several objects, and among them are a table, a drawer of a table, a telephone and an umbrella. Let's set the locationObjects function, which would return the list of objects for this location:
locationObjects :: Location -> [ Object ]
locationObjects Home = [ Umbrella , Drawer , Phone , Table ]
locationObjects _ = [ ]
In general, lists look simple. The list of objects is of type [Object]. For the Home location, we return a list of four objects. An empty list is indicated by empty square brackets - let it be while all other locations are without objects. It is worth testing the code before moving on.
* Main > locationObjects Home
[ Umbrella , Drawer , Phome , Table ]
* Main > locationObjects Garden
[ ]
* Main > describeObject Table
“Good wooden table with drawer.”
Since the Object type is inherited from the Show type class, we can use the show function not only for type constructors, but also for a list of this type:
* Main > show Umbrella
"Umbrella"
* Main > show [ Umbrella , Table ]
"[Umbrella, Table]"
* Main > show ( locationObjects Home )
"[Umbrella, Drawer, Phone, Table]"
* Main > putStrLn ( show ( locationObjects Home ) )
[ Umbrella , Drawer , Phone , Table ]
The reverse is also true. If the elements of the list can be parsed using the read function, then the entire list can also be parsed:
* Main > read "[Table, MailBox]" :: [ Object ]
[ Table , MailBox ]
Now, when we have dealt with the syntax, we will make it so that for the current location not only its description is displayed, but also a list of the objects that are there is printed. We use let-expressions:
run curLoc = do
let locDescr = describeLocation curLoc
let objectsDescr = " \ n There are some objects here:" ++ show ( locationObjects curLoc )
let fullDescr = locDescr ++ objectsDescr
putStrLn fullDescr
... - the rest of the code, do not forget to update the Look action.
The output of the program now looks like this:
* Main > main
Quest adventure on Haskell .
Home
You are standing in the middle of the wooden table .
There are some objects here: [ Umbrella , Drawer , Phone , Table ]
Enter command:
The code has a little trouble. Even if there are no objects in the location, the inscription “There are some objects here:” will still make us an eyesore.
Enter command: Go North
You walking to North .
Garden
You are in the garden . .....
There are some objects here: [ ]
Enter command:
In order not to display it for an empty list of objects, we will move all the code describing objects into a separate function, let's call it enumerateObjects. At the input, it takes a list, and at the output it passes a string with the listed objects.
enumerateObjects :: [ Object ] -> String
enumerateObjects [ ] = ""
enumerateObjects objects = " \ n There are some objects here:" ++ show objects
The first variant of the enumerateObjects function returns an empty string, but only when the list of transferred objects is empty. If it is not empty, the first option will be rejected, and the second will work. Run function:
run curLoc = do
let locDescr = describeLocation curLoc
let objectsDescr = enumerateObjects ( locationObjects curLoc )
let fullDescr = locDescr ++ objectsDescr
putStrLn fullDescr
... - ...
Or, if you want, you can take out the let-expressions from the do block:
run curLoc =
let
locDescr = describeLocation curLoc
objectsDescr = enumerateObjects ( locationObjects curLoc )
fullDescr = locDescr ++ objectsDescr
in do
putStrLn fullDescr
... - ...
Thanks to this simple refactoring, we can now change the function enumerateObjects, without affecting the run. Here we wanted, say, not only to list the objects, but also to describe them, so we climbed our playful pens into enumerateObjects and corrected what was clever there. But somehow later. Now add a custom Investigate action. At this command, the program should produce a detailed description of the object. It is clear that the user must enter something like this:
Enter command: Investigate Umbrella
The string "Investigate Umbrella" will have to be parsed. Familiar, right? We have already done this with the Go team. Here is the same thing: just put an Object type parameter into the Investigate constructor.
data Action =
...
| Investigate Object
...
Only a little remains - to correct the run function:
run curLoc = do
...
...
case ( convertStringToAction x ) of
Investigate obj -> do
putStrLn ( describeObject obj )
run curLoc
...
And voila! You have implemented the next user action! Congratulations! You can check that if you enter the "Investigate Umbrella" command, the program will display the line "Nice red mechanic Umbrella.", I do not cheat.
Just ... Do you know what is wrong with us? If we, while in the Home location, enter the “Investigate MailBox” command, we will be given a description of the mailbox, which cannot be seen from here at all! Well, then we will invent the function isVisible, which returns True if we see an object - and only in this case we will give a description of the object. What are the arguments of the isVisible function? We need an object under study, as well as a list of all objects in a location. Something like this:
isVisible :: Object -> [ Object ] -> Bool
Now you need to somehow find out if there is an object in this list. We will not come up with "stupid" options with the enumeration of all objects in the list, although this is very possible, but simply use the regular function elem. It takes two parameters: an element and a list. If the item is found in the list, returns True. Exactly what is needed!
isVisible :: Object -> [ Object ] -> Bool
isVisible obj objects = elem obj objects
In Haskell, there is a special form of writing in which a function is placed between its arguments, for greater clarity and clarity of meaning. To do this, you need to enclose the function in reverse apostrophes, - those that are located on the "E" button.
isVisible ' :: Object -> [ Object ] -> Bool
isVisible 'obj objects = obj ` elem` objects
But the "stupid" versions of the same function. In them, we manually search for an object inside the list.
isVisible '' :: Object -> [ Object ] -> Bool
isVisible '' obj [ ] = False
isVisible '' obj ( o: os ) = ( obj == o ) || ( isVisible '' obj os )
isVisible '' ' :: Object -> [ Object ] -> Bool
isVisible '' 'obj [ ] = False
isVisible '' 'obj objects = ( obj == head objects ) || ( isVisible '' 'obj ( tail objects ) )
They work the same way, the only difference is in the means used. Both there and there the list is split into parts: the head element and the tail of the remaining elements. In the first example, the list is split using an entry (o: os). It is clear that the variable "o" contains the head, and the variable "os" - everything else. In the second example, we do the same thing, only using the built-in functions on the head and tail lists. Next, we simply check if the object matches the head element, and if not, call isVisible recursively for the remaining elements. In order for the recursion not to be infinite, we added a variant of the function “isVisible obj [] = False” - it will work if we suddenly bite off all the elements from the list and nothing remains.
Well, it remains only to use this feature. As usual, we change run, and in order not to request objects of the current location several times, we put them into a let-expression:
run curLoc = do
let locObjects = locationObjects curLoc
let locDescr = describeLocation curLoc
let objectsDescr = enumerateObjects locObjects
let fullDescr = locDescr ++ objectsDescr
putStrLn fullDescr
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Investigate obj -> do
if ( isVisible obj locObjects )
then putStrLn ( describeObject obj )
else putStrLn ( “You don't see any” ++ show obj ++ "here." )
run curLoc
Quit -> putStrLn "Be seen you ..."
... - the rest of the code
That's all. On this positive note, we will stop, - it's time to relax, and so we got a huge cart of knowledge.
Tasks for fixing.
1. Move all functions related to objects to the Objects module, and all functions related to locations to the Locations module.
2. To make an experimental conclusion of location objects in the following form:
Home
You are standing in the middle of the wooden table.
There are some objects here:
Umbrella: Nice red mechanic Umbrella.
Table: Good wooden table with drawer.
Phone: The Phone has some voice messages for you.
Drawer: There is nothing special about Drawer.
3. To refactor the processing of the Investigate action, creating a separate function for this. The code of the run function after refactoring should look like this:
...
Investigate obj -> do
putStrLn ( investigate obj locObjects )
run curLoc
...
Sources to this part .
Table of contents, list of references and additional information can be found in the
"Welcome" .