Мини заметки — выпуск 17, полностью посвященный Haskell
24 февраля 2014
Этот выпуск мини змаеток полностью посвящается замечательному языку программирования Haskell. Сегодня мы узнаем, что в ghci можно просматривать документацию, что в Haskell есть глобальные переменные и интерполяция строк, а также о других интересных вещах. Предыдущие выпуски: шестнадцатый, пятнадцатый, четырнадцатый, тринадцатый.
1. Отображение документации в ghci
Через ghci можно просматривать документацию. Для этого в файл ~/.cabal/config прописываем, если у вас это еще не сделано:
Теперь во время сборки пакетов документация к ним будет генерироваться в каталоге ~/.cabal/share/doc. Затем устанавливаем утилиту haskell-docs:
в ~/.ghci дописываем:
: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.
Теперь мы можем делать так:
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
. Рассмотрим пример:
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.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. Используется как-то так:
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. Пример использования:
$ 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 содержит следующий код:
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 можно получить так:
hasktags ./src
Теперь в Vim можно прыгать к определениям функций и обратно, используя сочетания типа Ctr+] и Ctr+T. Удобно, когда работаете с кодом довольно крупного и (особенно!) незнакомого проекта, например, Cloud Haskell.
7. Зачем нужен cabal sandbox add-source
Допустим, вы работаете одновременно над несколькими зависящими друг от друга проектами, каждый из которых находится в своем репозитории. Не только работаете, но и желаете экспериментировать с разными ветками например.
Как быть? Вот для этого и нужен cabal sandbox add-source:
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> [s|Hello "world"!|]
"Hello \"world\"!"
"Hello \"World\""
ghci> :{
ghci| [str|Hello,
ghci| | "world"!|]
ghci| :}
"Hello,\n \"world\"!"
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. Программа будет несколько дольше компилироваться, но зато быстрее работать. Если по каким-то причинам для вас очень важен размер программы, попробуйте собрать ее таким образом:
Возьмем к примеру нашу с вами телефонную книгу. На моем компьютере ее полная компиляция (то есть, со скачиванием и сборкой всех зависимостей) занимает 18 минут, в результате получается файл размером 11.8 Мб. Если же использовать флаг --enable-split-objs
, полная компиляция длится 22 минуты, но размер файла при этом получается 3.2 Мб. А еще в GHC 7.8 должен наконец-то появится флаг параллельной компиляции -j.
10. Тестирование примеров в документации с помощью Doctest
Рассмотрим следующий код (все исходники целиком лежат здесь):
{- | 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-документацию к этой программе можно так:
Как видите, в документации содержится пример использования функции sayHello в REPL. Пакет doctest предназначен для тестирования этих примеров:
Examples: 1 Tried: 1 Errors: 0 Failures: 0
Давайте прикрутим doctest к самому пакету, чтобы его было удобно запускать из Jenkins и все такое. Для этого в cabal-файле дописываем:
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 пишем:
main = doctest ["-isrc", "src/Main.hs"]
Проверяем:
$ cabal test
Нулевой код возврата означает, что все ОК. Смотрите, что получается. Теперь у нас есть тесты, проверяющие код, документацию, а также то, что документация соответствует коду! Ну разве не круто?
Дополнение: Мини заметки — выпуск 18
Метки: Haskell, Всячина, Функциональное программирование.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.