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)
    < 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.


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

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

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