Haskell vs PHP

Здравствуйте, меня зовут Пётр и я PHP программист.

Давно уже поглядывал на Haskell, когда-то начинал читать книгу «Изучай Haskell во имя добра», а на днях пробежал по диагонали «О Haskell по-человечески» — действительно по-человечески написана.

Захотелось сделать что-нибудь реальное для production системы! Вспомнил, что недавно писал на PHP парсер простенького текстового формата — из длинного файла получить массив структур (массивов). Алгоритм элементарный — самое то, чтобы попробовать Haskell в деле.

TL;DR: удалось переписать за один рабочий день, включая тест производительности. В итоге получилось три программы: php скрипт в императивном стиле, программа на Haskell и php скрипт в функциональном стиле (калька с Haskell).

Скорость: Haskell программа выполняется быстрее всех, php скрипт в императивном стиле примерно в 6 раз медленнее, а php скрипт в функциональном стиле ещё в 5 раз медленнее (т.е. в итоге 30 раз медленнее чем Haskell). Причём «функциональный php» не может переварить большие объёмы тестов — заканчивается память из-за рекурсии.

Объём кода: php в императивном стиле — 25 строк, Haskell — 40 строк, php в функциональном стиле — 55 строк.

Первое, что потребовалось, это найти в Haskell аналоги строковых функций и функций работы с массивами. Часть из них я прочитал в книге «О Haskell по-человечески», часть нагуглил.

Вторая важная штука — это типы данных. В Haskell строгая статическая типизация. В целом в этом нет ничего страшного, однако, из-за отсутствия опыта, по ходу дела приходилось «бороться» с типами. Например, часть строковых функций, которые я нагуглил, работают с типом Text из пакета Data.Text, а часть с типом String — приходилось конвертировать с помощью pack / unpack. Наверняка можно было бы всю программу написать, используя только один из типов (Text или String), но для этого нужно получше разобраться со всем набором функций.

Наконец, основная проблема в том, чтобы сменить императивное мышление на функциональное, где нет переменных, нет циклов, только рекурсия, только хардкор. Впрочем, писать цепочки из filter / map / reduce я уже умел из опыта работы с JavaScript библиотеками underscore/lodash, и это помогло.

Самая страшилка из мира Haskell — монады, но в моей задаче их практически не было. Я использовал IO монаду для чтения stdin и записи в stdout, но всё это заняло все три строчки кода подсмотренного где-то на StackOverflow.

В итоге, для сравнения, я оформил три программы (ссылки ведут на gist этих программ):

  1. php скрипт в императивном стиле
  2. Haskell программу
  3. php скрипт в функциональном стиле (калька с Haskell кода)

Как тестировал: подготовил большой текстовый массив (21тыс строк) и подавал на вход (stdin) в каждую из трёх программ. Запуск и измерение времени производил удобным для себя способом: маленьким test.php с помощью proc_open и microtime. Версия PHP: 5.5.7, версия GHC 7.8.3, все тесты запускались на ноутбуке с Windows 8.

Результаты такие:

  1. Haskell ~ 0.2 сек
  2. php в императивном стиле ~ 1.2 сек
  3. php в функциональном стиле ~ 6.2 сек

При этом php в функциональном стиле из-за рекурсии не смог переварить более солидный тест в 40тыс строк — не хватило памяти. Ссылки для тестирования: example.txt и test.php

Подводя итог, хочу сказать, что этот эксперимент, конечно, затевался не для сравнения производительности, а для разминки ума. Узнавать и пробовать что-то новое, разные языки и подходы в написании программ — это и интересно и полезно! Всем рекомендую попробовать Haskell. Я лично продолжу свои эксперименты по переводу реальных фрагментов боевого PHP кода на Haskell.

Кстати, если Вы гуру хаскеля, посмотрите, пожалуйста, мой код, дайте советы, как можно было бы более «грамотно» его написать.

А для PHP программиста порекомендую сравнить исходники скрипта на php в функциональном стиле и исходники на Haskell — увидите много интересного!

Haskell vs PHP: 7 комментариев

  1. Здравствуйте.
    Программирую на php, читаю «О Haskell по-человечески».
    Есть пара предложений по haskell, но все по мелочи:
    — в книге упоминается Data.String.Utils из пакета MissingH, там есть strip.
    — разбивать по строкам — lines
    — имхо: наверно лучше новый блок пихать в голову, а потом один раз сделать reverse
    — splitAt вместо take 20 и drop 20
    с моими изменениями https://gist.github.com/alchimik/a4268812f867caa1124d

    > array_merge(array_slice($lines, 0, 20), array_slice($lines, 21))
    может лучше array_splice заюзать?

    > Так надо переписать ваш PHP-код …
    если обрабатывать строки по одной, то получится другой алгоритм, тогда надо и haskell-код переписывать

    • Отличные предложения! К сожалению не смог установить у себя MissingH — я работаю на Windows 8 и cabal install missingh упал при попытке установить зависимость:
      Configuring network-2.6.0.1…
      setup.exe: The package has a ‘./configure’ script. This requires a Unix
      compatibility toolchain such as MinGW+MSYS or Cygwin.

      Новый блок в голову, а в конце reverse — добавление в голову работает быстрее?

      >> array_merge(array_slice($lines, 0, 20), array_slice($lines, 21))
      >может лучше array_splice заюзать?
      Да, тут напрашивается array_splice, но это уже будет не в функциональном стиле, array_splice меняет массив по месту!

      • > К сожалению не смог установить у себя MissingH — я работаю на Windows 8 и …:
        У меня тоже win8. Сначала поставил haskell platform на неё, но одумался, и поставил на виртуалку, а именно https://www.fpcomplete.com/page/haskell-eval-vm

        > Новый блок в голову, а в конце reverse — добавление в голову работает быстрее?
        Да. ‘Писать’ и читать списки можно только с головы и последовательно. Список в haskell’е почти-что стэк. Чтобы ‘добавить’ элемент в конец, надо пройтись по всему списку от начала до конца.
        По этой же причине и String работает медленно, ибо произвольного доступа нет.
        Можете взглянуть на сорцы функции (++) http://hackage.haskell.org/package/base-4.7.0.1/docs/src/GHC-Base.html#%2B%2B

        > Да, тут напрашивается array_splice, но это уже будет не в функциональном стиле, array_splice меняет массив по месту!
        Изменяемые данные для функционального стиля мне кажется норм, из популярных функциональных языков haskell чуть ли не один чистый.

        Переписал на Data.Text, почти в два разы быстрее вышло, чем мой прошлый вариант с String.


        {-# LANGUAGE OverloadedStrings #-}
        module Main where
        import qualified Data.Text as T
        parse :: T.Text -> [[T.Text]]
        parse = splitPerBlocks . getLines
        getLines :: T.Text -> [T.Text]
        getLines = map T.strip . T.lines
        splitPerBlocks :: [T.Text] -> [[T.Text]]
        splitPerBlocks = reverse . splitPerBlocksRecursiveFold []
        where splitPerBlocksRecursiveFold blocks lns
        | length lns < 20 = blocks
        | length linesWithoutHeaderAndFlag >= 20 =
        let (first20Lines, otherLines) = splitAt 20 linesWithoutHeaderAndFlag
        newFoundBlocks = first20Lines : blocks
        in splitPerBlocksRecursiveFold newFoundBlocks otherLines
        | otherwise = blocks
        where linesWithoutHeaderAndFlag = dropFlagLine . dropHeaderLine $ lns
        dropHeaderLine :: [T.Text] -> [T.Text]
        dropHeaderLine lns@(x:xs)
        | "Container No." `T.isInfixOf` x = xs
        | otherwise = lns
        dropFlagLine :: [T.Text] -> [T.Text]
        dropFlagLine lns
        | length lns <= 20 = lns
        | T.isInfixOf "Flag:" $ lns !! 20 = take 20 lns ++ drop 21 lns
        | otherwise = lns
        main :: IO()
        main = do
        s <- getContents
        print $ length $ parse $ T.pack s

        Хотел сделать с ByteString. Должен быть быстрее Data.Text, так как работает с байтами а не с юникодом, но в Data.ByteString не оказалось пары функций, а переделывать мне лень, может позже.
        ps: OverloadedStrings конеxно можно было и не юзать, а запаковать литералы вручную.

      • Спасибо за ссылку на виртуальную машину!
        А ещё понравилась вот эта строка:
        dropHeaderLine lns@(x:xs)
        Сам когда писал, всё думал «не хватает паттерн мачинга!» 🙂

Оставьте комментарий