Моя первая программа на Haskell

7 июня 2011

Решил потратить время на изучение какого-нибудь функционального языка программирования. Их оказалось довольно много, но наиболее правильным (обсуждаемым, активно используемым, хорошо документированным, …) мне показался Haskell. Недавно вышло несколько книг на русском языке, посвященных этому языку (автор — Роман Душкин), что также повлияло на мой выбор.

Откуда во мне проснулся интерес к функциональным языкам, я расскажу как-нибудь в другой раз. Пока достаточно будет сказать, что у функциональных языков есть несколько интересных свойств. Да и просто ознакомиться с парадигмой, альтернативной вездесущему ООП, по крайне мере не повредит.

Не могу сказать, что я полностью разобрался в Haskell, но первую программку сваять получилось. Решил ее выложить, чтобы похвастаться была опорная точка для дальнейшего изучения языка. Кстати, я припоминаю, что в институте нас пытались учить какому-то функциональному языку (надо же, пригодилось!), возможно это даже был Haskell. К сожалению, когда я поинтересовался «а кому нужно это ваше функциональное программирование?», был получен ответ «ну, математикам так проще писать программы», после чего интерес к предмету пропал. Не верьте тому, чему учат в институтах! :)

Теперь немного ссылок и справочной информации:

  • GHC — наиболее каноничный компилятор Haskell. В комплекте с GHC идет программа ghci, запускающая компилятор в интерактивном режиме. Она часто используется в различных мануалах и облегчает отладку;
  • Hugs — популярный интерпретатор Haskell. Я им (пока?) не пользовался, но говорят, штука хорошая;
  • Hackage — онлайн-каталог модулей (пакеджей) для Haskell. Все недостающие модули нужно искать на нем;
  • Hoogle — поиск модулей и функций. Есть поиск функций по сигнатурам, что может пригодиться;
  • Cabal — система управления пакетами Haskell. Что-то вроде Perl‘овой утилиты cpan;
  • Haddoc — система комментирования, типа Doxygen и Javadoc;

Изучив немного мат части, я бросился устанавливать ghc, cabal и тд. Оказывается, так делать не надо. Существует готовый пакет программ The Haskell Platform, включающий в себя все необходимое — GHC, Cabal, Haddoc и самые полезные модули. Есть инсталлятор под Windows, порт под FreeBSD (pkg_add -r hs-haskell-platform), пакеты для MacOS и различных дистрибутивов Linux. Среды разработки в комплекте нет, но на ее роль вполне подойдет VIM или Geany (см пункт 6). Еще есть плагин для Eclipse, haskell-mode для Emacs и даже специальная (только для Хаскеля) IDE под названием Leksah. Я остановился на Geany.

В Haskell Platform для работы с регулярными выражениями предлагается использовать модуль Text.Regex.Posix. Проблема в том, что в нем реализованы регулярные выражения Posix, а я всю жизнь работал с регулярными выражениями Perl. Хоть разница между ними и небольшая, но мне так и не удалось понять, как в регулярных выражениях Posix будет выглядеть аналог (abc)(?:def)(ghi) и возможно ли его вообще написать. Также утверждается, что Text.Regex.Posix работает медленно, так что я решил установить Text.Regex.PCRE.

Установка новых модулей происходит очень просто:

cabal install regex-pcre

Правда, под Windows предварительно следует установить библиотеку pcre, а также воспользоваться флагами —extra-lib-dirs и —extra-include-dirs, иначе модуль не установится. Но такие сложности возникли только с Text.Regex.PCRE. Описание пакета и его правильное название можно найти на Хакедже.

Вообще, мне интересно, что мешало использовать в Cabal и самом языке одинаковые обозначения модулей? Например, позже выяснилось, что Data.String.Utils входит в пакет MissingH. Никакой связи между модулями языка и пакетами!

Дополнение: О статической линковке PCRE можно прочитать в 4-м параграфе статьи Кроссплатформенное GUI приложение на Haskell. Об использовании этой библиотекой напрямую вы можете прочитать в заметке Работа с регулярными выражениями в C/C++ при помощи библиотеки libpcre.

После установки можно немного потестировать модуль в ghci:

> :m +Text.Regex.PCRE
> "aaa" =~ "a" :: Bool
True
> "aaa" =~ "(a)(a)" :: Bool
True
> "Привет!!!" =~ "(?:При)(вет)" :: Bool
True
> "Привет!!!" =~ "(?:При)(вет)" :: (String,String,String,[String])
("","\1055\1088\1080\1074\1077\1090","!!!",["\1074\1077\1090"])
> :q

Наконец, мы добрались до кода. Приведенная программа скачивает видео с RuTube (пополняю коллекцию видео-доунлоудеров). Описание алгоритма было найдено на tradiz.org, от меня требовалось только закодить его.

-- rutube-dl.hs v 0.1.0
-- (c) Alexandr A Alexeev 2011 | http://eax.me/
-- based on http://goo.gl/Q1TMU

import Data.Char -- toLower
import Data.String.Utils -- replace
import Text.Printf -- printf
import Text.Regex.PCRE
import Network.HTTP
import System

-- начало программы
main = do
  args <- getArgs  -- получили аргументы
  parseArgs args  -- обрабатываем их
 
-- проверяем количество аргументов и выводим usage;
parseArgs :: [String] -> IO ()

-- если число аргументов - два или больше:
parseArgs (url:outFile:xs) = do
  let xmlUrl = urlToXmlUrl url
  xmlData <- httpGet xmlUrl
  let cmd = genCmd (xmlToRtmpUrl xmlData) outFile
  if cmd == "" then do
    putStrLn $ "Failed to parse url!"
    exitWith $ ExitFailure 1
  else do
    putStrLn $ "cmd: " ++ cmd
    exitCode <- system cmd
    putStrLn $ "rtmpdump terminated, exit code = " ++
               show exitCode
 
-- если передано меньше двух аргументов
parseArgs _ = do
  progName <- getProgName
  putStrLn $ "Usage: " ++ progName ++ " <url> <outfile>"
  exitWith $ ExitFailure 2
 
-- скачиваем заданную страницу
httpGet :: String -> IO(String)
httpGet "" = do
  return ""
httpGet url = do
  query <- simpleHTTP (getRequest url)
  body <- getResponseBody query
  return body

-- преборазуем rtmp-ссылку и имя выходного файла в команду
genCmd :: String -> String -> String
genCmd rtmpUrl outFile =
  let regex = "(?i)^(rtmp://[^\"/]+/)([^\"]*?/)(mp4:[^\"]*)$"
      match = rtmpUrl =~ regex :: [[String]]
  in case match of
    [[_, rtmp, app, playPath]] ->
      let live = if app == "vod/" then " --live" else ""
          -- кавычка - нормальная часть имени
          outFile' = replace "\"" "\\\"" outFile in
      -- на самом деле ничего не выводим, как sprintf в сях
      printf ( "rtmpdump --rtmp \"%s\" --app \"%s\" --playpath \"%s\""
        ++ " --swfUrl http://rutube.ru/player.swf --flv \"%s\"%s" )
        rtmp app playPath outFile live
    _ -> ""

-- выдираем rtmp-ссылку из xml файла
xmlToRtmpUrl :: String -> String
xmlToRtmpUrl xml =
  let regex = "(?i)<!\\[CDATA\\[(rtmp://[^\\]]+)\\]\\]>"
      match = xml =~ regex :: [[String]]
  in case match of
    [] -> ""
    [[_, rtmpUrl]] -> rtmpUrl

-- преобразование ссылки на видео в ссылку на xml
urlToXmlUrl :: String -> String
urlToXmlUrl url =
  let regex="(?i)^(?:http://)?rutube\\.ru/.*?[\\?&]{1}v=([a-f\\d]{32})"
      match = url =~ regex :: [[String]]
  in case match of
    [] -> ""
    [[_, hash]] -> "http://bl.rutube.ru/" ++ map toLower hash ++ ".xml"

Программа использует утилиту rtmpdump. В Windows ее можно просто положить в один каталог с exe’шником. Объяснять код, как я понимаю, бессмысленно, ибо он снабжен комментариями и ничего сверхсложного не делает. Поскольку это моя первая программа на Хаскеле, писал я ее не с нуля. Сначала был написан скрипт на Perl, после чего этот скрипт переписывался на Хаскель. Код скрипта:

#!/usr/bin/perl

# rutube-dl.pl v 0.1.0
# (c) 2011 Alexandr A Alexeev | http://eax.me/
# based on http://goo.gl/Q1TMU

use strict;

# проверяем наличие всех необходимых утилит
{
   my @depends = qw/wget rtmpdump/;
   my $not_found;
   for(@depends) {
     print "ERROR: $_ not found" and ++$not_found
       if(system("which $_ > /dev/null"));
   }
   exit 1 if($not_found);
}

my $url = shift;
my $outfile = shift;

die "Usage: $0 <url> <outfile>\n"
  unless $url and $outfile;

if($url !~ m#^(?:http://)?rutube\.ru/.*?[\?&]{1}v=([a-f\d]{32})#i) {
  die
    "Invalid url, something like\n".
    "  http://rutube.ru/tracks/1234567.html?v=01234abcd\n".
    "was expected.\n";
}

$url = "http://bl.rutube.ru/$1.xml";

print "Downloading $url...\n";
my $data = `wget -q $url -O -`;
die "Error: wget returns $?\n" if($?);

if($data !~ m#<!\[CDATA\[((?:rtmp|http)://[^\]]+)\]\]>#is) {
  die "Failed to parse $url\n";
}

$url = $1;
print "Video url: $url\n";

# предположительно, вариант с использованием http устарел
# TODO - проверить на 10 000 случайных роликах
if($url !~ m#^(rtmp://[^'/]+/)([^']*?/)(mp4:[^']*)$#i) {
  die "Failed to parse video url\n";
}

my ($rtmp, $app, $playpath) = ($1, $2, $3);
print "rtmp = $rtmp\napp = $app\nplaypath = $playpath\n";
$outfile =~ s/'/\'/g;

my $cmd= "rtmpdump --rtmp '$rtmp' --app '$app' --playpath '$playpath'";
$cmd.= " --swfUrl 'http://rutube.ru/player.swf' --flv '$outfile'";
$cmd.= " --live" if($app eq "vod/");

system($cmd);

Вот, пожалуй, и все, о чем я хотел сегодня рассказать. В качестве дополнительных источников информации могу посоветовать хаскеловский сборник рецептов и онлайн-учебник LearnYouAHaskell.com. Также обратите внимание на подборку русскоязычных материалов.

Дополнение: См также заметку Причины, по которым мне нравится Haskell.

Метки: , , .


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