Haskell Quest Tutorial - Поляна

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