Haskell Quest Tutorial - Canyon View

  • Tutorial
Canyon View
You are at the top of Great Canyon on its west wall. From here there is a marvelous view of the canyon and parts of the Frigid River upstream. Across the canyon, the walls of the White 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 flows out from a great dark cavern. To the west and south can be seen an immense forest, stretching for miles around. A path leads northwest. It is possible to climb down into the canyon here.


Contents:
Greeting
Part 1 - Entrance
Part 2 - Forest
Part 3 - Polyana
Part 4 - View of the canyon
Part 5 - Hall

Part 4,
in which we will refactor, implement a couple of actions, learn about pattern matching and recursion, and also make from the quest real program.


And let's enter the current location? And then say - it's time. We already have a run function, where all the most important things happen, which means that the current location should be in it. Suppose the function run first displays a description of the location, and then everything else. So, the run function knows about the current location. We will pass this location to her 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 room at the wooden table.
Enter command: Look
Action: Look!
* Main>


What a short program, however! It works as expected, but it ends right there. Well, this is not a game. In games, an event handler usually spins in a loop until it is clearly 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? To begin with, 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 we transfer the old current location, and later we’ll come up with something.

run curLoc = do
        putStrLn (describeLocation curLoc)
        putStr  "Enter command:"
        x <- getLine
        putStrLn (evalAction (convertStringToAction x))
        putStrLn  "End of turn. \ n"   - New move - from a new line.
        run curLoc
 
- Testing:
 
* Main> run Home
Home
You are standing in the middle room at the wooden table.
Enter command: Look
Action: Look!
End of turn.
 
Home
You are standing in the middle room at the wooden table.
Enter command:  Interrupted


The program obediently started the barrel organ from the beginning. We do not yet have adequate event handling, so the only way to interrupt the program is to click. And to make her react to the Quit command, a little refactoring is needed. We 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 dirty here! .. Let's figure it out. The start of the run function is familiar to you. The do keyword at the very beginning links the actions in a chain. What actions are included in this chain? Only four: (print the description of the location) - (display the line “Enter command:„) - (get the string from the user and associate it with the variable x) - (execute the expression inside the case-construction). When execution reaches case, it jumps to the desired alternative and continues already there. Suppose the user entered “Quit”, and then the function (convertStringToAction x) returns the Quit constructor, which means that the first alternative should work. There is only one action in this alternative - printing the line “Be seen you ...”. There are no more actions anywhere - neither inside the Quit alternative, nor after the case-construction, so the run function has nothing more to do, and it will end. Now suppose the user didn’t enter “Quit,” but “Look," what will happen? It’s clear that the Quit alternative will not work, butotherwise it is always on guard, and execution will continue there. And what's there? Another “do” keyword! So, a new chain of actions has begun here, and it is carried out in exactly the same way, in steps. What actions are included in this chain? Only three: (print the processed command) - (print the line "End of turn. \ N") - (run the run function).

At first glance, the function is incomprehensible: all these case, do, left and right arrows! But everything falls into place if you carefully monitor the implementation. We just have to check whether the program actually works as we think.

* Main> run Home
Home
You are standing in the middle room at the wooden table.
Enter command: Look
Action: Look!
End of turn.
 
Home
You are standing in the middle room at the wooden table.
Enter command: Quit
Be seen you ...
* Main>


Hurrah! We are well done! But our run function is still very far from ideal. Please note that it contains a call (convertStringToAction x) twice, and this is bad. Fortunately, the function convertStringToAction is simple, it does not require resources, otherwise it would be an overrun. In Haskell, and in any other language, repetition should be avoided. Since this call is in our case-construction, its result can be put into a variable. Let's change the run function a little bit:

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 a new line.
                                run curLoc


Yes, instead of otherwise, the convertResult variable is now. If the previous alternatives on some value did not work, then this value, calculated once, is placed in a variable and then used. And this is a good practice, which is even more common than otherwise , for obvious reasons.

Well, that 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 exactly where the first of them began. It could be aligned differently, for example, like this:

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 move - from a new line.
                run curLoc
 


Each block has its own indentation, and you do not need to mix them.


Who said you can diverge? We continue! Of course, you are already making plans to improve the program. And rightly so! Add the actual (and not fictitious) processing of the Look command yourself someday, but 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 is not clear to him how to parse this line:

* Main> run Home
Home
You are standing in the middle room at the wooden table.
Enter command: Go West
*** Exception: Prelude.read: no parse


Maybe we’ll break the line “Go West” into two and parse them separately, because we have Go and West constructors? The idea, of course, is correct, and it certainly will work, but ... Haskell would not be a magic language if it did not allow Go West to be parsed even easier, just for this you need to prepare something. Let's go from afar. As you remember, in the third part there was an inset that the constructors of ADT 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


What do we know about functions? Right. That they may have arguments. And what question should we ask? Right. “If constructors are functions, does this mean that they can also have arguments?” As you may have guessed, yes! When defining an ADT type, you can specify which types of arguments are passed to this or that constructor. Add a Direction argument to the Go constructor.

data Action =
          Look
        | Go direction
        | Inventory
        | Take
        | Drop
        | Investigate
        | Quit 
        | Save 
        | Load 
        | New
    deriving ( EqShowRead )


In this case, the type of the constructor will change slightly, because we made a function that takes a Direction and returns an Action. Let's check how the translation of the constructor into the string and vice versa works:

* Main> show (Go West)
"Go West"
 
* Main> read  "Go North"  :: Action
Go North
 
* Main>: t Go
Go :: Direction -> Action


Here. And, best of all, there are almost no efforts on our part. A composite constructor is no more complicated than others; it is even better, because it intuitively sets data. And working with him is very simple! Update the run case:

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 a new line.
                                run curLoc
 
- Check:
 
* Main> run Home
Home
You are standing in the middle room at the wooden table.
Enter command: Go West
You going to West!


If the parser recognizes “Go Something” in the line, the alternative “Go dir” will work, and the same “Something” will fall into the dir variable. Well, then we work with the dir variable. Magic!

Haskell's algebraic data types are even better. Perhaps you have already figured out how to rewrite the "Dumb Calculator" task from the third part using constructors with arguments. You could get 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 mapped to each option in turn.
                Minus x y -> x - y - Which sample came up, such an 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.
5
* Main> evalOp (Minus 2 3)
-1
* Main> evalOp (Prod 2 3)
6
* Main> evalOp (Negate 5)
-5


What kind of adventurers are we if we stomp in the same location? It is necessary to gather strength into a fist and make a transition between locations! Suppose we have a walk function (task from the second part), which takes the current location and direction of movement, and returns a new location. Here is one:

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 case! 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 are no cases:

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, how many walk functions! And all the same, there are fewer lines than in the previous example, and 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 will be chosen, which will return Home.

* Main> walk Garden South
Home


Interest is the last walk. She just leaves us in the current location. You can guess that when everyone else does not work, it will work. In it, the first parameter will fit into the curLoc variable, and the second parameter will not fit anywhere. For us, in general, it doesn’t matter what is in the second parameter, we won’t use it, therefore we put the underscore sign. You can, of course, slip some kind of variable, but the underline is more obvious. Do not specify an argument; if you do so, ghci scolds, they say, why are you so fickle? ..

......
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, which is used here, is a useful tool, with it the code becomes clearer, safer, shorter. There are other nice features of pattern matching. You can “palm off” not only constants, but also variables, and even disassemble the term into its component parts. Using a special entry, it is easy to separate the first element of the list and the rest. Here's what it looks like 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 final step is to modify the run function. Everything is trivial: we add a chain of actions to the Go dir alternative, display the line "\ nYou walking to" ++ show dir ++ ". \ N", and run the run function again, but this time we get the 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 ( "\ nYou 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, in fact, is 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 spin recursion even a thousand times, which means you should not be afraid of a resource leak, which could have been in the same C ++. Haskell has mechanisms in place that make recursion a safe tool. 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 is able to reuse what was once computed. There is no stack as such in Haskell - the memory will not be full. Finally, optimizing the recursion into the tail call,

You can compile a simple program and look at the dynamics of resource use. 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 ( "\ nTurns count:"  ++ show cnt)
        putStrLn $ show $ last $ take cnt fibs
        run (cnt + 1)
 
main = do
    run 1


Compilation with optimization:

ghc -O test.hs


Do you like code order? The code of our quest looks, in general, not bad, but it lacks one important thing: the entry point to the program. It cannot even be compiled into an executable file. While we used the GHCi interpreter, we did not think about it, but what if a friend wants to play our game? It is not Haskell’s code to be given to him - a friend in general, maybe not a programmer and will understand nothing. But the executable is easy. To compile the program for real, at the command prompt, run the ghc command, 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 module 'Main'


The GHC compiler says that the main function was not found in the Main module. This is how the entry point to the Haskell program is formed. 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, files with the extensions .o and .hi will appear - 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 significantly faster than compilation from scratch. For example, if a module was once compiled and no longer changed, just as the modules on which it depends did not change, then it does not need to be recompiled, just take the old .o and .hi files. Therefore, it is good practice to separate the code into modules; even better - by modules and folders; 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 a Types.hs file, at the very top define it as a module using the line “module Types where” and transfer all the ADT types from the QuestMain.hs file there.

- Types.hs:
 
module Types where
 
data Location =
          Home
        | ...
    deriving ( EqShowRead )
 
data Direction =
              North
            | ...
    deriving ( EqShowRead )
 
data Action =
          Look
        | Go direction
        | ...
    deriving ( EqShowRead )


If ghci now executes the command: r, the interpreter will panic: it 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 need to connect our module with types - and they will become 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 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 divided the code into modules - and that’s good. We added an entry point, and the code became a real program. It's also good. In the end, we got acquainted with recursion and pattern matching, and also came up with one composite constructor of the Action type. Nice job! Now you should have a good rest and consolidate knowledge.

Assignments for fixing.

1. Create a GameAction module in the GameAction.hs file and extract all functions from Main except main and run into it.
2. Add (if not already) the processing of the Look command next to the processing of Quit, Go dir. Consider how you can improve duplicate code "(describeLocation curLoc)" so as not to call it twice.
3. Add processing for the New command with a check to see if the user is sure that he wants to start a new game.


Sources for this part .

The table of contents, references and additional information can be found in the Greetings .

Also popular now: