📜 ⬆️ ⬇️

Haskell Quest Tutorial - Forest

Forest
This is a forest with trees in all directions. To the east, there appears to be sunlight.
You hear in the distance the chirping of song bird.


Content:
Greeting
Part 1 - The Threshold
Part 2 - Forest
Part 3 - Polyana
Part 4 - View of the canyon
Part 5 - Hall

Part 2,
in which we will torture the describeLocation function, and even find out what ADT is.
')
It is time to think better about the game. What will it be? A classic adventure game, where you can go somewhere, find and use objects, interact with non-player characters? Or will it be a rogue-like text game with magic, evil creatures, with lots of weapons, armor, scrolls, swords and bows? Or, perhaps, we want to create quests a la "Space Rangers-2"? Well, in terms of game mechanics, we will follow in the footsteps of Zork, and choose another story - the wonderful Lighthouse NF quest. Just because I like him.

Erase everything that you have written in the file QuestMain.hs. If you feel sorry for washing, then leave. Or use the version control system (git, svn): I assure you, the fear that you accidentally break the code will disappear forever! Any version of the code can be seen and restored whenever you want. Refactoring Haskell programs is a pleasure in and of itself, and with a version control system, it's not at all burdensome. Yes, refactoring can also be enjoyable! You rule, rule Haskell code so that it finally compiles, and when it compiles, it starts working! That part of the mistakes that you would make in an imperative language is simply impossible here. There are, of course, logic errors, but it is not hard to find and fix, but with the version control system it is even easier. In addition, logs with hundreds of other edits are a good example of how you did a good job, and the visible results from the path traveled motivate you to work further.


Last time, we came up with a function that gives the location description by its number:

describeLocation locNumber = case locNumber of
1 -> "You are standing in the middle of the wooden table."
2 -> "You are standing behind a small wooden fence."
otherwise -> "Unknown location."


What type can this function have? Let's argue. It takes an integer (Integer) and returns a string (String), so the type should be like this:

describeLocation :: Integer -> String


Well, this is practically the case - except for the fact that until we explicitly specify an Integer, the compiler will think that locNumber is a parameter of a more general numeric type, Num, which also includes floating-point numbers. We can pass such a number - there will be no error.

* Main > describeLocation 2.0
“You’re behind the small wooden fence.”
* Main > describeLocation 2.6
“Unknown location.”


Until we explicitly specify the type, let's see what the compiler thinks about it:

* Main > : type describeLocation
describeLocation :: Num a => a -> [ Char ]


Hmm, the “Num a => a -> [Char]” post is mysterious and frightening. Let her We have no need for these difficulties yet. Add a function definition in front of the function itself and define explicitly Integer, String:

describeLocation :: Integer -> String
describeLocation locNumber = case locNumber of
1 -> "You are standing in the middle of the wooden table."
2 -> "You are standing behind a small wooden fence."
otherwise -> "Unknown location."


Check:

* Main > : t describeLocation
describeLocation :: Integer -> String


ABOUT! That's better. But, unfortunately, we now cannot pass a floating-point number as an argument:

* Main > describeLocation 2
“You’re behind the small wooden fence.”
* Main > describeLocation 2.0

< interactive > : 1 : 18 :
No instance for ( Fractional Integer )
arising from the literal ' 2.0 '
Possible fix: add an instance declaration for ( Fractional Integer )
...


We have limited the type of the first parameter from more general (Num) to more private (Integer), clarified. Function definitions are optional, but the code is clearer with them. In Haskell, one of the good tone rules is to define each function; sometimes it is enough to understand how the function should work.

What happens: we take the first argument (locNumber) and match the type to it in the first position (Integer). We have no second parameter, so the type at the second position is the type of the return value (String). Remember the "prod xy" function? What type would she have? It could be, for example, like this:

prod :: float -> float -> float
prod x y = x * y


Got an idea? .. The first Float is the type for x, the second Float is the type for y, and the last Float is the type of the result. That is, in fact, all shamanism.

The library documentation primarily provides a definition of the types of functions and a brief description of them. It may appear that types have no other tasks anymore; However, it is not. Types are the main data description objects; this is a higher abstraction over data. Using types, you can construct data structures of any complexity, create abstract types, set the behavior of code, check its correctness, plan future algorithms, influence their performance and semantics. If code is a program behavior, then data types are the content and structure of the program. And the data is the filling of the program. As we shall see, Haskell has a wonderful type system that is not only deep and expressive, but also supported by a powerful mathematical apparatus. It is convenient to work with types in Haskell, because they are based on several basic constructs that complement each other well.


Here we should think about how we will distinguish the locations. The number is not a very clear notation, it would be better if it was something mnemonic. Maybe a string as a name? Let's try:

describeLocation :: String -> String
describeLocation locName = case locName of
"Home" -> "You are standing in the middle of the wooden table."
"Friend's yard" -> "You’re behind the small wooden fence."
otherwise -> "Unknown location."

* Main > describeLocation "Home"
"You are standing in the middle of the room at the wooden table."


... Do you like Caps Lock? And if it turns on EXTREMELY, and you notice it, already typing a couple of words? Just imagine, you have - hysterical Caps Lock. You wanted to dial "Home", and received "hOmE". Then the function describeLocation will not understand you, although it will work. This is a very unpleasant and subtle error if there is a lot of code.

* Main > describeLocation "hOmE"
“Unknown location.”


To insure against hysterical Caps Lock, you can think of a function that translates a word into upper case. Alternatives in the case-design should also be written in capital letters.

upperCaseString :: String -> String
upperCaseString str = ............ - Somehow we make all the letters BIG.
describeLocation :: String -> String
describeLocation locName = case ( upperCaseString locName ) of
"HOME" -> "You are standing in the middle of the wooden table."
"FRIEND'S YARD" -> "You are standing behind a small wooden fence."
otherwise -> "Unknown location."


Now hysterical Caps Lock is not terrible:

* Main > describeLocation "FRieNd'S yard"
“You’re behind the small wooden fence.”
* Main > describeLocation "hOMe"
"You are standing in the middle of the room at the wooden table."


Yeah, I see your curious eyes. Want function upperCaseString? Isn't it too early? Okay. I have nothing to hide. We need some function, "toUpper", in the standard Prelude module it is not. It is from the “Char” module, so it needs to be connected:

import Char - At the beginning of QuestMain.hs we connect the Char module

upperCaseString :: String -> String
upperCaseString str = map toUpper str


I will not explain anything here! They themselves wanted to climb ahead - do it yourself and sort it out!

... Well, well, well, persuaded. In outline. The map function takes two arguments: the toUpper function and our str string. The map's task is simple: apply the toUpper function to each element of the str string. What elements does a string consist of? Correct, from characters. The toUpper function is applied to all these characters, which takes them to uppercase (well, if these are letters, of course).


Another solution is to add constant functions. You will not write them wrong, because the program simply will not compile!

home :: String
home = "HOME"

friend'sYard :: String - The apostrophe character (') can be used inside and at the end as a letter.
friend'sYard = "FRIEND'S YARD"

garden :: String
garden = "GARDEN"

* Main > describeLocation home
"You are standing in the middle of the room at the wooden table."
* Main > describeLocation friend'sYard
“You’re behind the small wooden fence.”
* Main > describeLocation garden
“Unknown location.”


But think: how many more functions will there be in which it is necessary to distinguish locations? The function of traveling from one location to another, the Look command (“Look around”), some actions with objects in this location ... Each time, building letters in upper case is inconvenient, unattractive, costly. There must be some other way to define locations, to identify them.

It is fair to say that when we call locations as strings, we introduce some dynamics into the static code. At later stages of development, it may suddenly turn out that this was, in general, a good idea, since locations can be added and added without almost changing the code. But then the describeLocation function would have a different look, and in general there would be a different philosophy of working with locations. Here's how to add a new location without the describeLocation edit:

home :: String
home = "You are standing in the middle of the middle of the wooden table."

friend'sYard :: String
friend'sYard = "This is a wooden wooden fence."

garden :: String
garden = "You are in the garden. Garden looks very well: clean, tonsured, cool and wet. "

describeLocation :: String -> String
describeLocation location = location

* Main > describeLocation garden
“You are in the garden. Garden looks very well: clean, tonsured, cool and wet. "


That is, we pass a constant function with a location description to describeLocation. The function describeLocation returns it. In this case, the case-design is no longer needed, and we can produce at least a thousand constant functions. By the way, notice that our constant functions are no different from just strings. Since in Haskell every expression is a function, and the string is an expression, the string is also a function. We simply assigned a name to the string and got a constant function. (We can assume that the “pi” function is also a constant function, something like this: pi = 3.1415 ....)

* Main > friend'sYard == "You’re behind the small wooden fence."
True
* Main > : t friend'sYard
friend'sYard :: [ Char ]
* Main > : t "You’re behind the small wooden fence."
“You’re behind the small wooden fence.” :: [ Char ]


Here, [Char] is the same as String. Literally, [Char] is a list of characters, a synonym for String. We could replace String with [Char] or even mix both types in one program, there would be no error. But it is more convenient to write a "string" than a "list of characters." We ourselves will set synonyms for many of our types. So, in my adv2game ObjectName is defined, also a string (also a list of characters). Looking at ObjectName, I understand that this is not just a string, but in it, in theory, should be the name of the object. Synonyms are defined by the type keyword, which works in the same way as typedef in C ++:

type ObjectName = String


And this is how the String type is specified in the Prelude module:

type String = [ Char ]


The square brackets say that this is a list from Char. Say “STRING” is the same as the list of characters: ['S', 'T', 'R', 'I', 'N', 'G']. It’s just that no one in their right mind would write a string like this, because in Haskell there is a simplification for the list of characters - “strings in quotes”.

* Main > [ 'S' , 'T' , 'R' , 'I' , 'N' , 'G' ]
"STRING"
* Main > “I am a” ++ [ 'S' , 'T' , 'R' , 'I' , 'N' , 'G' ] ++ "."
"I am a STRING."
* Main > putStrLn [ 'S' , 'T' , 'R' , '\ n' , 'I' , 'N' , 'G' ]
STR
Ing
* Main > [ 'S' , 'T' , 'R' , 'I' , 'N' , 'G' ] == "STRING"
True


You can specify lists of anything: a list of integers [Integer], a list of strings [String] (which expands to a list of Char lists), and so on. Lists are the main structure in the FN, and we will learn a lot about them.


Do we want to somehow distinguish locations or not? Only not by a string name, it’s so easy to make a mistake. I wish there was something permanent instead of a string. Here is:

describeLocation :: ????? -> String
describeLocation loc = case loc of
Home -> "You are standing in the middle of the wooden table."
Friend'sYard -> “You ’ve been behind a small wooden fence.”
Garden -> “You are in the garden. Garden looks very well: clean, tonsured, cool and wet. "
otherwise -> "Unknown location."


Obviously, instead of question marks, there should be a PROFIT of some type in which there is Home, Friend'sYard, Garden. For such cases, Haskell implemented the so-called algebraic data types (ADT). With their help, you can intuitively describe the data of a completely different structure. Algebraic data types replace enumerations, unions, objects in OOP-languages ​​of any complexity. Through ATD, ATD (abstract data types, which are also “ATD”) are expressed, and through ADT, ATD itself is expressed (recursively). In addition, they can make lists, trees, sets, and much more. Moreover, ADT is not a specific language tool, but an element of the mathematical type theory, thanks to which the compiler itself infers types, and also checks the code for correctness during compilation.

At the beginning of the QuestMain.hs file, specify the type for the locations:

data Location = Home | Friend'sYard | Garden


Using the data keyword, we create a new algebraic data type. A location is a type, and all that follows the equal sign is constructors. We can assume that they are just values, that's all. All constructors must begin with a capital letter, as is customary in Haskell. This distinguishes them from functions in which the first letter is necessarily small. To make it clearer, you can rewrite it differently, the position of the elements and indents do not matter:

data Location =
Home
| Friend'sYard
| Garden


The sign "|" can be read as “or.” That is, Location type variables can take one value: either Home, or Friend'sYard, or Garden. Calling Location type constructors, we create a variable of this type. It is important to understand that by calling any of the constructors, we are still dealing with the Location type, and there are no such types as “Home” or “Garden”.

Substitute a new type instead of question marks:

describeLocation :: Location -> String
describeLocation loc = case loc of
Home -> "You are standing in the middle of the wooden table."
Friend'sYard -> “You ’ve been behind a small wooden fence.”
Garden -> “You are in the garden. Garden looks very well: clean, tonsured, cool and wet. "
otherwise -> "Unknown location."

* Main > describeLocation Home
"You are standing in the middle of the room at the wooden table."
* Main > describeLocation Friend'sYard
“You’re behind the small wooden fence.”


Errors are now excluded. It is also not necessary to translate the loc parameter to uppercase, and the constant functions seem to be useless. You either write the constructor correctly, and the program is compiled, or it is incorrect, and then you get an error. You can verify this by adding these three functions:

describeHomeLocation = describeLocation Home
describeGardenLocation = describeLocation garDEN
describeGardenLocation ' = describeLocation GarDEN

* Main > : r
[ 1 of 1 ] Compiling Main ( H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . Hs , interpreted )

H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 15 : 43 :
Not in scope: 'garDEN'

H: \ Haskell \ QuestTutorial \ Quest \ QuestMain . hs: 16 : 44 :
Not in scope: data constructor 'GarDEN'
Failed , modules loaded: none .


Now erase this erroneous code! .. We do not have any “garDEN” or “GarDEN”! But you can add a GarDEN constructor if you want. What it will mean is your business. And it will be correct: GarDEN and Garden are different designers, because the register matters. By the way, nobody forbids you to make a constructor with the same name as the type; it is useful, but not in our case:

data Location =
Location - This is correct, although it is not clear why.
| Home
| Friend'sYard
| Garden


There will be no errors, because the Haskell smart compiler knows where Location should be understood as a type, and where - as a constructor. And these places do not overlap. ("Types and their constructors are in different namespaces.")

Let's fantasize for the future, what other types we will have.

- Where to go with the Walk or Go team.
data Direction = North | South | West | East

- Player's actions.
data Action = Look | Go | Inventory | Take | Drop | Investigate | Quit | Save | Load | New


What can we do with these types? Well, not so much yet. We cannot even compare constructors of the same type:

* Main > North == North

< interactive > : 1 : 7 :
No instance for ( Eq Direction )
arising from a use of ' == '
Possible fix: add an instance declaration for ( Eq Direction )
In the expression: North == North
In this equation for 'it': it = North == North


The interpreter complains that it is not written anywhere in our country how to compare constructors of the Direction type. Like, you want the operation "=="? Then add your type to the family of compared types!

More specifically, it asks you to add the Direction type to the Eq type class. Eq is a class of types for which the operations "==" and "/ =" are defined.

And now forget what is written in this box. It’s too early for us to talk about type classes .


There are several ways to make our designers comparable. The simplest of these is to add a couple of magic words to the type definition:

data direction =
North
| South
| West
| East
deriving ( Eq ) - There should be indentation from the left edge.


Magic words you see. Literally, this means that we borrow (inherit) the comparison operation, which is the default. It lies in the class of types Eq. And this operation will work for our designers in a quite expected way:

* Main > North == North
True
* Main > North / = North
False
* Main > North == South
False


That's good! We now have signs "equal" and "not equal." Do the same with our other types - Action and Direction. Useful!

deriving (Eq) is one of the magic options with which we can compare constructors. In general, there are a lot of magic options for data, and they make something more out of your type. But let's leave miracles to the next part. It's enough for today. We have already tormented with this describeLocation function. In the next part we will learn about ADT something else.

Task for fixing:

Think of the walk function - the function of travel between locations. It takes two parameters: the current location (Location) and direction (Direction), and returns a new location located in this direction. The route map for each location is as follows:

Home:
to the north - Garden
South - Friend'sYard
to the east - Home
to the west - Home

Garden:
to the north - Friend'sYard
to the south - Home
to the east - Garden
to the west - Garden

Friend'sYard:
to the north - Home
to the south - Garden
to the east - Friend'sYard
to the west - Friend'sYard


Sources to this part .

Table of contents, list of references and additional information can be found in the "Welcome"

Source: https://habr.com/ru/post/121189/


All Articles