Haskell Quest Tutorial — Преддверие

  • 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)
< 0!
*Main> printSqrt2 (-4)
< 0!


if не может быть без else, потому что всё, что находится после знака «равно» — это выражение, в котором должны быть учтены все варианты (альтернативы). Если вы какой-то вариант не учли, а он выпал, вы получите ошибку, что у вас неполный набор альтернатив.

printSqrt2 x = case x < 0 of
                True -> putStrLn «x < 0!»
 
*Main> :r
...
*Main> printSqrt2 (-4)
< 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-конструкция очень удобна. Ее легче читать и расширять, чем if-then-else. Она, конечно, может быть выражена через серию вложенных if-then-else, следовательно, конструкции эквивалентны. Скажу по секрету: case еще более функционален, и скоро мы это увидим. А пока дополнительный пример. Немного искусственный, ну да не беда:

thinkAboutSquaredX x = case x of
                0.0 -> "I think, x is 0, because 0 * 0 = 0."
                1.0 -> "x is 1, because 1 * 1 = 1."
                4.0 -> "Well, x is 2, because 2 * 2 = 4."
                9.0 -> "x = 3."
                16.0 -> "No way, x = 4."
                25.0 -> "Ha! x = 5!"
                otherwise -> if x < 0 then "x < 0!" else "Sqrt " ++ show x ++ " = " ++ show (sqrt x)


 
*Main> thinkAboutSquaredX 1
«x is 1, because 1 * 1 = 1.»
*Main> thinkAboutSquaredX 25
«Ha! x = 5!»

Слово otherwise то и значит: «в противном случае». Когда по очереди не подошли остальные варианты, подойдет otherwise, потому что это лишь синоним True. Не стоит его вставлять в середину, потому что тогда все нижние варианты будут недоступны.

thinkAboutSquaredX x = case x of
                0.0 -> «I think, x is 0, because 0 * 0 = 0.»
                1.0 -> «x is 1, because 1 * 1 = 1.»
                otherwise -> if x < 0 then «x < 0!» else «Sqrt » ++ show x ++ " = " ++ show (sqrt x)
                4.0 -> «Well, x is 2, because 2 * 2 = 4.»
                9.0 -> «x = 3.»
                16.0 -> «No way, x = 4.»
                25.0 -> «Ha! x = 5!»


Код скомпилируется, но мы получим предупреждение о пересечении образцов для сопоставления:

*Main> :r
[1 of 1] Compiling Main    (H:\Haskell\QuestTutorial\Quest\QuestMain.hs, interpreted)
 
H:\Haskell\QuestTutorial\Quest\QuestMain.hs:16:24:
    Warning: Pattern match(es) are overlapped
             In a case alternative:
                 4.0 -> ...
                 9.0 -> ...
                 16.0 -> ...
                 25.0 -> ...
Ok, modules loaded: Main.
*Main> thinkAboutSquaredX 1
«x is 1, because 1 * 1 = 1.»
*Main> thinkAboutSquaredX 9
«Sqrt 9.0 = 3.0»
*Main> :t otherwise
otherwise :: Bool
*Main> otherwise == True
True


Вспомним, чем мы тут занимаемся: мы пишем квест! В любом квесте есть три вещи: локации (путями перемещения), объекты и действия игрока. Как бы мы могли вывести описание локации, если бы у нас был ее номер? Вот решение:

describeLocation locNumber = case locNumber of
            1 -> «You are standing in the middle room at the wooden table.»
            2 -> «You are standing in the front of the night garden behind the small wooden fence.»
            -- Здесь вставлять описание других локаций.
            otherwise -> «Unknown location.»
 
*Main> describeLocation 2
«You are standing in the front of the night garden behind the small wooden fence.»
*Main> describeLocation 444
«Unknown location.»


Обратите внимание: комментарий! Однострочные комментарии начинаются с двойного минуса (как в SQL!). Для многострочных используются символы {- и -}:

describeLocation locNumber = case locNumber of
            1 -> «You are standing in the middle room at the wooden table.»
            2 -> «You are standing in the front of the night garden behind the small wooden fence.»
            {- Здесь вставлять описание других локаций.
            Или здесь. 
            adfsdf few fef jel jle jkjlefjaiejeo -}

            otherwise -> «Unknown location.»


Ну вот. Мы познакомились с не всеми базовыми конструкциями языка Haskell. putStrLn, show, case-конструкция, строки, ghci — все это нам понадобится в дальнейшем. Мы даже написали одну функцию для квеста. Пожалуй, достаточно. Во второй части мы начнем работать над квестом и по ходу дела изучим еще какие-нибудь замечательные трюки Haskell. Приключения ждут нас!

Для закрепления можете решить следующие задачки:

1. Задать функцию z и вычислить ее для некоторых значений:
t = (7 * x^3 — ln (abs (a))) / (2,7 * b)
y = sin(t) — sin(a)
z = 8.87 * y^3 + arctan(t)
где x, a, b — переменные типа Float.

2. Задать функцию y и вычислить ее для некоторых значений:
| ln (abs(sin(x))), если x > 5
y = | x^2 + a^2, если x <= 5 и a <= 3
| x / a + 7.8*a, если x <= 5 и a > 3
где x, a — переменные типа Float.


Оглавление, список литературы и дополнительную информацию вы найдете в «Приветствии»
Поделиться публикацией

Похожие публикации

Комментарии 11

    +2
    Очень неплохо! Ну только стоит отметить, что лучше всего читать туториал вместе с каким-нибудь Real World Haskell или хотя-бы Yet Another Haskell Tutorial, для полноты картины. Все-таки тема монад не раскрыта. Но однозначно, один из самых доступных туториалов, которые я видел :)
      0
      Ну, я это отметил здесь: habrahabr.ru/blogs/Haskell/120590/
      Вообще вы правы, надо ссылку на «Приветствие» в статью добавить.
      +1
      Отличная статья! Написано с юмором и понятно.
      Я бы ещё начинающим порекомендовал отличный сайт Hoogle. Здесь вы всегда можете получить справку по библиотечным функциям, очень удобно :)
        +1
        Спасибо!
        +2
        Огромное спасибо автору статьи. Это первая статья, после которой я хоть немного въехал в смысл этого Haskell.
          +3
          Пожалуйста! Но это еще не конец. :) В запасах лежит еще одна готовая часть, а сейчас пишу часть 3.
            +2
            С нетерпением жду! Впрочем, я уверен что не я один такой.
          +1
          Круто! Очень круто! И язык, и статья отличные! Вы главное не забросьте, пожалуйста! Жду с нетерпением продолжения!
            0
            Вы правы: выучить язык гораздо легче, чем завершить проект… Но я постараюсь! Следующая статья будет через неделю. Просто потому, что я хочу выкладывать очередную статью, когда в запасе есть еще одна.
          • НЛО прилетело и опубликовало эту надпись здесь
              +1
              Неплохо для начала. Для людей, знающих другие языки, очень просто, но таки полезно.
              Спасибо, жду продолжения.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое