Clearing
You are in a small clearing in a well marked forest path that extends to the east and west.
Зміст:
Вітання
Частина 1 - Переддень
Частина 2 - Ліс
Частина 3 - Поляна
Частина 4 - Вид каньйону
Частина 5 - Зала
Частина 3,
в якій ми станемо вчитися чарівності з АТД і пізнаємо магічні перетворювачі Show і Read.
У минулій частині ми винаходили різні варіанти describeLocation, а в кінці створили три алгебраїчні типи - Location, Direction, Action. Я обмовився про чарівність і дивні можливості АТД, але сказав, що ми розглянемо їх пізніше. Ми тільки успадкували наші типи від класу типів Eq, в якому лежать операції "= =" "і" "/= "", а тепер...
Хочете чудес? Ну що ж... Подивимося ще раз на тип Location:
data Location =
Home
| Friend'sYard
| Garden
| OtherRoom - Додано новий конструктор.
deriving (Eq)
*Main> Home /= Friend'sYard
True
*Main> Home == OtherRoom
False
Дуже добре! У першій частині ми дізналися, що є функція show, яка переводить щось у рядок. Спробуємо:
*Main> show Home
<interactive>:1:1:
No instance for (Show Location)
arising from a use of 'show'
Possible fix: add an instance declaration for (Show Location)
In the expression: show Home
In an equation for 'it': it = show Home
Не вийшло... Ми з вами вже стикалися з подібною помилкою в кінці другої частини. Там ми намагалися порівняти два конструктори, але нічого не вийшло, тому що ghci не знав, як їх порівнювати. Ми вирішили проблему, додавши в кінці типу Location заклинання «deriving (Eq)», - і отримали «фабричну» функцію порівняння «» = = «». Чи можемо ми зробити щось подібне, щоб отримати функцію show? Можемо! Достатньо успадкувати клас типів Show:
data Location =
Home
| Friend'sYard
| Garden
| OtherRoom
deriving (Eq, Show)
*Main> :r
[1 of 1] Compiling Main ( H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted )
Ok, modules loaded: Main.
*Main> show Home
«Home»
*Main> «Current location name: » ++ show Home ++ ""."
«Current location name: Home.»
*Main> show Friend'sYard
«Friend'sYard»
Як це можна використовувати? О, найрізноманітнішими способами. Давайте зараз покращимо функцію describeLocation. Перепишемо останню альтернативу («otherwise»):
describeLocation :: Location -> String
describeLocation loc = case loc of
Home -> «You are standing in the middle room at the wooden table.»
Friend'sYard -> «You are standing in the front of the night garden behind the small wooden fence.»
Garden -> «You are in the garden. Garden looks very well: clean, tonsured, cool and wet.»
otherwise -> «No description available for location with name » ++ show loc ++ ""."
А тепер, не вдаючись до допомоги ghci, скажіть мені: що буде, якщо викликати «describeLocation OtherRoom»? Прослідкуйте, куди потрапить конструктор OtherRoom, як спрацює case, який з варіантів вибереться, і що за рядок цей варіант поверне. Готово? Перевірте себе:
*Main> describeLocation OtherRoom
«No description available for location with name OtherRoom.»
У мене, на жаль, немає для вас пиріжків; але якщо ви правильно здогадалися, можете пишатися собою. Тільки що ви взяли функцію show з класу типів Show і перетворили конструктор на рядок. Гарно? По-моєму, так. Спробуйте, наприклад, у C++ так само легко перетворити на рядок елемент якого-небудь перерахування...
Функція show дуже корисна. Успадкуйте від класу типів Show типи Action і Direction. Обіцяю, не прогадаєте!
Конструктори типів, такі як Home, Friend'sYard або Garden, насправді, є особливими функціями, яким дозволено починатися з заголовної букви. А раз це функції, то у них є тип. Що видасть команда «»:type Home""? Це ж елементарно, Ватсон.
*Main> :type Home
Home :: Location
Знаєте, мене тут щось не влаштовує. Подивіться на цитати з Zork на початку кожної з частин: там спочатку виводиться назва локації, а потім - з нового рядка - опис. Давайте перепишемо функцію describeLocation... Так-так, знову її, не стогніть так!.. Я хочу, щоб назва локації виводилася перед її описом. Рішення «в лоб»: я просто впровадив назву локації в текстовий рядок.
describeLocation loc = case loc of
Home -> «Home\nYou are standing in the middle room at the wooden table.»
Friend'sYard -> «Friend'sYard\nYou are standing in the front of the night garden behind the small wooden fence.»
Garden -> «Garden\nYou are in the garden. Garden looks very well: clean, tonsured, cool and wet.»
otherwise -> «No description available for location with name » ++ show loc ++ ""."
Працювати, звичайно, буде. Якщо вам хочеться забруднювати описи, то будь ласка. Мені не хочеться. Варіант номер два:
describeLocation loc = case loc of
Home -> show loc ++ "\n" ++ «You are standing in the middle room at the wooden table.»
Friend'sYard -> show loc ++ "\n" ++ «You are standing in the front of the night garden behind the small wooden fence.»
Garden -> show loc ++ "\n" ++ «You are in the garden. Garden looks very well: clean, tonsured, cool and wet.»
otherwise -> «No description available for location with name » ++ show loc ++ ""."
Вже краще, хоча додається багато роботи з усіма цими плюсиками... І повторюватися - поганий тон... Є більш простий і елегантний спосіб! Стежте за руками:
describeLocation loc = show loc ++ "\n" ++
case loc of
Home -> «You are standing in the middle room at the wooden table.»
Friend'sYard -> «You are standing in the front of the night garden behind the small wooden fence.»
Garden -> «You are in the garden. Garden looks very well: clean, tonsured, cool and wet.»
otherwise -> «No description available for location with name » ++ show loc ++ ""."
Фокус в тому, що case-конструкція - це один великий вираз, від слова «case» і до кінця останньої альтернативи. Ми можемо впроваджувати case всередину інших виразів. У нашому випадку case завжди повертає рядок, значить, ми можемо додати його до іншого рядка. Код стає читабельнішим, стрункішим і красивішим. Якщо ви протестуєте всі три варіанти, то побачите, що вони видають те, що треба.
*Main> describeLocation Home
«Home\nYou are standing in the middle room at the wooden table.»
*Main> putStrLn (describeLocation Home)
Home
You are standing in the middle room at the wooden table.
case-конструкція, безумовно, хороша. Є, однак, випадки, коли вона незручна. Якщо ви вирішували завдання № 2 з першої частини, ви вже здогадуєтеся, про що я. Нагадаю, що там потрібно було реалізувати наступну функцію для деяких x і a:
| ln (abs (sin (x))), якщо x > 5
y = | x ^ 2 + a ^ 2, якщо x < = 5 і a < = 3
| x/a + 7.8 * a, якщо x < = 5 і a > 3
Функція як функція, в математиці таких темрява. Але спробуйте реалізувати її на Haskell за допомогою if-then-else або case:
y x a = if x > 5
then log (abs (sin(x) ) )
else
if x <= 5 && a <= 3
then x^2 + a^2
else x / a + 7.8*a
y' x a = case x > 5 of
True -> log (abs (sin(x) ) )
False -> case x <= 5 && a <= 3 of
True -> x^2 + a^2
False -> x / a + 7.8*a
Функцію важко читати через кілька рівнів вкладеності. Невже по-іншому не можна?.. Ну як же! Охоронні вирази! І Haskell-функція стає схожою на функцію в математиці. Дивіться:
y'' x a | x > 5 = log (abs (sin(x) ) )
y'' x a | x <= 5 && a <= 3 = x^2 + a^2
y'' x a | otherwise = x / a + 7.8*a
- Або те ж саме:
y'' x a | x > 5 = log (abs (sin(x) ) )
| x <= 5 && a <= 3 = x^2 + a^2
| otherwise = x / a + 7.8*a
Легко зрозуміти, що функція приймає вигляд «log (abs (sin (x))» якщо x буде більше п'яти. Охоронний вираз (guard) - це вираз між знаками «» | «» і «» = «». Для охоронних виразів діють ті ж закони, що і для альтернатив case-конструкції: набір виразів повинен бути повним, а otherwise завжди спрацьовує.
Але давайте повернемося до проектування гри. У будь-якій грі є код, де знову і знову викликаються обробники подій, розраховуються графіка, фізика, ШІ. У нас гра простіше. Користувач вводить команду з клавіатури, - і щось відбувається, потім він знову вводить команду, і знову щось відбувається, і так далі. Буде приблизно такий алгоритм:
0. Пояснюємо ігрову обстановку:
- виводимо опис поточної локації;
- виводимо опис об'єктів у локації.
1. Чекаємо команду від гравця у вигляді рядка.
2. Намагаємося розпізнати команду.
3а. Якщо команду розпізнано:
- виконуємо її;
- повертаємося до пункту 0.
3б. Якщо команду не розпізнано:
- видаємо повідомлення про це;
- повертаємося до пункту 1.
Половина пункту 0 вже готова: це функція «describeLocation». Об'єктів поки у нас немає, ми їх додамо пізніше. Отже, переходимо до пункту 1. Як отримати введення з клавіатури? У першій частині я розповів про функцію putStrLn, яка друкує рядок в реальній консолі; час познайомитися з протилежною функцією - getLine. Розгляньмо наступне заклинання:
run =
do
x <- getLine
putStrLn x
Саме час прокачати навички «Грамотність» і «Орлине око»! Що відбувається у функції run? Декілька простих дій. Ми чекаємо рядок з клавіатури (getLine); цей рядок пов'язуємо з x; друкуємо x у реальній консолі. І щоб зв'язати дії в ланцюжок, використовується ключове слово «do» - така ось особливість мови Haskell. А тепер випробуємо:
*Main> run
Hello!
Hello! -- Те, що надрукувала функція putStrLn
*Main> run
kjljfs
kjljfs
Ще раз: функція getLine просить у на рядок. Рядок зв'язується з змінною x, а на наступному кроці функція putStrLn друкує x. Давайте внесемо ясність, додавши перед введенням рядка запрошення "Enter command: ». Нехай користувач бачить, що від нього хочуть.
run = do
putStr «Enter command: »
x <- getLine
putStrLn x
*Main> run
Enter command: Look
Look
Я використовував функцію putStr: вона щось друкує, але курсор на новий рядок не переводить. Взагалі, тут повна аналогія з Pascal: writeLn <=> putStrLn, write <=> putStr.
Ви, звичайно, помітили, що я написав «пов'язуємо з x», а не «привласнюємо x». У Haskell привласнення немає, тому-то і стоїть там стрілка («» < - «»), а не знак присвоєння («» = «», «»:=""). Стрілка показує, звідки ми беремо результат і з чим його пов'язуємо. Між присвоєнням і зв'язуванням є суттєва різниця з далекосяжними слідствами. Але доки ми не використовуємо ці слідства, то й переживати не варто.
Тепер нам потрібно виконати команду, введену користувачем. Для цього придумаємо просту функцію «evalAction» і викличемо її з run:
evalAction :: String -> String
evalAction strAct = «Action: » ++ strAct ++ ""!"
run = do
putStr «Enter command: »
x <- getLine
putStrLn (evalAction x)
- Тестуємо:
*Main> run
Enter command: Look
«Action: Look!»
*Main> run
Enter command: Quit
«Action: Quit!»
Хо-хо! Наша заготовка, без сумнівів, працює! Тільки evalAction приймає рядок, а не спеціальний тип Action. Через це ми можемо передати в функцію будь-яку абракадабру.
*Main> run
Enter command: lubaya_abrakadabra
«Action: lubaya_abrakadabra!»
Нас вводять в оману. Такого Action, як lubaya_abrakadabra, немає... Ми вже якось провернули трюк із заміною рядка на Location у функції describeLocation. Що якщо повторимо його тут? Заміна рядка на Action:
evalAction :: Action -> String
evalAction act = «Action: » ++ show act ++ ""!"
run = do
putStr «Enter command: »
x <- getLine
putStrLn (evalAction x)
Здається, evalAction виглядає добре: абракадабру не передаси в принципі, а конструктор буде оброблений будь-хто, хоч і таким примітивним чином. Але цей код трохи проблемний: він не скомпілюється.
*Main> :r
[1 of 1] Compiling Main ( H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted )
H:\Haskell\QuestTutorial\Quest\QuestMain.hs:46:38:
Couldn't match expected type 'Action' with actual type '[Char]'
Expected type: Action
Actual type: String
In the first argument of 'evalAction', namely 'x'
In the first argument of 'putStrLn', namely '(evalAction x)'
Failed, modules loaded: none.
GHCi нам каже, що не збігаються типи. Функція evalAction хоче тип Action, а зовсім не String. Ми помилилися тут: «putStrLn (evalAction x)». Хех... Адже така ідея була хороша!..
Програмуючи на Haskell, ви часто будете бачити помилки типізації. Нічого поганого в цьому немає; у них написано, в якому місці нестиковка, що очікувалося отримати (Expected type), і що отримали насправді (Actual type). Скомпілювати неправильний код не можна. При великому рефакторингу може виникнути до декількох десятків помилок, а то й більше, - і доведеться їх все виправити, одну, іншу, третю... Коли нарешті помилки зникнуть, код з високою ймовірністю запрацює саме так, як ви очікуєте. І це, скажу я вам, дуже-дуже здорово!
Щоб отримати конструктор типу Action з рядка «x», існує декілька рішень. Для початку спробуємо придумати функцію convertStringToAction. Питання на «трійку»: яким буде тип функції, яка перетворює String на Action? Це ж очевидно!
convertStringToAction :: String -> Action
Найпростіший спосіб - використовувати case. В останній альтернативі ми перестрахуємося і повернемо Quit, якщо раптом чого.
convertStringToAction :: String -> Action
convertStringToAction str = case str of
«Look» -> Look
«New» -> New
otherwise -> Quit
Найкраще її вставити при виклику evalAction у функції run. Ось так:
-- Обробляємо дію.
evalAction :: Action -> String
evalAction act = «Action: » ++ show act ++ ""!"
-- Перетворюємо рядок на Action
convertStringToAction :: String -> Action
convertStringToAction str = case str of
«Look» -> Look
«New» -> New
otherwise -> Quit
- Отримуємо введення з клавіатури, конвертуємо його в дію, викликаємо обробник, виводимо результат.
run = do
putStr «Enter command: »
x <- getLine
putStrLn ( evalAction (convertStringToAction x) )
А тепер перевіримо:
*Main> :r
[1 of 1] Compiling Main ( H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted )
Ok, modules loaded: Main.
*Main> run
Enter command: Look
Action: Look!
*Main> run
Enter command: dfdf
Action: Quit!
Що ж, це перемога! Тепер функція evalAction працює не з рядком, а з Action. Все б добре, але... Ви бачите, скільки роботи належить, коли ми захочемо додати ще якусь команду крім Look? У нас їх у типі цілих десять: Look, Go, Inventory, Take, Drop, Investigate, Quit, Save, Load, New, - та й інші можуть з'явитися. І що ж, знову і знову розширювати case-конструкцію у функції convertStringToAction? Не дуже-то хочеться...
До речі, їжа для роздумів: ще два способи записати функцію convertStringToAction. Тезами, без пояснень.
-- Охоронні вирази (guards)
convertStringToAction' :: String -> Action
convertStringToAction' str | str == «Look» = Look
| str