Мини заметки — выпуск 17, полностью посвященный Haskell

24 февраля 2014

Этот выпуск мини змаеток полностью посвящается замечательному языку программирования Haskell. Сегодня мы узнаем, что в ghci можно просматривать документацию, что в Haskell есть глобальные переменные и интерполяция строк, а также о других интересных вещах. Предыдущие выпуски: шестнадцатый, пятнадцатый, четырнадцатый, тринадцатый.

1. Отображение документации в ghci

Через ghci можно просматривать документацию. Для этого в файл ~/.cabal/config прописываем, если у вас это еще не сделано:

documentation: True

Теперь во время сборки пакетов документация к ним будет генерироваться в каталоге ~/.cabal/share/doc. Затем устанавливаем утилиту haskell-docs:

cabal install haskell-docs

в ~/.ghci дописываем:

:set -package regex-pcre
:set -package process
:set -package directory
:set -w
:m +Text.Regex.PCRE
:def doc \x -> return (unlines [":unset +s",":redir eax_info :i " ++ x,"let (_,_,_,[eax_mod]) = (eax_info =~ (\"Defined in `(.*?)'\" :: String)) :: (String,String,String,[String])","System.Cmd.system (\"haskell-docs \" ++ eax_mod ++ \" \" ++ \"" ++ x ++ "\") >>= \\_ -> return ()", ":set +s"])

Если у вас в ~/.ghci не прописано :set +s, то первую и последнюю команду из списка, которые unset и set, можно смело убрать. Предполагается, что вы уже используете макрос redir. Если это не так, возьмите его здесь. Там же можно найти классный пример с макросом pasteCode.

Теперь мы можем делать так:

ghci> :doc ProcessInfo
Provide information about a running process
ghci> :doc spawnLocal
Spawn a process on the local node

Весь фокус в том, чтобы выполнить :i имя_функции, выдрать регулярным выражением имя модуля, в котором она определена, а затем выполнить haskell-docs имя_модуля имя_функции. Если вдруг что-то пойдет не так, например, функция с одним именем определена в нескольких модулях, можно прямо сказать :!haskell-docs аргументы.

Чтобы этот прием работал в песочнице, нужно внутри этой песочницы поставить пакет regex-pcre, то есть, просто сказать cabal install regex-pcre. Кстати, внутри песочницы можно подгружать любые установленные в этой песочнице модули, если в REPL сказать :set -package имя_пакета.

К сожалению, я пока не научился смотреть в REPL документацию к пакету, который ты пилишь внутри песочницы. Насколько я понимаю, haskell-docs с песочницами не работает.

2. Оптимизация кода с помощью rewrite rules

Rewrite rules — это такая фишка в компиляторе GHC, позволяющая делать крутые оптимизации в стиле map f (map g xs) = map (f.g) xs. Рассмотрим пример:

data FBB a = Foo a | Bar a | Baz a
             deriving (Show)

foo :: a -> FBB a
foo x = Foo x

bar :: FBB a -> FBB a
bar (Foo x) = Bar x
bar (Bar x) = Bar x
bar (Baz x) = Bar x

baz :: a -> FBB a
baz x = Baz x

{-# RULES
"bar foo -> baz" forall x. bar $ foo x = baz x
  #-}


main = print $ bar $ foo "abc"

Если скомпилировать программу с флагом -O2, она выведет Baz "abc". Также благодаря rewrite rules можно делать специализированные для определенных типов версии функций. Для отладки rewrite rules в GHC предусмотрен ряд флагов, например, -ddump-rule-firings. Подробности можно найти здесь.

3. Глобальные переменные в Haskell через unsafePerformIO

Вы обратили внимание, что в пакете hslogger используется глобальное состояние? Не задумывались, как это работает, при условии, что в Haskell вроде бы как нет глобальных переменных? Так вот, оказывается, что они все-таки есть.

Рассмотрим пример:

import Control.Concurrent
import Control.Concurrent.STM
import System.IO.Unsafe (unsafePerformIO)

{-# NOINLINE counter #-}
counter :: TVar Int
counter = unsafePerformIO $ newTVarIO 0

incCounter :: IO Int
incCounter =
  atomically $ do
    t <- readTVar counter
    let t' = t + 1
    writeTVar counter t'
    return t'

main :: IO ()
main = do
  n1 <- incCounter
  print n1
  n2 <- incCounter
  print n2
  n3 <- incCounter
  print n3

Весь фокус заключается в использовании функции unsafePerformIO. Это такой хак, позволяющий использовать грязные функции из чистых. В сочетании с ленивыми вычислениями получаем тот же эффект, что дают глобальные переменные!

Здесь пишут, что такой прием считается безопасным. Если вы боитесь, что при выходе очередной версии GHC прием перестанет работать, обратите внимание на пакет safe-globals. Подробности о глобальных переменных в Haskell можно найти здесь.

Ну а в целом, конечно же, вместо использования этого приема лучше оборачивать глобальное состояние в монаду. Глобальные переменные приводят к сложным в обнаружении ошибкам и препятствуют распараллеливанию кода.

4. Строгие вычисления в Haskell — пакет strict-identity

Есть такой занятный пакет strict-identity с объявлением строгой версии монады Identity. Используется как-то так:

foo = runStrictIdentity $! do
    x <- f a b
    y <- g x y
    return $! x + y

Здесь x, y и foo приводятся к WHNF (то есть, делается seq), что дает неплохие гарантии строгости. Подозреваю, что, используя ту же идею, при желании можно делать и deepseq. Ну или, как минимум, просто шаблон на Template Haskell написать.

5. Строгие вычисления в Haskell — плагин для GHC

А еще для GHC есть плагин strict-ghc-plugin. Пример использования:

$ cabal install strict-ghc-plugin
$ git clone git@github.com:thoughtpolice/strict-ghc-plugin.git
$ cd strict-ghc-plugin/tests
$ ghc -fplugin=Strict.Plugin NonTerminating.hs
$ ./NonTerminating
Stack space overflow: current size 8388608 bytes.
Use `+RTS -Ksize -RTS' to increase it.
$ ghc -fforce-recomp NonTerminating.hs
$ ./NonTerminating
[0,1,2,3,4,5,6,7,8,9]

Файл NonTerminating.hs содержит следующий код:

{-# LANGUAGE CPP #-}
module Main ( main ) where
import Strict.Annotation

{-# ANN foreverFrom Strictify #-}
foreverFrom :: Int -> [Int]
foreverFrom n = n : foreverFrom (n + 1)

main :: IO ()
main = do
  let xs = foreverFrom 0
  print (take 10 xs)

При использовании плагина (флаг -fplugin) функция foreverFrom делается строгой благодаря соответствующей аннотации, что приводит к предсказуемым последствиям.

6. Vim и быстрый переход к определениям в коде на Haskell

Файл tags для кода на Haskell можно получить так:

cabal install hasktags
hasktags ./src

Теперь в Vim можно прыгать к определениям функций и обратно, используя сочетания типа Ctr+] и Ctr+T. Удобно, когда работаете с кодом довольно крупного и (особенно!) незнакомого проекта, например, Cloud Haskell.

7. Зачем нужен cabal sandbox add-source

Допустим, вы работаете одновременно над несколькими зависящими друг от друга проектами, каждый из которых находится в своем репозитории. Не только работаете, но и желаете экспериментировать с разными ветками например.

Как быть? Вот для этого и нужен cabal sandbox add-source:

git clone git@github.com:haskell-distributed/distributed-process-platform.git
cd distributed-process-platform
cabal sandbox init
mkdir deps
cat REPOS | perl -lne 'system("cd deps; git clone git://github.com/haskell-distributed/$_; cd $_; git fetch; git checkout development; cd ../..; cabal sandbox add-source ./deps/$_")'

Все зависимости, перечисленные в файле REPOS, будут сложены в каталог deps. При сборке проекта исходники пакетов будут браться оттуда.

8. Интерполяция строк и строки с кавычками

В самом Haskell нет интерполяции строк или аналога тройных кавычек из мира Python. Но все это несложно добавить, используя Template Haskell. Для этого уже есть множество готовых пакетов.

Например, string-qq:

ghci> :m + Data.String.QQ
ghci> [s|Hello "world"!|]
"Hello \"world\"!"

Heredoc:

ghci> [str|Hello "World"|]
"Hello \"World\""
ghci> :{
ghci| [str|Hello,
ghci|     | "world"!|]
ghci| :}
"Hello,\n \"world\"!"

Here:

ghci> :m + Data.String.Here
ghci> let str = "world"
ghci> [i|Hello ${str}|]
"Hello world"
ghci> [iTrim|   Hello ${str}  |]
"Hello world"
ghci> [i|1 + 2 = ${1 + 2}|]
"1 + 2 = 3"

И множество других — raw-strings-qq, neat-interpolation, interpolatedstring-perl6.

В общем, вы без труда найдете пакет по вкусу. Да и свой собственный написать не так уж трудно.

9. Как уменьшить размер исполняемого файла

Haskell позволяет найти компромисс между скоростью компиляции программы, ее размером и скоростью, с которой программа будет выполнятся. Например, программу можно вообще не компилировать, запуская ее в интерпретаторе runhaskell. При компиляции можно указать флаг -O2. Программа будет несколько дольше компилироваться, но зато быстрее работать. Если по каким-то причинам для вас очень важен размер программы, попробуйте собрать ее таким образом:

cabal install --enable-split-objs

Возьмем к примеру нашу с вами телефонную книгу. На моем компьютере ее полная компиляция (то есть, со скачиванием и сборкой всех зависимостей) занимает 18 минут, в результате получается файл размером 11.8 Мб. Если же использовать флаг --enable-split-objs, полная компиляция длится 22 минуты, но размер файла при этом получается 3.2 Мб. А еще в GHC 7.8 должен наконец-то появится флаг параллельной компиляции -j.

10. Тестирование примеров в документации с помощью Doctest

Рассмотрим следующий код (все исходники целиком лежат здесь):

module Main where

{- | Say hello

>>> sayHello "Alex"
Hello, Alex!
-}

sayHello :: String -- ^ User name
         -> IO ()  -- ^ Return nothing
sayHello name =
  putStrLn $ "Hello, " ++ name ++ "!"

-- | Main
main = do
  putStrLn "What's your name?"
  name <- getLine
  sayHello name

Сгенерировать haddock-документацию к этой программе можно так:

$ cabal haddock --executables

Как видите, в документации содержится пример использования функции sayHello в REPL. Пакет doctest предназначен для тестирования этих примеров:

$ doctest ./src/Main.hs
Examples: 1  Tried: 1  Errors: 0  Failures: 0

Давайте прикрутим doctest к самому пакету, чтобы его было удобно запускать из Jenkins и все такое. Для этого в cabal-файле дописываем:

test-suite doctests
  type:                exitcode-stdio-1.0
  ghc-options:         -threaded
  main-is:             doctests.hs
  build-depends:       base >= 4.6 && < 4.7,
                       doctest >= 0.9 && < 0.10
  hs-source-dirs:      ./test
  default-language:    Haskell2010

В файле test/doctests.hs пишем:

import Test.DocTest

main = doctest ["-isrc", "src/Main.hs"]

Проверяем:

$ cabal install --enable-tests
$ cabal test

Нулевой код возврата означает, что все ОК. Смотрите, что получается. Теперь у нас есть тесты, проверяющие код, документацию, а также то, что документация соответствует коду! Ну разве не круто?

Дополнение: Мини заметки — выпуск 18

Метки: , , .


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