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-конструкція дуже зручна. Її легше
