Canyon view
You are at the top of the Great Canyon on its west wall. From here there is a marvelous view of the canyon and parts of the Frigid River upstream. The Cliffs and the Cliffs join the mighty ramparts of the Flathead Mountains to the east. Following the Canyon upstream to the north, Aragain Falls may be seen, complete with rainbow. The mighty frigid river cavern. Stretching for miles around. A path leads northwest. It is possible to climb down into the canyon here.
Content:
GreetingPart 1 - The ThresholdPart 2 - ForestPart 3 - PolyanaPart 4 - View of the canyonPart 5 - HallPart 4,
in which we will do refactoring, implement a couple of actions, learn about pattern matching and recursion, and also make a real program from the quest.
')
And let's enter the current location? And it’s time to say something. We already have a run function, where all the most important things happen, which means that it should contain the current location. Suppose, in the run function, the location description is first displayed, and then everything else. This means that the run function is aware of the current location. Let's give her this location as a parameter:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
putStrLn ( evalAction ( convertStringToAction x ) )
Good. We try:
* Main > run Home
Home
You are standing in the middle of the wooden table .
Enter command: Look
Action: Look !
* Main >
What a short program, however! It works as expected, but ends right there. Well, this is not a game. In games, the event handler is usually spinning in a loop until it is explicitly interrupted by the “Exit” button. I would like something like this: the game should work until we say “Quit” to it. How to do it? First we need to organize continuous processing of commands from the user. The easiest way is to call run from itself. It has a parameter, - the current location, - while transmitting the old current location, and later we will think of something.
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
putStrLn ( evalAction ( convertStringToAction x ) )
putStrLn “End of turn. \ n " - New move - from the new line.
run curLoc
- We are testing:
* Main > run Home
Home
You are standing in the middle of the wooden table .
Enter command: Look
Action: Look !
End of turn .
Home
You are standing in the middle of the wooden table .
Enter command: < Ctrl + C > Interrupted .
The program obediently started a street organ from the beginning. We do not yet have adequate event handling, so the only way to interrupt the program is to press <Ctrl + C>. And to make her react to the “Quit” command, a little refactoring is needed. Rewrite the run function as follows:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
otherwise -> do
putStrLn ( evalAction ( convertStringToAction x ) )
putStrLn “End of turn. \ n "
run curLoc
Yeah, something is unclean here! .. Let's see. The beginning of the run function is familiar to you. The “do” keyword, which stands at the very beginning, links actions into a chain. What actions are included in this chain? Only four: (print the location description) - (output the line “Enter command:„) - (get the string from the user and link it to the variable x) - (execute the expression inside the case structure). When execution comes to case, it jumps to the right alternative and continues there. Suppose a user has entered “Quit”, and then the function (convertStringToAction x) returns the Quit constructor, which means that the first alternative should work. In this alternative, only one action - printing the string "Be seen you ...". There is no other action anywhere — neither inside the Quit alternative, nor after the case construct, so the run function has nothing more to do and it will end. Now suppose that the user entered not “Quit”, but “Look”, - what will happen? It is clear that the alternative to Quit will not work, but
otherwise is always on guard - that is where the execution will continue. And what's there? Another keyword "do"! It means that a new chain of actions has begun here, and it is performed in the same way, in steps. What actions are included in this chain? Only three: (print the processed command) - (display the line "End of turn. \ N") - (start the run function).
At first glance, the function is incomprehensible: all these cases, do, left and right arrows! But everything is in place, if you carefully trace the implementation. We only need to check whether the program really works, as we think.
* Main > run Home
Home
You are standing in the middle of the wooden table .
Enter command: Look
Action: Look !
End of turn .
Home
You are standing in the middle of the wooden table .
Enter command: Quit
Be seen you ...
* Main >
Hooray! We are great! But our run function is still far from ideal. Note: there is a call (convertStringToAction x) twice in it, and this is bad. Fortunately, the convertStringToAction function is simple, it does not require resources, otherwise it would be an overrun. In Haskell, and in any other language, you need to avoid repetition. Since we have this call in the case-construction, its result can be put in a variable. Slightly change the run function:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
convertResult -> do
putStrLn ( evalAction convertResult )
putStrLn “End of turn. \ n " - New move - from the new line.
run curLoc
Yes, instead of
otherwise now the convertResult variable. If past alternatives on some value did not work, then this value, calculated once, is placed in the variable and then used. And this is a good practice, which is even more common than
otherwise , for obvious reasons.
Well, it was very difficult, so that's all for today.
Do not forget about indents, they are important here; for the first and second “do” all actions are located strictly where the first one began. It would be possible to align it in another way, for example, as follows:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
otherwise -> do
putStrLn ( evalAction ( convertStringToAction x ) )
putStrLn "End of turn. \ n " - New turn - from a new line.
run curLoc
Each block has its own indents, and there is no need to mix them.
Who said you can disperse? We continue! Of course, you are already making plans for how to improve the program. And right! Add some actual (and not fictitious) processing of the Look command yourself, but for now let's think about the Go command. How is it implemented in Zork?
Clearing
...
> Go West
Forest
...
If we write “Go West”, the command parser will be unhappy because it doesn’t understand how to parse this line:
* Main > run Home
Home
You are standing in the middle of the wooden table .
Enter command: Go West
*** Exception: Prelude . read : no parse
Maybe we break the line “Go West” into two and separate them, because we have the designers Go and West? The thought is certainly correct, and it certainly will work, but ... Haskell would not be a magic language if it didn’t allow Go West to be parsed even easier, but for that you need to prepare something. Come from afar. As you remember, in the third part there was a tie-in that the constructors of ATD-types are special functions. For example, Home is a function of type Location, Go is a function of type Action, and West is a function of type Direction.
* Main > : type Home
Home :: Location
* Main > : type West
West :: Direction
* Main > : type Go
Go :: Action
And what do we know about the function? Right. That they may have arguments. And what question should we ask? Right. “If constructors are functions, then does this mean that they can also have arguments?” As you may have guessed, yes! When defining an ATD type, you can specify which type arguments are passed to one or another constructor. Add a Direction type argument to the Go constructor.
data Action =
Look
| Go direction
| Inventory
| Take
| Drop
| Investigate
| Quit
| Save
| Load
| New
deriving ( Eq , Show , Read )
In this case, the type of the constructor slightly changes, because we have made a function that accepts Direction, and returns an Action. Check how the translation of the constructor into the string and vice versa will work:
* Main > show ( Go West )
"Go West"
* Main > read "Go North" :: Action
Go north
* Main > : t Go
Go :: Direction -> Action
Here it is. And, best of all, almost no effort on our part. The composite constructor is no more difficult than the others; it is even better because it intuitively sets the data. And working with him is very easy! Update the run case with:
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
Go dir -> putStrLn ( "You going to" ++ show dir ++ "!" )
convertResult -> do
putStrLn ( evalAction convertResult )
putStrLn “End of turn. \ n " - New move - from the new line.
run curLoc
- Check:
* Main > run Home
Home
You are standing in the middle of the wooden table .
Enter command: Go West
You going to West !
If the parser recognizes in the “Go Something” line, the alternative “Go dir” will work, and that “Something” will fall into the dir variable. Well, then we are working with the variable dir. Magic!
Algebraic data types in Haskell are even better. Perhaps you have already figured out how to rewrite the “Blunt Calculator” puzzle from the third part, using constructors with arguments. You could have something like this:
data IntegerArithmOperation =
Plus Integer Integer
| Minus integer integer
| Prod integer integer
| Negate integer
evalOp :: IntegerArithmOperation -> Integer
evalOp op = case op of
Plus x y -> x + y - The “op” parameter is matched with each option in turn.
Minus x y -> x - y - Which sample came up, this alternative is chosen.
Prod x y -> x * y - Instead of x and y, the numbers that we passed along with the constructor are substituted.
Negate x -> - x - Returns the result of the calculation. For example, 2 * 3 = 6.
* Main > evalOp ( Plus 2 3 ) - (Plus 2 3) is a constructor with two arguments.
five
* Main > evalOp ( Minus 2 3 )
- 1
* Main > evalOp ( Prod 2 3 )
6
* Main > evalOp ( Negate 5 )
- 5
What are we adventurers, if we are marking time in the same location? It is necessary to gather strength into a fist and make the transition between locations already! Suppose we have a function walk (task from the second part), which takes the current location and direction of movement, and returns a new location. Here it is:
walk :: Location -> Direction -> Location
walk curLoc toDir = case curLoc of
Home -> case toDir of
North -> Garden
South -> Friend'sYard
otherwise -> Home
Garden -> case toDir of
North -> Friend'sYard
South -> Home
otherwise -> Garden
- ... Add the remaining options here.
In fact, a terrible approach. So much extra work! So many nested cases! We will definitely come up with something else when we have enough knowledge, and now we can only improve the walk function so that there is no case:
walk :: Location -> Direction -> Location
walk Home North = Garden
walk Home South = Friend'sYard
walk Garden North = Friend'sYard
walk Garden South = Home
walk Friend'sYard North = Home
walk Friend'sYard South = Garden
walk curLoc _ = curLoc
Wow, so many walk functions! And all the same - the lines are smaller than in the previous example, and there is more functionality. How it works? Very simple. When we call the walk function with arguments, the appropriate option is selected. For example, “walk Garden South” - and the fourth one will be selected, which will return Home.
* Main > walk Garden South
Home
Interest is the last walk. It just leaves us in the current location. One can guess that when all the others do not work, it will work. In it, the first parameter will fit into the curLoc variable, and the second parameter will not fit anywhere. We, in general, and it does not matter what is in the second parameter, we will not use it, that's why we put the underscore sign. You can, of course, slip some variable, but the underlining is more obvious. Do not specify an argument; if you do that, ghci will scream, they say, why are you so inconstant? ..
......
walk Friend'sYard North = Home
walk Friend'sYard South = Garden
walk curLoc = curLoc
* Main > : r
[ 1 of 1 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 50 : 1 :
Equations for 'walk' have different numbers of arguments
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 50 : 1 - 24
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 56 : 1 - 22
Failed , modules loaded: none .
Pattern matching (pattern matching), which is used here, is a useful tool, with which the code becomes clearer, safer, and shorter. There are other nice features of pattern matching. You can “slip” not only constants, but also variables, and even disassemble a term into its constituent parts. With the help of a special record it is easy to separate the first element of the list and the rest. Here’s how it looks for strings:
headLetter :: String -> Char
headLetter ( ch: chs ) = ch
tailLetters :: String -> String
tailLetters ( ch: chs ) = chs
* Main > headLetter "Abduction"
'A'
* Main > tailLetters "Abduction"
"Bduction"
The last step is to modify the run function. Everything is trivial: add a chain of actions to an alternative to Go dir, output the line "\ nYou walking to" ++ show dir ++ ". \ N", and run the run function again, but this time we will get a new current location using walk .
run curLoc = do
putStrLn ( describeLocation curLoc )
putStr "Enter command:"
x <- getLine
case ( convertStringToAction x ) of
Quit -> putStrLn "Be seen you ..."
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
- Check
* Main > run Home
Home
You are standing ...
Enter command: Go North
You walking to North .
Garden
You are in the garden . ...
That's all. Go action works.
Recursion is recursion. it is a function call from itself. We are actively using recursion in Haskell - and there is nothing wrong with that. Unlike imperative languages ​​such as C ++, functional languages ​​encourage us to use recursion where it is difficult to do with other methods. In this case, recursion is acceptable; we don’t produce a lot of data, we don’t twist recursion even a thousand times, so you shouldn’t be afraid of resource leaks, which could be in the same C ++. In Haskell, there are mechanisms with which recursion becomes a safe means. So, only those expressions that are directly needed here and now are calculated, which means that resources are not spent on a huge layer of other code. The compiler can reuse what was once computed. There is no stack as such in Haskell - the memory will not overflow. Finally, by optimizing recursion into a tail call, the compiler can ensure that there is no recursion in the usual sense at all.
You can compile a simple program and look at the dynamics of the use of resources. The program calculates the Fibonacci numbers at each step of the recursion and displays the last of them.
- test.hs:
module Main where
fibs = 0 : 1 : zipWith ( + ) fibs ( tail fibs )
run cnt = do
putStrLn ( " \ n Turns count:" ++ show cnt )
putStrLn $ show $ last $ take cnt fibs
run ( cnt + 1 )
main = do
run 1
Compile with optimization:
ghc - O test . hs
Do you like order in code? The code of our quest looks, in general, not bad, only it lacks one important thing: the entry point to the program. It cannot even be compiled into an executable file. While we were using the GHCi interpreter, we didn’t think about it, but what if a friend wants to play our game? It’s not the Haskell code to give it to - a friend in general, maybe not a programmer, and he will not understand anything. But the executable file is easy. To compile a program for real, on the command line, run ghc, specifying the path to QuestMain.hs:
H: \ Haskell \ QuestTutorial \ Quest > ghc QuestMain . hs
[ 1 of 1 ] Compiling Main ( QuestMain . Hs , QuestMain . O )
QuestMain . hs: 1 : 1
The function "Main" is not defined in the module .
The GHC compiler says that the main function was not found in the Main module. This is the way the entry point is entered into a Haskell program. It looks like other languages ​​where there is a main function in one form or another. Add it to the bottom of the QuestMain.hs file:
main = do
putStrLn "Quest adventure on Haskell. \ n "
run home
And at the very beginning of the file we define the Main module, in which all our functions will lie:
module Main where
Now the compiler safely eats the source, and you will see the executable file. I have this QuestMain.exe. Among other things, there will be files with the extensions .o and .hi - these are temporary files (object and file with interfaces). If they bother you, you can remove them. While the project is small, they do not play a special role. Then they, as in other languages, can be used for partial compilation, which is much faster than compiling from scratch. For example, if a module was once combined and never changed again, as well as the modules on which it depends did not change, then it does not need to be recompiled, it is enough to take old .o- and .hi-files. Therefore, it will be good practice to divide the code into modules; even better - by module and folder; and even better - by modules, folders and libraries.
Let's divide our quest into two modules: the Types module and the Main module. To do this, create the Types.hs file, at the very top define it as a module using the line “module Types where” and transfer all the ATD types from the QuestMain.hs file there.
- Types.hs:
module Types where
data Location =
Home
| ...
deriving ( Eq , Show , Read )
data direction =
North
| ...
deriving ( Eq , Show , Read )
data Action =
Look
| Go direction
| ...
deriving ( Eq , Show , Read )
If in ghci now execute the command: r, the interpreter will panic: he does not know these types!
* Main > : r
[ 1 of 1 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 4 : 21 :
Not in scope: type constructor or class 'Location'
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 7 : 13 :
Not in scope: data constructor 'Home'
H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 8 : 13 :
Not in scope: data constructor 'Friend'sYard'
... and there are twenty more such lines .
Prelude >
No problem! We have to connect our module with types, and they will be visible in Main. Somewhere at the top of the Main module, under “module Main where”, add a simple line:
- QuestMain.hs:
module Main where
import types
- ... the rest of the code ...
Now the compilation is successful. We have time to notice: there are already two compiled files, which is logical.
Prelude > : r
[ 1 of 2 ] Compiling Types ( H: \ Haskell \ QuestTutorial \ Quest \ Types . Hs , interpreted )
[ 2 of 2 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )
Ok , modules loaded: Main , Types .
* Main >
We have divided the code into modules, which is good. We added an entry point - and the code became a real program. It's also good. We finally got acquainted with recursion and pattern matching, and also came up with one composite constructor of type Action. Great job! Now you should have a good rest and consolidate knowledge
Tasks for fixing.
1. Create a GameAction module in the GameAction.hs file and bring all the functions from Main into it, except main and run.
2. Add (if not already) handling the Look command next to Quit, Go dir. Think about how you can improve duplicate code "(describeLocation curLoc)", so as not to call it twice.
3. Add the processing of the New command to check whether the user is sure that he wants to start a new game.
Sources to this part .
Table of contents, list of references and additional information can be found in the
"Welcome" .