Haskell Quest Tutorial - Hall
- Tutorial
Most likely, this is the last part published on time. My vacation is almost over, and now writing an article a week will be very difficult. Thanks to everyone who was interested in the Haskell Quest Tutorial!
Contents:
Greeting
Part 1 - Anticipation
Part 2 - Forest
Part 3 - Polyana
Part 4 - View of the canyon
Part 5 - Hall
Part 5,
in which we will draw significant consequences from a small mistake, and then add objects to the game.
First, let's refresh our memory. Let's go over 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:
In task number 2 to the last part, there was a proposal to think about how to avoid two calls to "(describeLocation curLoc)". Of course, we 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 there is no assignment as such. But there are other mechanisms that, as it turns out, not only generalize the assignment, but also, taking into account the unchanged state, give interesting effects. For example, the determinism of execution. In fact, if some global state does not affect the function, and inside it the data is obviously unchanged, it means that it will return the same values to the same arguments.
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 similar to an assignment, and the idea here is simple: you can use the variable as much as you need. From the first call you get approximately the following code:
But this code is a trick because it no longer compiles.
What is the difference between "x <- getLine" and "locDescr <- describeLocation curLoc"? It seems to be the same thing: the getLine function returns a string that is associated with the variable x; and the function (describeLocation curLoc) also returns a string that binds to another variable locDescr. But the interpreter says that some types do not match. Let's look at the situation - and for this you need to understand what actually happens in the run function.
Let's go back to the working code, commenting out the error lines.
We intentionally did not write a definition of the run function, so as not to go into the intricacies of its type. But she has a type, and the compiler dutifully displays it himself. Check:
And here a surprise awaits us. It is clear that Location is the type of a single parameter, and for the strange type IO () there is nothing left but to be the type of the 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 his dark deeds and arbitrariness, then do not rush: IO we very much use it. The fact is that according to Haskell, any input-output (I / O) action is a potential error and malfunction, also known as “side effects”. Therefore, the I / O functions (putStrLn, putStr, putChar, getLine, getChar, readFile, writeFile) are dangerous, they can return different results to the same arguments, and in some cases they can even throw an error. Therefore, they are of type IO, which, in the opinion of the language, should concern us. Moreover, all other functions that have an IO type inside become non-deterministic too and must adopt it. Our run function chose unsafe actions, which is why it became infected with the IO () type. It follows, by the way, that the entry point to the program - the main function - did not escape this fate, since run is called in it.
How does the above 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.
The do keyword, as we know, links actions in a chain, but not in any way. do-notation requires that all actions in the chain return the same type. Since we work with I / O, the type of functions must contain IO.
In addition to an argument of type String, these two functions have the same return type - IO (). Empty brackets here indicate that nothing useful is returned. In fact, it doesn’t matter to us what putStrLn returns, if only it would print its argument in the console. On the contrary, we expect something “useful” from the getLine function, and this is reflected in its type in its own way:
getLine receives a string from the user and packs it, as a gift, into the IO type. And do whatever you want with the gift. For example, we pass it to the convertStringToAction function:
But the convertStringToAction function has a different type! - exclaim you and you will be right. She is waiting for String at the input, not IO String! What is the trick? Well, somewhere in these two lines the gift unfolds, the IO box is thrown away, and the blank line goes on. And this is done by the arrow. It takes the packed result from the right side and unpacks it into a variable on the left. That is why, by the way, this operation is called binding, not assignment. And if you think about it, our expression “locDescr <- describeLocation curLoc” is wrong, because nothing is packed anywhere on the right side - read, the result is not put in the IO box.
Well, here we are ... They wanted it as simple as that, but they got what kind of consequence as many pages. But I have good news for you: there is a solution! We ourselves will pack what is on the right into type IO, and let the arrow choke. There is a return function for this. The following code will work as intended:
Yes, the return function will pack the string into the do (IO) block type, and the arrow will unpack it and associate it with the locDescr variable. And you no longer need to calculate the location description several times. The logic, of course, is unusual, but you must agree: there is some kind of inner beauty in it. It’s worth a drink!
Well, we solved the problem, which turned out to be unexpectedly capacious. So why should we 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. Inside the where block, the variables of the parent function are available, and all laws that apply to ordinary functions apply. This means that you can create several variants of the same function, use pattern matching, security expressions, and even create nested where-constructions.
Where the construction does not require any special explanations, just look at the example:
We did the same indent before the where clause as the do block. Essentially, we defined where for this block. It might seem like we are assigning the result to the variable locDescr, but it is not. There are no variables and assignments here; locDescr is a function that returns a description of a location. We call this function from the parent code, and twice; however, it is most likely calculated only once. Haskell compilers can keep past results for reuse. When data is no longer needed, it is deleted by the built-in garbage collector. Remember the sidebar from the last part, where the Fibonacci numbers are calculated. How would the program work if at every 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. And although let-expressions are very similar to where, they define expression aliases, not functions with such names. Inside the do block, the let expression looks like this:
If you use a let expression outside the do block, for example, inside other expressions, then the record will change a little. The in keyword is added:
In fact, you can define any number of such expressions between let and in - you will see how this is simply done when we move on to adding objects. In the meantime, we will leave the previous option, where the let-expression is inside the do-block.
You ask when we get to the objects? I have already switched. Are you not yet, or what? Then catch up. Create an ADT type for objects, like this:
In the image of the locations, add a function with a description of the objects:
There are no dirty tricks, we already know all this and we are able. A new one begins when we need to define objects for locations. We would like, of course, that everything was ready, the Take and Drop actions worked, there was an inventory, compound objects ... But this does not happen. Now we will make a draft version of the objects in locations where they can neither be taken nor thrown, and on its basis 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 table drawer, a telephone and an umbrella. We set the locationObjects function, which would return a list of objects for this location:
In general, the lists look simple. A list of objects is a type of [Object]. For the Home location, we return a list of four objects. An empty list is indicated by empty square brackets, so long as all the other locations are without objects. It is worth testing the code before moving on.
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:
The converse is also true. If the list items can be parsed by the read function, then the whole list can also be parsed:
Now that we have understood the syntax, we will make it so that for the current location not only its description is displayed, but also a list of objects that are there is printed. Let's use let-expressions:
The output of the program now looks like this:
The code has a little nuisance. Even if there are no objects in the location, the inscription “There are some objects here:” will still be an eyesore to us.
In order not to display it for an empty list of objects, we put all the code for describing objects into a separate function, call it enumerateObjects. At the input, it takes a list, and at the output it passes a string with the listed 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:
Or, if you want, you can pull out the let expressions from the do block:
Thanks to this simple refactoring, we can now modify the enumerateObjects function without affecting run. So we wanted to, say, not only enumerate objects, but also display descriptions of them - so we climb into our enumerateObjects with our playful pens and fix something clever there. But somehow later. Now add the custom action Investigate. By this command, the program should give a detailed description of the object. It is clear that the user should enter something like this:
The line “Investigate Umbrella” will have to be parsed. Familiar, right? We already went through this with the Go team. The same thing here: just put an Object parameter in the Investigate constructor.
Only a little remained - to correct the run function:
And voila! You have implemented another user action! Congratulations! You can verify that if you enter the command "Investigate Umbrella", the program will display the string "Nice red mechanic Umbrella."
That's just ... Do you know what's bad with us? If we, when in the Home location, enter the “Investigate MailBox” command, we will be given a description of the mailbox, which is not visible from here at all! Well, we’ll come up with an isVisible function, which will return True if we see an object, and only in this case we will issue a description of the object. What are the arguments to the isVisible function? We need an object to be studied, as well as a list of all location objects. Something like this:
Now you need to somehow find out if there is an object in this list. We will not come up with “stupid” options with enumerating all the objects in the list, although this is very possible, we just use the standard elem function. It takes two parameters: element and list. If the item is found in the list, returns True. Exactly what is needed!
Well, all that remains is to use this feature. As usual, we will change run, and in order not to request objects of the current location several times, we will put them in a let-expression:
That's all. We will stop on this major note - it’s time to rest, we already got a huge cart of knowledge.
Sources for this part .
The table of contents, references and additional information can be found in the Greetings .
Living Room
You are in the living room. There is a doorway to the east, a wooden door with strange gothic lettering to the west, which appears to be nailed shut, a trophy case, and a large oriental rug in the center of the room.
Above the trophy case hangs an elvish sword of great antiquity.
A battery-powered brass lantern is on the trophy case.
Contents:
Greeting
Part 1 - Anticipation
Part 2 - Forest
Part 3 - Polyana
Part 4 - View of the canyon
Part 5 - Hall
Part 5,
in which we will draw significant consequences from a small mistake, and then add objects to the game.
First, let's refresh our memory. Let's go over 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 ( "\ nYou walking to" ++ show dir ++ ". \ n" )
run (walk curLoc dir)
convertResult -> do
putStrLn (evalAction convertResult)
putStrLn "End of turn. \ n"
run curLoc
In task number 2 to the last part, there was a proposal to think about how to avoid two calls to "(describeLocation curLoc)". Of course, we 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 there is no assignment as such. But there are other mechanisms that, as it turns out, not only generalize the assignment, but also, taking into account the unchanged state, give interesting effects. For example, the determinism of execution. In fact, if some global state does not affect the function, and inside it the data is obviously unchanged, it means that it will return the same values to the same arguments.
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 similar to an assignment, and the idea here is simple: you can use the variable as much as you need. 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 no longer compiles.
* 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 thing: the getLine function returns a string that is associated with the variable x; and the function (describeLocation curLoc) also returns a string that binds to another variable locDescr. But the interpreter says that some types do not match. Let's look at the situation - and for this you need to understand what actually happens in the run function.
Let's go back to the working code, commenting out the error 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 a definition of the run function, so as not to go into the intricacies of its type. But she has a type, and the compiler dutifully displays it himself. Check:
* Main>: t run
run :: Location -> IO ()
And here a surprise awaits us. It is clear that Location is the type of a single parameter, and for the strange type IO () there is nothing left but to be the type of the 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 his dark deeds and arbitrariness, then do not rush: IO we very much use it. The fact is that according to Haskell, any input-output (I / O) action is a potential error and malfunction, also known as “side effects”. Therefore, the I / O functions (putStrLn, putStr, putChar, getLine, getChar, readFile, writeFile) are dangerous, they can return different results to the same arguments, and in some cases they can even throw an error. Therefore, they are of type IO, which, in the opinion of the language, should concern us. Moreover, all other functions that have an IO type inside become non-deterministic too and must adopt it. Our run function chose unsafe actions, which is why it became infected with the IO () type. It follows, by the way, that the entry point to the program - the main function - did not escape this fate, since run is called in it.
* Main>: t main
main :: IO ()
In fairness, it is worth saying that in the explanations the assumption is made 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 discussions can be found on the Internet.
How does the above 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 do keyword, as we know, links actions in a chain, but not in any way. do-notation requires that all actions in the chain return the same type. Since we work with I / O, the type of functions must contain IO.
* Main>: t putStrLn
putStrLn :: String -> IO ()
* Main>: t putStr
putStr :: String -> IO ()
In addition to an argument of type String, these two functions have the same return type - IO (). Empty brackets here indicate that nothing useful is returned. In fact, it doesn’t matter to us what putStrLn returns, if only it would print its argument in the console. On the contrary, we expect something “useful” from the getLine function, and this is reflected in its type in its own way:
* Main>: t getLine
getLine :: IO String
getLine receives a string from the user and packs it, as a gift, into the IO type. And do whatever you want with the gift. For example, we 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 String at the input, not IO String! What is the trick? Well, somewhere in these two lines the gift unfolds, the IO box is thrown away, and the blank line goes on. And this is done by the arrow. It takes the packed result from the right side and unpacks it into a variable on the left. That is why, by the way, this operation is called binding, not assignment. And if you think about it, our expression “locDescr <- describeLocation curLoc” is wrong, because nothing is packed anywhere on the right side - read, the result is not put in the IO box.
* Main>: t (describeLocation Home)
(describeLocation Home) :: String
Well, here we are ... They wanted it as simple as that, but they got what kind of consequence as many pages. But I have good news for you: there is a solution! We ourselves will pack what is on the right into type IO, and let the arrow choke. There is a return function for this. 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 associate it with the locDescr variable. And you no longer need to calculate the location description several times. The logic, of course, is unusual, but you must agree: there is some kind of inner beauty in it. It’s worth a drink!
Do-notation and IO type have even more consequences and pitfalls. If we dig deeper, we will see a universal way to tame not only side effects, but also many other calculations that can be related according to different laws than IO actions. Making the calculations using the so-called monads , we end up with surprisingly simple and convenient logic, and the code becomes concise, understandable and expressive. It is important that such a code is strictly mathematical and very convenient. It is very suitable for describing specific tasks such as parsing, DSL, mutable states, and much more. Perhaps someday we will return to this topic - but not earlier than this will really be needed.
Well, we solved the problem, which turned out to be unexpectedly capacious. So why should we 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. Inside the where block, the variables of the parent function are available, and all laws that apply to ordinary functions apply. This means that you can create several variants of the same function, use pattern matching, security expressions, and even create nested where-constructions.
Where the construction does not require any special explanations, 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 did the same indent before the where clause as the do block. Essentially, we defined where for this block. It might seem like we are assigning the result to the variable locDescr, but it is not. There are no variables and assignments here; locDescr is a function that returns a description of a location. We call this function from the parent code, and twice; however, it is most likely calculated only once. Haskell compilers can keep past results for reuse. When data is no longer needed, it is deleted by the built-in garbage collector. Remember the sidebar from the last part, where the Fibonacci numbers are calculated. How would the program work if at every 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. And although let-expressions are very similar to where, they define expression aliases, not functions with such names. Inside the do block, the 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 you use a let expression outside the do block, for example, inside other expressions, then 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 any number of such expressions between let and in - you will see how this is simply done when we move on to adding objects. In the meantime, we will leave the previous option, where the let-expression is inside the do-block.
Let-expressions are good not only because they are written shorter, but also because they can be used in some special cases. So, for example, let can be inserted into list comprehensions for the same purpose: to define an expression with a limited scope. However, in comparison with where there are also disadvantages: you cannot use guard expressions and pattern matching. Of course, since we specify pseudonyms, then within their scope they should not be repeated.
You ask when we get to the objects? I have already switched. 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 the 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, we already know all this and we are able. A new one begins when we need to define objects for locations. We would like, of course, that everything was ready, the Take and Drop actions worked, there was an inventory, compound objects ... But this does not happen. Now we will make a draft version of the objects in locations where they can neither be taken nor thrown, and on its basis 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 table drawer, a telephone and an umbrella. We set the locationObjects function, which would return a list of objects for this location:
locationObjects :: Location -> [Object]
locationObjects Home = [Umbrella, Drawer, Phone, Table]
locationObjects _ = []
In general, the lists look simple. A list of objects is a type of [Object]. For the Home location, we return a list of four objects. An empty list is indicated by empty square brackets, so long as all the 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 converse is also true. If the list items can be parsed by the read function, then the whole list can also be parsed:
* Main> read "[Table, MailBox]" :: [Object]
[Table, MailBox]
Now that we have understood the syntax, we will make it so that for the current location not only its description is displayed, but also a list of objects that are there is printed. Let's use let-expressions:
run curLoc = do
let locDescr = describeLocation curLoc
let objectsDescr = "\ nThere are some objects here:" ++ show (locationObjects curLoc)
let fullDescr = locDescr ++ objectsDescr
putStrLn fullDescr
... - 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 room at the wooden table.
There are some objects here: [Umbrella, Drawer, Phone, Table]
Enter command:
The code has a little nuisance. Even if there are no objects in the location, the inscription “There are some objects here:” will still be an eyesore to us.
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 put all the code for describing objects into a separate function, 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 pull 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 modify the enumerateObjects function without affecting run. So we wanted to, say, not only enumerate objects, but also display descriptions of them - so we climb into our enumerateObjects with our playful pens and fix something clever there. But somehow later. Now add the custom action Investigate. By this command, the program should give a detailed description of the object. It is clear that the user should enter something like this:
Enter command: Investigate Umbrella
The line “Investigate Umbrella” will have to be parsed. Familiar, right? We already went through this with the Go team. The same thing here: just put an Object parameter in the Investigate constructor.
data Action =
...
| Investigate Object
...
Only a little remained - 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 another user action! Congratulations! You can verify that if you enter the command "Investigate Umbrella", the program will display the string "Nice red mechanic Umbrella."
That's just ... Do you know what's bad with us? If we, when in the Home location, enter the “Investigate MailBox” command, we will be given a description of the mailbox, which is not visible from here at all! Well, we’ll come up with an isVisible function, which will return True if we see an object, and only in this case we will issue a description of the object. What are the arguments to the isVisible function? We need an object to be studied, as well as a list of all location objects. 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 enumerating all the objects in the list, although this is very possible, we just use the standard elem function. It takes two parameters: element and 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
Haskell has a special form of notation in which a function is placed between its arguments - for greater clarity and transparency of meaning. To do this, you need to enclose the function in reverse apostrophes, those that are on the ё button.isVisible' :: Object -> [Object] -> Bool
isVisible' obj objects = obj `elem` objects
А вот «тупые» варианты той же функции. В них мы вручную ищем объект внутри списка.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))
Работают они одинаково, разница лишь в используемых средствах. И там, и там список расщепляется на части: головной элемент и хвост из оставшихся элементов. В первом примере список расщепляется с помощью записи (o:os). Понятно, что переменная «o» содержит голову, а переменная «os» — всё остальное. Во втором примере мы делаем то же самое, только с помощью встроенных функций над списками head и tail. Далее мы просто проверяем, совпадает ли объект с головным элементом, и если нет, вызываем isVisible рекурсивно для оставшихся элементов. Для того, чтобы рекурсия не была бесконечной, мы добавили вариант функции «isVisible obj [] = False», — он сработает, если вдруг мы откусим от списка все элементы и ничего не останется.
Well, all that remains is to use this feature. As usual, we will change run, and in order not to request objects of the current location several times, we will put them in 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 ..."
... - rest of the code
That's all. We will stop on this major note - it’s time to rest, we already got a huge cart of knowledge.
Assignments for fixing.
1. Transfer all functions associated with objects to the Objects module, and all functions associated with 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 room at 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 something like this:...
Investigate obj -> do
putStrLn (investigate obj locObjects)
run curLoc
...
Sources for this part .
The table of contents, references and additional information can be found in the Greetings .