UProLa

Неокріпші думки

Про тетріс, Хаскель і типи

with 4 comments

Писав я недавно один проект на Python і зрозумів – відсутність типів у пайтончику заважає розбиратись у чужому коді. Щоб взнати, який тип має змінна доводилось вписувати print(var) і перезапускати програму (а вона була графічна). Це не особливо напрягало, проте тепер я розумію, чому вказувати типи потрібно навіть в “очевидних” випадках – не всім воно очевидно.

З такою логікою я взявся і дописав типи до більшості функцій свого тетріса на Хаскелі. Заодно перейшов від чисто функціонального програмування до типо-функціонального (тут немає помилки в слові “типо”).

https://gist.github.com/2281537 – для тих, хто спершу дивиться код.

Ну, по-перше, код збільшився на 45 рядків. Печалька… По-друге, в намаганнях зробити кросплатформенність я так нічого і не досяг, для запуску коду під лінуксом потрібно модифікувати код. Печалька :`( …

Випадковий елемент

> randomItem xs = (xs!!) <$> randomRIO (0, length xs - 1)

Навчу вас читати цей код. Першим ділом дивіться на символ <$>. Це потужний символ, він розділяє вираз на дві частини – ліву і праву. Все, що зліва – функція. Все, що справа – дані. Символ <$> застосовує функцію зліва до даних справа.

Чому саме так? Про це дещо нижче. Глянемо тепер на ліву частину – функцію. Символ !! еквівалентний отриманню елементу по індексу. Те, що в нормальних мовах xs[2] в Хаскелі записується як xs !! 2. Якщо xs – список, то xs !! 2 поверне елемент цього списку. А чому дорівнює xs !! ? Це уже буде функція, що приймає на вхід індекс а повертає елемент списку! Тому запис (xs!!) еквівалентний (\index -> xs !! index), тільки коротший.

Справа знаходяться дані, що генерує функція randomRIO. Вона повертає випадкове число з інтервалу, вказаного у дужках. Єдина проблема – це випадкове число загорнуто у монаду IO, з якої, як відомо, ніщо не може вийти.

Тепер ще раз про <$>. Цей оператор вводить функцію зліва в монаду IO справа і спокійно застосовує функцію до даних. Загальний результат залишиться в монаді IO, що, в принципі, також непогано.

> randomItem xs = randomRIO (0, length xs - 1) >>= (\index -> return (xs !! index))

Ось так в коді виглядає те, що я описав. Але всі пишуть через <$>, бо так простіше.

Таймер

> -- | Таймер для відмірювання одного інтервалу
> type Timer = ( Rational  -- ^ показник часу на початок відмірювання
>              , Rational) -- ^ інтервал відмірювання (в секундах)

> -- | Створити новий таймер
> createTimer :: Rational    -- ^ Інтервал часу для таймера (в секундах)
>                -> IO Timer

> -- | Оновити стан таймера. Друге поле кортежу результату показує, 
> -- чи прошов вказаний у таймері інтервал часу
> updateTimer :: Timer -> IO (Timer, Bool)

Об’ява такого типу дещо спростила роботу з часом за рахунок інкапсуляції коду. Тепер мені не доводиться заморочуватись параметрами таймера при передачі часу в mainLoop, що радує. Також зауважте спосіб анотації параметрів.

Таймер використовується, наприклад, у main:

> main = do timer <- createTimer 1
>           score <- gameLoop (World (emptyScreen 10 12) Nothing) 0 timer
>           putStrLn ("Your score - " ++ (show score))

Світ

> data World = World [[Char]]            -- ^ поле гри
>                    (Maybe ( Int        -- ^ координата X фігури
>                           , Int        -- ^ координата Y фігури
>                           , [[Char]])) -- ^ геометрія фігури

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

було

> rotateFigure (screen, (x, y, fig)) = (screen, (x, y, rotate fig))

стало

> -- | Поворот фігури проти годинникової
> rotateFigure :: World -> World
> rotateFigure (World screen figure) = World screen (rot <$> figure)
>     where rot (x, y, fig) = (x, y, rotate fig)

Ось так ось… Але чого не зробиш заради читабельності! До-речі, бачите знову цей символ <$> ? На цей раз він працює не з монадою IO, а з монадою Maybe. В загальному, він працює з будь-яким аплікативним функтором, а оскільки монади являються функторами, то символ працює і з будь-якими монадами. Для списку він навіть має окрему загальноприйняту назву – map (девелопери спеціально залишили цю застарівшу назву map для того, щоб зробити помилки компіляції більш зрозумілими, коли справа йде про списки – все для новачків!)

Наслідки типізації

> -- | Визначити, які рухи довзолено фігурі в заданій позиції
> findPossibleMoves :: World
>                      -> ( Bool  -- ^ рух вправо
>                         , Bool  -- ^ рух вліво
>                         , Bool  -- ^ рух вниз
>                         , Bool) -- ^ поворот проти годинникової

> -- | Об'єднує поле з фігурою та викидує заповнені рядки.
> removeFilledLines :: World 
>                      -> ( [[Char]]  -- ^ об'єднане з фігурою поле гри
>                         , Int)      -- ^ кількість заповнених та видалених рядків

> gameLoop ::    World  -- ^ Стан гри
>             -> Int    -- ^ Рахунок
>             -> Timer  -- ^ Таймер, що відмірює час падіння фігури на один блок вниз
>             -> IO Int

Як глобальним наслідком тотальної типізації стала … поява коментарів до параметрів функцій! Якщо не впадлу написати тип параметру, то вже й не впадлу написати комент до типу. Хоча, якщо порівняти gameLoop з попепередньою версією:

> gameLoop :: ([[Char]], (Int, Int, [[Char]])) -- ^ світ
>              -> Int -- ^ рахунок (скільки ліній заповнено)
>              -> Bool  -- ^ необхідність рефреша світу
>              -> (Rational, Rational) -- ^ стан часу
>              -> IO ()

То прогрес як на долоні. Якщо постаратись, то за допомогою лінз та реактивного програмування можна буде добитись ООПшної лаконічності при модифікації стану гри, над чим я ще попрацюю.

Written by danbst

Травень 31, 2012 at 08:54

Оприлюднено в Програмування

Відповідей: 4

Subscribe to comments with RSS.

  1. > Писав я недавно один проект на Python і зрозумів – відсутність типів у пайтончику заважає розбиратись у чужому коді.
    — ну, це і з вежі С++/Java очевидно.

    Довге пояснення щодо для тих, хто в темі, можна було б замінити двома рядками:
    ($) :: (a -> b) -> a -> b
    (<$>) :: Functor f => (a -> b) -> f a -> f b

    > data World = World [[Char]] — ^ поле гри
    > (Maybe ( Int — ^ координата X фігури
    > , Int — ^ координата Y фігури
    > , [[Char]])) — ^ геометрія фігури
    — здаєтся, тут «хорошим тоном» було б наробити щось типу
    newtype Field = [[Char]]
    newtype Shape = [[Char]]
    newtype Position = (Int, Int)
    newtype Figure = (Position, Shape)
    data World = World Field (Maybe Figure)
    — але не факт, що це читабельніше; в’язнути у купі невідомих типів також мало приємно, але моя нелюбов до надмірної кількості коментарів стає більш спокійною.

    dmytrish

    Травень 31, 2012 at 09:20

    • Можливо, можливо. Я вирішив не забивати “проект” лишніми типами, бо він й так маленький. Або може я не доріс до “надмірної” типізації.

      PS. А чому саме newtype? І ще – у тебе код запускається?

      danbst

      Травень 31, 2012 at 15:48

      • Таксь, перепрошую за довге мовчання.
        Мій ghc (Kubuntu 12.04, x64):
        $ ghc –version
        The Glorious Glasgow Haskell Compilation System, version 7.4.1
        Для запуску потрібно закоментувати getKeyWindows і згадки про неї (цікаво, тут би допоміг Template Haskell для генерації коду під конкретну платформу? Я про нього знаю трохи більше, ніж нічого). Також у мене import List довелось замінити на import Data.List, здається, це через версію.

        А так працює great.

        dmytrish

        Червень 2, 2012 at 23:46

      • Оу, дякую за Data.List. А TH тут не обов’язковий, можна обійтися препроцесором С (#ifdef), якщо знайду макроконстанту для типу платформи.

        danbstt

        Червень 8, 2012 at 06:39


Залишити відповідь

Заповніть поля нижче або авторизуйтесь клікнувши по іконці

Лого WordPress.com

Ви коментуєте, використовуючи свій обліковий запис WordPress.com. Log Out / Змінити )

Twitter picture

Ви коментуєте, використовуючи свій обліковий запис Twitter. Log Out / Змінити )

Facebook photo

Ви коментуєте, використовуючи свій обліковий запис Facebook. Log Out / Змінити )

Google+ photo

Ви коментуєте, використовуючи свій обліковий запис Google+. Log Out / Змінити )

З’єднання з %s

%d блогерам подобається це: