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 и преобразовали конструктор в строку. Красиво? По-моему, да. Попробуйте, например, в С++ столь же легко преобразовать в строку элемент какого-нибудь перечисления…

    Функция 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 typeString
        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). Скомпилировать неправильный код нельзя. При большом рефакторинге может возникнуть до нескольких десятков ошибок, а то и больше, — и придется их все исправить, одну, другую, третью… Когда наконец ошибки исчезнут, код с высокой вероятностью заработает именно так, как вы ожидаете. И это, скажу я вам, очень-очень здорово!


    Чтобы из строки «x» получить конструктор типа Action, есть несколько решений. Для начала попробуем придумать функцию 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 == «New»  = New
                               | otherwise     = Quit
     
    -- Сопоставление с образцом (pattern matching)
    convertStringToAction'' :: String -> Action
    convertStringToAction'' «Look» = Look
    convertStringToAction'' «New»  = New
    convertStringToAction'' _      = Quit
     
    -- Проверка в ghci
    *Main> convertStringToAction' «New»
    New
    *Main> convertStringToAction'' «New»
    New


    «И что же, снова и снова расширять case-конструкцию у функции convertStringToAction? Не очень-то хочется...» — это что за нотки отчаяния?!.. Haskell — ленивый язык, и настоящий программист тоже должен быть ленивым, чтобы не писать лишний код там, где это не нужно. Не хочется расширять case? И не надо! Что мы сделаем? Приготовьтесь! Мы учим новое заклинание! Запишите: "класс типов Read, в котором лежат функции read и reads". Унаследуем от Read все наши АТД-типы и посмотрим, к чему это приведет.

    data Location =
              Home
              ...
        deriving (Eq, Show, Read)
     
    data Direction =
                  North
                  ...
        deriving (Eq, Show, Read)
     
    data Action =
              Look
              ...
        deriving (Eq, Show, Read)


    Чтобы почувствовать функцию read, немного поиграемся в ghci. Два примера для сравнения:

    *Main> describeLocation Home
    «Home\nYou are standing in the middle room at the wooden table.»
     
    *Main> describeLocation (read «Home»)
    «Home\nYou are standing in the middle room at the wooden table.»


    Какой вывод можно сделать? Функция describeLocation не изменилась, ей по-прежнему нужен тип Location. В первом примере мы передаём конструктор, а во втором — получаем его из строки «Home».

    *Main> describeLocation ( read (show Home) )
    «Home\nYou are standing in the middle room at the wooden table.»


    Функция read берет строку и пытается распарсить её к типу, который нужен в этом месте. Откуда read знает про типы? Он берет их из окружающих выражений. В данном случае он видит, что (read «Home») — это параметр функции describeLocation, а у параметра тип задан строго. Бывают случаи, когда тип брать неоткуда, но очень редко. Простой пример: если вызвать 'read «Home»' в ghci, компилятор нас не поймёт:

    *Main> read «Home»
     
    <interactive>:1:1:
        Ambiguous type variable 'a0' in the constraint:
          (Read a0) arising from a use of 'read'
        Possible fix: add a type signature that fixes these type variable(s)
        In the expression: read «Home»
        In an equation for 'it': it = read «Home»


    Но мы можем ему помочь, указав тип явно с помощью специальной записи:

    *Main> read «Home» :: Location
    Home
    *Main> read «5.5» :: Float
    5.5
    *Main> read «True» :: Bool


    Волшебство, не так ли? Внедряя read в функцию convertStringToAction, мы получим более краткий и при этом более функциональный код.

    -- Обрабатываем действие.
    evalAction :: Action -> String
    evalAction act = «Action: » ++ show act ++ "!"
     
    -- Преобразовываем строку в Action
    convertStringToAction :: String -> Action
    convertStringToAction str = read str
     
    -- Получаем ввод с клавиатуры, конвертируем его в действие, вызываем обработчик, выводим результат.
    run = do
            putStr «Enter command: »
            x <- getLine
            putStrLn ( evalAction (convertStringToAction x) )
     
    -- Проверяем в ghci:
    *Main> run
    Enter command: Look
    Action: Look!
    *Main> run
    Enter command: Quit
    Action: Quit!


    К сожалению, у read есть один существенный недостаток: если мы передадим абракадабру, то получим исключение, и программа завершится.

    *Main> run
    Enter command: abrakadabra
    Action: *** Exception: Prelude.read: no parse


    Не расстраивайтесь! У нас есть более безопасная функция reads. Я, конечно, покажу, как ее использовать, но объяснения оставлю на будущее. А сегодня — «Action: Quit!». Отдыхать и учить заклинания.

    convertStringToAction :: String -> Action
    convertStringToAction str = case reads str of
                [(x, _)] -> x
                _ -> Quit
     
    *Main> run
    Enter command: abrakadabra
    Action: Quit!


    Задания для закрепления.

    1. Объекты квеста и действие Investigate.
    — Сделать АТД «Объект», добавить туда любые объекты.
    — Составить функцию describeObject, которая выдаёт описание объекта.
    — Составить функцию investigate, которая запрашивает у пользователя название объекта и выводит описание этого объекта.

    2. Программа «Тупой калькулятор».
    Имеются целочисленные операции «Сложить», «Вычесть» и «Умножить».
    Написать программу, которая требует у пользователя целое число1, затем требует целочисленную операцию, затем требует целое число2. Когда это всё получено, программа должна выполнить над числами соответствующую операцию и вывести результат.


    Исходники к этой части.

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

    Подробнее
    Реклама
    Комментарии 2
    • +1
      Большое спасибо за эту серию статей!
      • 0
        Пожалуйста!

        Хотелось бы написать еще несколько частей, но это, видимо, не раньше отпуска.

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