Haskell Quest Tutorial - Переддень

West of House


You are standing in an open field west of a white house, with a boarded front door.

There is a small mailbox here.

> open mailbox

Opening the small mailbox reveals a leaflet.

> read leaflet

(Taken)

«WELCOME TO ZORK!

ZORK is a game of adventure, danger, and low cunning. In it you will explore some of the most amazing territory ever by mortals. No computer should be without one!»

Зміст:

Вітання

Частина 1 - Переддень

Частина 2 - Ліс

Частина 3 - Поляна

Частина 4 - Вид каньйону

Частина 5 - Зала

Частина 1,

в якій ми познайомимося з не всіма основами мови Haskell і напишемо одну корисну для квесту функцію.

Отже, ви стоїте на самому початку, перед зачиненими дверима і бачите поштову скриньку.

Для програмування на Haskell потрібно небагато. Насамперед - бажання. Можливо, знадобляться також інтерпретатор і компілятор. Ми будемо використовувати компілятор GHC (Glasgow Haskell Compiller) у складі Haskell Platform під Windows, просто тому, що це зручно. (Якщо у вас Linux, вам поради не потрібні, - ви краще за мене знаєте, як зробити так, щоб все працювало.) Встановлення тривіальне: скачуєте і встановлюєте. Після встановлення виконайте команду ghci. Якщо її не знайдено, додайте теку «» ...\Haskell Platform\x.y.z.0\bin\« »в PATH. Код мовою Haskell зберігається у файлах «» * .hs «». Його можна писати в HugIDE або в Notepad++ (як це роблю я). Після встановлення Haskell Platform файли * .hs будуть асоційовані з інтерпретатором GHCi - і це дуже зручно, як ви побачите далі. Ще можна завантажити величезний репозиторій всякого добра Hackage, і там ви знайдете простеньку гру advgame. Вона була прообразом і орієнтиром для мене в написанні своєї.

Створіть теку для вашого квесту. Створіть порожній файл QuestMain.hs і запустіть його. Ви побачите, гм, консоль GHCi-інтерпретатора, який буде нам допомагати в налагодженні. Ви побачите, що GHCi завантажив якісь бібліотеки, успішно скомпілював 1 файл з 1 і сказав: «Ok, modules loaded: Main.» Ви можете погратися: у запрошенні командного рядка "* Main >" "ввести будь-який математичний вираз. Ні-ні, ми не будемо писати програму в нім. Зараз ми трохи повивчаємо цей інструмент, щоб потім було простіше налагоджувати програму.

*Main> 4

4

*Main> 2+4-7

-1

*Main> 9 / (2*5.0)

0.9

*Main> (-1)+4

3

*Main> 7 == 7

True

*Main> -5 > 0

False

Вам так само доступні математичні функції: sin x, cos x, tan x, atan x, abs x.

*Main> sin 10 * sin 10 + cos 10 * cos 10

1.0

*Main> sin (2*cos 10) + tan 5

-4.374758876543559

Haskell - чутлива до регістру мова (як С++, Java). Я знаю, що ви завжди хотіли функцію «cOS», але її немає, змиріться! Є тільки «cos».

*Main> cos (cos (cos 0.5))

0.8026751006823349

*Main> cos pi

-1.0

Це все очікувано і зрозуміло. pi - функція, що повертає число pi, у неї немає аргументів. Тригонометричні функції отримують один аргумент. А як щодо функцій з кількома аргументами? Синтаксис простий: спочатку назва функції, потім аргументи. Без будь-яких дужок, комів і крапок з комами.

*Main> logBase 2 2

1.0

*Main> logBase (sin 2) 2

-7.28991425837762

У другому прикладі важливо обернути sin 2 в дужки, щоб це був аргумент № 1 функції logBase. Якщо цього не зробити, інтерпретатор подумає, що ми передали у функцію logBase три аргументи (sin, 2, 2) замість двох, і посвариться:

*Main> logBase sin 2 2

<interactive>:1:1:

No instance for (Floating (a0 -> a0))

arising from a use of 'logBase'

Possible fix: add an instance declaration for (Floating (a0 -> a0))

In the expression: logBase sin 2 2

In an equation for 'it': it = logBase sin 2 2

..................

Чого він нам повідомляє, ми поки не будемо вдумуватися. Не царська ця справа, у нас є і більш важливі справи.

Інші функції, зокрема математичні, можна знайти в супровідній документації, яка є в Haskell Platform («GHC Library Documentation»). За замовчуванням доступні всі функції, які є в додатку Prelude. Ви цей модуль не завантажували, він підвантажився сам. sin, cos та інші - визначені в нім. Prelude містить ще цілу купу корисних функцій, вони використовуються найчастіше, тому і зібрані разом. Подивіться документацію по Prelude, і ви побачите щось незвичайне, якісь дивні конструкції на зразок цієї:

words :: String -> [String]

або навіть цієї:

Eq a => Eq (Maybe a)

Не зрозуміло? Нічого, ще розберемося.

Хоча чого там, давайте ще пограємося, тільки тепер з рядками. Рядки Haskell виглядають так само, як і в Сі: символи всередині подвійних лапок. Спеціальний символ\n («Новий рядок») також працює.

*Main> «Hello, world!»

«Hello world!»

*Main> «Hello, \nworld!»

«Hello, \nworld!»

Що, не спрацював?? А. Коли ми пишемо в ghci рядок, він його просто повторює. Точно так само він буде повторювати одне число або True:

*Main> 1000000

1000000

*Main> True

True

Це - зневаджувальний висновок ghci, назвемо його так. Ні рядок, ні число ще на реальну консоль не були відправлені. Давайте відправимо рядок на друк у реальній консолі:

*Main> putStrLn «Hello, \nworld!»

Hello,

world!

Ну ось, функція putStrLn прийняла рядок і надрукувала його на реальній консолі. ghci відтворив для нас результат. Запишіть: putStrLn, приймає рядок, виводить його на екран. Два рядки можна вивести, з'єднавши їх операцією "+ +" ":

*Main> putStrLn («Hello, world!» ++ "\nHere we go!")

Hello, world!

Here we go!

Дужки потрібні, щоб ghci зрозумів нас правильно. Без дужок він подумає буквально наступне:

putStrLn «Hello, world!» ++ ""\nHere we go!"" <=> (putStrLn «Hello, world!») ++ (""\nHere we go!"")

І це засідка, тому що ми намагаємося до виведення на консоль строки1 додати строку2. Як ми можемо додати рядок до висновку?? Ось яка буде при цьому лайка:

*Main> putStrLn «Hello, world!» ++ "\nHere we go!"

<interactive>:1:1:

Couldn't match expected type [a0] with actual type 'IO ()'

In the return type of a call of 'putStrLn'

In the first argument '(++)', namely

'putStrLn «Hello, world!»'

In the expression: putStrLn «Hello, world!» ++ "\nHere we go!"

Два висновки на консоль теж не складаються. Ще кілька помилкових варіантів:

*Main> putStrLn «Hello, world!»  ++  putStrLn "\nHere we go!"

*Main> putStrLn «Hello, world!»      putStrLn "\nHere we go!"

*Main> «Hello, world!»  ++  putStrLn "\nHere we go!"

Варіант, коли функції putStrLn немає взагалі, спрацює, але ми отримаємо не «справжній», а зневаджувальний висновок рядка. Ми-то хотіли, щоб частина "\nHere we go! "" була надрукована з нового рядка, а не так:

*Main> «Hello, world!»  ++  "\nHere we go!"

«Hello, world!\nHere we go!»

Спробувавши виконати кожен з помилкових варіантів, ви дізнаєтеся, що думає про вас ghci. Знаємо ми з вами всю дріб'язковість цих інтерпретаторів і компіляторів!.. Їм подавай тільки правильне і акуратне, ніби вони не в нашому світі живуть, а в якомусь своєму, ідеальному.

Ну, взагалі-то, так і є. У мови Haskell свій світ і свої закони. Закони вимагають, щоб типи виразів були правильні. Haskell - дуже строго типізована мова. Функції, параметри, вирази - все має певний тип. Не можна передати параметр у функцію, якщо вона хоче параметр іншого типу. Спробуйте надрукувати число у реальній консолі:

*Main> putStrLn 5

<interactive>:1:10:

No instance for (Num String)

arising from the literal '5'

Possible fix: add an instance declaration for (Num String)

In the first argument of 'putStrLn', namely '5'

In the expression: putStrLn 5

In an equation for 'it': it = putStrLn 5

Ви бачите вже знайому лайку інтерпретатора про щось. Функція putStrLn хоче рядок (тип String), і тільки його, а отримує число. Типи не збігаються, = > виникає конфлікт. Можна зробити так:

*Main> putStrLn (show 5)

5

Функція show, якщо може, перетворює аргумент на рядок, який потім друкується за допомогою putStrLn. Щоб переконатися, що виконуючи функцію (show 5), ви отримуєте рядок «5», введіть щось таке:

*Main> putStrLn («String and » ++ (show 5) ++ "\n - a string again.")

String and 5

- a string again.

Функція show вміє перекладати на рядок багато типів. Вона дуже стане в нагоді в квесті.

*Main> putStrLn («sin^2 5 + cos^2 5 = » ++ show (sin 5 * sin 5 + cos 5 * cos 5))

sin^2 5 + cos^2 5 = 0.999999999999999

Звичайно, putStrLn стане в нагоді не менше. По ідеї, вона нічого не повинна повертати, адже паскалівська процедура writeLn теж нічого не повертає. Але в Haskell функції завжди щось повертають, тому що інакше які б вони були «функції». Переконаємося в цьому? У ghci можна вводити деякі службові команди, і команда «»:t"" ("":type «») показує тип будь-якого виразу:

*Main> :t 3 == 3

3 == 3 :: Bool

*Main> :t 3 /= 3

3 /= 3 :: Bool

*Main> :type 'f'

'f' :: Char

*Main> :type «I am a string»

«I am a string» :: [Char]

Тут можна вважати будь-який вираз особливим видом функції, що, можливо, не приймає параметрів, але обов'язково повертає результат. Тип функції putStrLn виглядає так:

*Main> :t putStrLn

putStrLn :: String -> IO ()

Якщо ви ще не стикалися з цією формою запису (в математиці, в деяких інших функціональних мовах), то, можливо, вам буде трохи незвично, - як колись було мені. Але до хорошого швидко звикаєш, а типи в Haskell дюжі хороші. Настільки гарні, що в переважній більшості випадків нам не потрібно їх вказувати, - Haskell виведе їх сам і навіть погрозить нам пальцем, якщо щось десь не співпаде. Ми з вами ще надивимося на всілякі конструкції з базових типів, і ви теж відчуєте, наскільки це зручно. Не те що в якому-небудь С++, де треба кожну змінну, кожен елемент описати, розписати, зареєструвати...

У даному випадку putStrLn приймає String і повертає IO (). Двокрапка поділяє назву функції і її тип. Правильно читати так: «putStrLn має тип зі String в IO ()». String - це тип нашого вхідного рядка. Тип «IO ()» - невід'ємна частина введення-виведення (Input/Output, як ви здогадалися). Тип «IO ()» показує, що функція putStrLn балується з «нечистою» областю програми. У цій області може статися що завгодно, навіть катастрофа, і ми повинні уважно про це думати. Але поки у нас немає про що думати, і нехай IO () нас не турбує. Ми постараємося писати функції, в яких ніякої катастрофи бути не може, функції, в яких немає місця побічним ефектам.

Вони називаються чистими, детермінованими: такі функції зобов'язані повертати одне і те ж значення для одного і того ж аргументу. Мова Haskell є чистою функціональною мовою саме завдяки цій концепції. Тут, звичайно, виникає питання, а що робити з функціями, які при різних викликах можуть дати різний результат (генератори псевдопромінювальних чисел, наприклад). І як змінювати дані? Як працювати з пам'яттю? Як читати ввід з клавіатури? Адже все це веде до недетермінованості. Ну, в Haskell є особливі механізми («монади»), за допомогою яких ці та інші проблеми витончено вирішуються. Ми ще повернемося до цієї розмови в майбутньому.

Отже, відкрийте текстовий редактор QuestMain.hs. Поки там порожньо, - але це тільки початок, переддень. Скоро тут будуть плавати русалки і літати дракони. А поки напишіть просту функцію, що обчислює твір двох чисел.

prod x y = x * y

Забудьте про привласнення! У Haskell присвоєння немає! Те, що ви бачите вище - це декларація функції. «Так» означає, що функції prod з двома аргументами x і y ми зіставляємо вираз x * y. Цей вираз буде обчислено, якщо ми викличемо функцію. Давайте це і зробимо. Збережіть файл QuestMain.hs. Якщо ви вже закрили консоль ghci, знову запустіть (ghci QuestMain.hs). Якщо ви відкрили консоль, введіть команду:r - це змусить ghci перезавантажити і скомпілювати поточний файл, тобто, ваш QuestMain.hs.

*Main> :r

[1 of 1] Compiling Main    (H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted)

Ok, modules loaded: Main.

*Main> prod 3 5

15

Працює! (Якщо не працює, перевірте: регістр літер; чи збережено QuestMain.hs; чи завантажена ця версія в ghci.) Легко здогадатися, що числа 3 і 5 пов'язуються зі змінними x і y відповідно. Інтерпретатор підставляє замість prod 3 5 вираз 3 * 5, який і обчислюється.

*Main> prod 3 (2 + 3)

15

*Main> prod 3 (cos pi)

-3.0

Напишемо і випробуємо ще пару функцій. (Тут і далі я більше не буду уточнювати, що пишемо функції у файлі, а випробовуємо - в ghci.) Наприклад, таких:

printString str = putStrLn str

printSqrt x = putStrLn («Sqrt of » ++ show x ++ "" = " ++ show (sqrt x))

*Main> printString «dfdf»

dfdf

*Main> printSqrt 4

Sqrt of 4.0 = 2.0

*Main> printSqrt (-4)

Sqrt of -4.0 = NaN

В останньому випадку квадратний корінь з негативного числа дає результат «Not a Number». Припустимо, що цей висновок нас не влаштовує, і ми б хотіли, щоб на негативний x видавався рядок «x < 0!». Перепишемо функцію printSqrt кількома способами, а заодно вивчимо пару дуже корисних конструкцій.

printSqrt1 x =

if x < 0

then putStrLn «x < 0!»

else putStrLn («Sqrt of » ++ show x ++ "" = " ++ show (sqrt x))

printSqrt2 x = case x < 0 of

True -> putStrLn «x < 0!»

False -> putStrLn («Sqrt of » ++ show x ++ "" = " ++ show (sqrt x))

*Main> printSqrt1 (-4)

x < 0!

*Main> printSqrt2 (-4)

x < 0!

if не може бути без else, тому що все, що знаходиться після знака «одно» - це вираз, в якому повинні бути враховані всі варіанти (альтернативи). Якщо ви якийсь варіант не врахували, а він випав, ви отримаєте помилку, що у вас неповний набір альтернатив.

printSqrt2 x = case x < 0 of

True -> putStrLn «x < 0!»

*Main> :r

...

*Main> printSqrt2 (-4)

x < 0!

*Main> printSqrt2 10

*** Exception: H:\Haskell\QuestTutorial\Quest\QuestMain.hs:(12,16)-(13,41): Non-exhaustive patterns in case

Також зверніть увагу, що у варіантах конструкції case відступи («відбивка») важливі. Вони повинні бути однакові, - це стосується і того, прогалини там чи таби. Спробуйте скомпілювати:

printSqrt2 x = case x < 0 of

True -> putStrLn «x < 0!»

False -> putStrLn («Sqrt of » ++ show x ++ "" = " ++ show (sqrt x))

*Main> :r

[1 of 1] Compiling Main    (H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted)

H:\Haskell\QuestTutorial\Quest\QuestMain.hs:14:23:

paerse error on input '->'

Failed, modules loaded: none.

Case-конструкція дуже зручна. Її легше

COM_SPPAGEBUILDER_NO_ITEMS_FOUND