воскресенье, 28 августа 2011 г.

Патч для ctags c лучшей поддержкой C++ и исправлениями

На днях залил новый патч для ctags, в котором было исправлено несколько багов, связанных c C++, в том числе один весьма серьезный - c неправильной обработкой символа '<'. Ну а самое главное - это добавление поддержки переменных, инициализируемых конструктором, например
int a( 90 );
SomeType b( "a_word" );
В текущей версии ctags такие переменные просто пропускаются, или иногда рассматриваются как прототипы функций (также см. описание бага здесь).

Патч можно взять здесь. Он может быть наложен как на последнюю стабильную версию ctags, так и на trunk. Вместе c патчем выложен тестовый файл test.cpp, на котором можно опробовать текущую и пропатченную версии ctags и сравнить их.

Если кто-то вдруг захочет опробовать патч и найдет регрессионные ошибки, то я попрошу по возможности прокомментировать их здесь (ну или на трекере ctags). Лично я проверял патч на довольно большом C++ проекте и серьезных регрессий не нашел. Напомню, что для включения в теги локальных переменных и прототипов функций, нужно в ctags добавить опцию --c++-kinds=+pl, кроме того, для получения максимальной информации о тегах можно добавить опцию c перечислением всех возможных полей: --fields=afmikKlnsStz.

пятница, 19 августа 2011 г.

Установка Firefox 6 и Thunderbird 6 в Fedora 14

Да, я так и не перешел на Fedora 15, так как не определился, чем заменить старый добрый Gnome 2. А пользоваться обновленными продуктами от Mozilla очень хочется. Так, недавно вышли очередные мажорные версии Firefox и Thunderbird, обе за номером 6. Естественно, в репозиториях Федоры их нет, зато они нашлись в репозитории Remi. Я не подключал данный репозиторий, поэтому просто скачал соответствующие пакеты (thunderbird, firefox и xulrunner6) и установил их. К сожалению, сразу же всплыла старая проблема с ужасными шрифтами без поддержки Byte Code Interpreter. Насколько я помню, исходный код библиотеки cairo был с изменениями включен в состав Firefox для сборки из-за какого-то странного бага с никому ненужными анимированными файлами gif. Благодаря этому, результирующий вид шрифтов стал зависеть не от того, какие системные библиотеки, отвечающие за рендеринг шрифтов, используются в вашей системе, а от того, какие библиотеки были у сборщика во время компиляции. Таким образом, товарищ Remi Collet при компиляции Firefox и Thunderbird использовал опцию, которая включает измененный вариант cairo, и кроме того, он располагал библиотеками для рендеринга шрифтов (fontconfig и libXft), которые поставлялись с Fedora 14 по умолчанию, что, в общем-то, логично и неудивительно.

Итак, проблема ясна. Будем лечить. Я не захотел собирать firefox и thunderbird с нуля, а решил воспользоваться SRPM пакетами из репозитория Remi. Найти их можно здесь. Очевидно, для того чтобы правильно пересобрать эти пакеты, нужно чтобы соответствующие правильные библиотеки были представлены в вашей  системе. Проще всего это сделать (если вы до сих пор этого не сделали и не используете репозиторий infinality), подключив репозиторий russianfedora-fixes и обновившись через yum update. После обновления будут установлены правильные fontconfig и libXft. Кроме того, для сборки нам понадобятся fontconfig-devel, libXft-devel и, разумеется, cairo-devel:
yum install fontconfig-devel libXft-devel cairo-devel
Перейдем к сборке rpm. Если вы еще не завели специального псевдопользователя для этой цели (а так рекомендует Федора), то сейчас самое время. От имени суперпользователя вводим
useradd mockbuild
Я назвал его mockbuild, хотя конкретное имя ничего не значит, можно выбрать на свой вкус. Задаем ему какой-нибудь пароль:
passwd mockbuild
Для того, чтобы пользователь-сборщик не появился в списке приглашения GDM, откроем файл /usr/share/gdm/gdm.schemas, найдем там строку <key>greeter/Exclude</key> и добавим в список пользователей ниже внутри тегов <default>...</default> нашего mockbuild.

Теперь нужно установить пакет для поддержки сборки rpm пакетов:
yum install rpmdevtools
Затем логинимся как mockbuild:
su - mockbuild
и создаем дерево директорий для сборки пакетов:
rpmdev-setuptree
Устанавливаем скачанные пакеты SRPM (от имени mockbuild, конечно) с помощью простой команды
rpm -ivh firefox-6.0-1.remi.src.rpm xulrunner6-6.0-1.remi.src.rpm thunderbird-6.0-1.remi.src.rpm
После этой команды исходники попадают в rpmbuild/SOURCES, а spec файлы - в директорию rpmbuild/SPECS. Переходим в директорию со spec файлами и правим. В файлах firefox6.spec и xulrunner6.spec после строки
echo "ac_add_options --enable-system-lcms" >> .mozconfig
добавляем строку
echo "ac_add_options --enable-system-cairo" >> .mozconfig
а в файле thunderbird.spec добавляем строку
ac_add_options --enable-system-cairo
где-нибудь между строками
cat <<EOF | tee -a .mozconfig
и
EOF
(например в самом конце, прямо перед строкой EOF).

Возможно, для сборки вам придется установить дополнительные пакеты. Так, мне пришлось установить autoconf213, lcms-devel, wireless-tools-devel и libvpx-devel. С помощью yum это сделать несложно.

Переходим к построению rpm, последовательно запуская команды
rpmbuild -ba xulrunner6.spec
rpmbuild -ba thunderbird.spec
В списке нет firefox, поскольку он зависит от построенного xulrunner6, поэтому, когда эти две команды отработают (а ждать нужно достаточно долго), переходим в директорию /home/mockbuild/rpmbuild/RPMS/your_arch, где your_arch - архитектура вашей системы (например, x86_64) и от имени суперпользователя обновляем свежесобранные пакеты:
rpm -Uvh thunderbird-6.0-1.fc14.x86_64.rpm  xulrunner6-6.0-1.fc14.x86_64.rpm xulrunner6-devel-6.0-1.fc14.x86_64.rpm
Если в вашей системе уже стоит xulrunner-devel, то его нужно будет предварительно удалить, так как он конфликтует с xulrunner6-devel. После установки новых пакетов возвращаемся в директорию SPECS и от имени mockbuild строим firefox:
rpmbuild -ba firefox6.spec
После завершения сборки снова возвращаемся в директорию RPMS и от имени суперпользователя обновляем firefox:
rpm -Uvh firefox-6.0-1.fc14.x86_64.rpm
После установки запускаем новые firefox и thunderbird и наслаждаемся правильным рендерингом шрифтов и проделанной работой :)

суббота, 13 августа 2011 г.

vim: практические шаги для комфортного использования 2

Продолжение темы советов по кастомизации vim, которые помогут повысить комфорт при использовании этого замечательного редактора. На этот раз ничего патчить не будем :)

1. Новый маппинг для плагина Mark. Плагин Mark позволяет легко подсвечивать слова или паттерны, соответствующие регулярным выражениям. В отличие от встроенного механизма поиска и подсветки, Mark позволяет одновременно подсвечивать разными цветами несколько слов или паттернов. К сожалению, маппинг, принятый в Mark по умолчанию, на мой взгляд, требует изменения. Именно благодаря неудачному маппингу я некоторое время не пользовался этим плагином, хотя он и был установлен мною собственноручно.

Итак, маппинг по умолчанию: \m - подсветить слово, \n - снять подсветку, \r - ввести паттерн для подсветки. Этот маппинг ужасно тормозил (требовалось ждать около секунды, чтобы изменения вступили в силу), и я сначала думал, что это особенность реализации подсветки в Mark. Но проблема оказалась весьма тонкой. Выяснилось, что маппинг пересекается с маппингом плагина c.vim и еще одного установленного мною плагина. Так, в c.vim определено множество маппингов, начинающихся с \n и \r (\nc, \ns, \ni и т.д.). Таким образом, при вводе символов, соответствующих маппингу Mark, vim некоторое время ждал, а не введет пользователь еще один символ, который можно будет интерпретировать как маппинг из c.vim - это нормальное поведение vim.

Чтобы в следующий раз не наступать на грабли, можно посмотреть все маппинги, начинающиеся с определенного символа, скажем с \n (первый символ \ как таковой в маппинг не входит, так как является спецсимволом - так называемым лидером). Для этого нужно в командной строке vim ввести
:map \n
В новом маппинге я задействовал сочетание клавиш Ctrl-m:
nmap <C-m><C-m> <Plug>MarkSet
nmap <C-m>n     <Plug>MarkClear
nmap <C-m>r     <Plug>MarkRegex
vmap <C-m><C-m> <Plug>MarkSet
vmap <C-m>r     <Plug>MarkRegex
nmap <C-m>*     <Plug>MarkSearchCurrentNext
nmap <C-m>#     <Plug>MarkSearchCurrentPrev
nmap <C-m>/     <Plug>MarkSearchAnyNext
nmap <C-m>?     <Plug>MarkSearchAnyPrev
nmap <C-m>c     <Plug>MarkAllClear
nmap <C-m>t     <Plug>MarkToggle
nmap <C-m>s     :MarkSave<CR>
nmap <C-m>l     :MarkLoad<CR>
В принципе, тут все понятно: вместо \m нужно, удерживая клавишу Ctrl, два раза нажать m, вместо \n - нажать Ctrl-m, а затем n, вместо \r - Ctrl-m, а затем r. Кроме того, я добавил маппинг для переключения подсветки, которого нет по умолчанию: <Ctrl-m>t.

Update. По иронии судьбы Ctrl-m тоже плохой маппинг, так как это сочетание в vim полностью соответствует клавише Enter. Так что если оставить такой маппинг, то Enter в нормальном режиме будет так же тормозить, как раньше тормозил сам Mark. Клавиша Enter потенциально используется в vim достаточно часто, например в плагине taglist при переходе к метке под курсором. Поэтому в приведенных выше маппингах следует заменить все вхождения <C-m> на какое-нибудь другое, неиспользуемое сочетание клавиш, например на <C-k>.

2. Подсветка областей кода, не соответствующих принятому стандарту. Несмотря на грозное название, здесь имеется ввиду очень простая вещь. Допустим, вам требуется убедиться, что код, который вы в данный момент редактируете или только что открыли, не содержит знаков табуляции, все строки завершаются не пробельными символами и их длина не превышает 80 знаков. Для этого вам нужно иметь возможность подсвечивать паттерны регулярных выражений, соответствующих указанным требованиям. Сами паттерны не такие уж сложные, однако вы предпочитаете делать это простым нажатием пары клавиш и, кроме того, таким же простым нажатием убирать подсветку.

Здесь я покажу, как это сделать с помощью простого маппинга ,s (запятая - s), который будет включать или выключать соответствующую подсветку в зависимости от текущего состояния (т.е. реализует то, что в английском языке называется простым словом toggle).

Прежде всего объявим переменную g:RightBorder, которая будет содержать максимальную разрешенную длину строки (т.е. 80), и новую область подсветки FormatHints, с помощью которой мы будем подсвечивать код, не соответствующий стандарту:
let g:RightBorder = 80
highlight FormatHints term=standout ctermfg=250 ctermbg=229
            \ guifg=Red guibg=White
Определение FormatHints разделено на две строки как результат следования внутренней дисциплине и установленному правилу непревышения строкой 80 символов :) Здесь с помощью аргументов ctermfg и ctermbg определена подсветка для терминалов с поддержкой 256 цветов, которая, на мой взгляд, вполне подходит для темных цветовых схем (светло-серый текст на бледно-желтом фоне: в конце концов мы подсвечиваем "запрещенные" участки кода, поэтому светлое на светлом здесь вполне уместно). Поскольку я не использую GUI vim, значения для guibg и guifg выбраны произвольно.

Теперь определим функции для включения и выключения подсветки:
fun<SID>formathints()
    if !exists("w:m1") || w:m1 == 0
        let w:m1 = matchadd('FormatHints''\%>'.g:RightBorder.'v.\+'-1)
        let w:m2 = matchadd('FormatHints''[\t]'-1)
        let w:m3 = matchadd('FormatHints''[\t \r]\+$'-1)
    endif
endfun
fun<SID>formathints_hide()
    if exists("w:m1") && w:m1 > 0
        silentcall matchdelete(w:m1)
        silentcall matchdelete(w:m2)
        silentcall matchdelete(w:m3)
        let w:m1 = 0
        let w:m2 = 0
        let w:m3 = 0
    endif
endfun
С помощью переменных w:m1, w:m2 и w:m3 определяются паттерны, соответствующие областям кода, которые мы хотим подсвечивать. Приставка к переменной w: в vim означает, что будут созданы отдельные инстансы этой переменной в каждом отдельном окне vim, соответственно, наше переключение подсветки будет работать только в активном окне.

Теперь определим команды для включения и выключения подсветки:
command -bar ShowFormatHints call <SID>formathints()
command -bar HideFormatHints call <SID>formathints_hide()
Опция -bar нужна для использования команд в составе сложных выражений, содержащих элементы <Bar> в следующем итоговом маппинге:
nmap <silent> ,s :if !exists("w:m1") <Bar><Bar> w:m1 == 0 <Bar>
            \ ShowFormatHints <Bar> echo "Show format hints" <Bar> else <Bar>
            \ HideFormatHints <Bar> echo "Hide format hints" <Bar> endif<CR>
Напомним, что элементы <Bar> в маппингах расширяются в символ |, следовательно, без учета echo "Show format hints" и echo "Hide format hints", этому маппингу будет соответствовать команда
:if !exists("w:m1") || w:m1 == 0 | ShowFormatHints | else | HideFormatHints | endif
Если оставить этот маппинг без модификатора <silent> (и без дополнительных команд echo), то эта малоинформативная строка будет выводиться в командную строку vim каждый раз при нажатии ,s - добавление <silent> и команд echo делают использование этого маппинга намного более удобным за счет более информативных сообщений.

Итак, цель достигнута. Теперь при нажатии на клавиатуре комбинации ,s будет происходить переключение подсветки проблемных участков кода с выводом сопутствующей информации в командную строку vim.

3. Маппинг для переключения ограничительной колонки (colorcolumn). Цель почти такая же, как и в предыдущем пункте: создать маппинг для включения/выключения цветовой колонки, которая будет располагаться на 81-ом знаке и предупреждать пользователя о возможном нарушении стандарта кодирования, ограничивающего длину строк 80 знаками. Но реализовать эту задачу теперь проще:
nmap <silent> ,m :if &colorcolumn == 81 <Bar> set colorcolumn= <Bar>
            \ elseif !&colorcolumn <Bar> set colorcolumn=81 <Bar> endif<CR>
Новый маппинг соответствует комбинации клавиш ,m. На этот раз нет смысла выводить сообщения на дисплей, поскольку наличие или отсутствие ограничительной колонки заметно и так.

4. Маппинг для переключения режима paste. Режим paste бывает полезен, когда нужно вставить из буфера обмена многострочный участок кода без дополнительного форматирования. Маппинг соответствует комбинации клавиш ,p и выглядит следующим образом:
nmap <silent> ,p :set paste! <Bar> set paste?<CR>
Команда set paste! переключает (toggle) текущий режим paste, а команда set paste? выводит его значение на дисплей.

воскресенье, 26 июня 2011 г.

vim: практические шаги для комфортного использования

Ниже приводятся четыре практических совета, которые являются в большей степени методами устранения недостатков, присущих vim и его известным плагинам, нежели самостоятельными решениями. Поскольку в трех советах речь пойдет о модификации плагинов, будут представлены патчи, соответствующие текущим версиям этих плагинов. Патчи можно накладывать автоматически или вручную. В первом случае достаточно сохранить патч в файле, скажем c.vim.patch, затем перейти в директорию, в которой находится файл c.vim (в данном случае это $HOME/.vim/plugin/) и выполнить команду:
patch -p0 < /path/to/c.vim.patch
1. Поддержка директорий проектов в c.vim. Под поддержкой проекта будем понимать возможность использования различных шаблонов c.vim в зависимости от того, какому проекту принадлежит редактируемый файл. Как правило, файлы проекта расположены в отдельной директории, а сами проекты могут предъявлять разные требования к оформлению элементов, таких как заголовки файлов. В c.vim имеется поддержка отдельных проектов с помощью макроса STYLE (см. документацию c.vim), однако ее реализация имеет недостатки. В частности, этот подход подразумевает централизованное хранение всех шаблонов в директории ~/.vim/c-support/templates/. Нормальная поддержка проектов должна предполагать возможность хранения независимого набора шаблонов в директории проекта - это более гибкий и естественный подход.

Следующий патч (относительно c.vim версии 5.13) реализует возможность хранения шаблонов c.vim в директории проекта:
--- c.vim 2011-01-26 00:12:31.000000000 +0300
+++ c.vim.new 2011-06-26 13:07:16.000000000 +0400
@@ -327,6 +327,10 @@
 
 let s:C_SourceCodeExtensionsList = split( s:C_SourceCodeExtensions, '\s\+' )
 
+if exists( 'g:C_ProjectDirs' )
+ let s:C_ProjectList   = split( g:C_ProjectDirs, ',' )
+endif
+
 "------------------------------------------------------------------------------
 
 "------------------------------------------------------------------------------
@@ -2702,6 +2706,16 @@
     let s:C_FileVisited   = []
   let messsage     = ''
   "
+
+  for proj in s:C_ProjectList
+   if stridx(getcwd(), proj) != -1
+    if filereadable( proj."/.cvim.templates/Templates" )
+     call C_ReadTemplates( proj."/.cvim.templates/Templates" )
+     return
+    endif
+   endif
+  endfor
+
   if s:installation == 'system'
    "
    if filereadable( s:C_GlobalTemplateFile )
Здесь вводится новая глобальная переменная C_ProjectDirs, которая представляет собой список проектов, разделенных запятыми, для которых могут быть предложены отдельные наборы шаблонов. Например, в C_ProjectDirs добавим проект /path/to/proj1, далее в указанной директории создадим директорию .cvim.templates/, скопируем туда шаблоны из ~/.vim/c-support/templates/ и отредактируем по вкусу. Теперь при редактировании файлов в директории /path/to/proj1 и любой ее поддиректории будут использоваться новые шаблоны. При редактировании файла, не входящего ни в один из указанных в C_ProjectDirs проектов, будут использоваться шаблоны по умолчанию.

Переменную C_ProjectDirs можно определить в ~/.vimrc, например так:
let g:C_ProjectDirs = "/path/to/proj1,".$PROJ2WORKDIR
Здесь указаны 2 проекта: /path/to/proj1 и проект, определенный через переменную среды PROJ2WORKDIR, проекты разделены запятой.

2. Проблема взаимодействия fuzzyfinder и taglist. Taglist - самый популярный плагин vim. Он предоставляет отдельное окно для навигации по тегам исходного кода в открытых файлах. Это очень старый плагин и, к сожалению, давно не обновлялся, поэтому ему присущи недостатки, в частности, его сложно использовать при наличии нескольких табов (которые стали доступны в vim 7.0). Fuzzyfinder - очень интересный плагин, который легко использовать для навигации по файловой системе, тегам, закладкам и др. Особенностью данного плагина является то, что он не использует отдельное окно, а открывает временное окно поиска в левом верхнем углу терминала по запросу. К сожалению, при определенных настройках taglist, эти два плагина не дружат, что выражается в выводе нового буфера в окно taglist после закрытия окна поиска fuzzyfinder (см. здесь). Починить это поведение можно с помощью простого изменения в файле ~/.vim/autoload/l9/tempbuffer.vim (библиотека l9 поставляется вместе с fuzzyfinder, патч приведен относительно версии l9 1.1):
--- tempbuffer.vim 2010-09-28 23:55:28.000000000 +0400
+++ tempbuffer.vim.new 2011-06-26 13:13:54.000000000 +0400
@@ -22,7 +22,9 @@
   endif
   if bufnr('%') == s:dataMap[a:bufname].bufNr && winnr('#') != 0
     " if winnr('#') returns 0, "wincmd p" causes ringing the bell.
-    wincmd p
+    " my cmt: 'wincmd p' works badly with taglist - using 'wincmd j' instead
+    "wincmd p
+    wincmd j
   endif
 endfunction
 
@@ -43,7 +45,8 @@
 " a:listener:
 "   a:listener.onClose(written)
 function l9#tempbuffer#openScratch(bufname, filetype, lines, topleft, vertical, height, listener)
-  let openCmdPrefix = (a:topleft ? 'topleft ' : '')
+  " my cmt: open fuf buffer above current window ('leftabove' instead 'topleft')
+  let openCmdPrefix = (a:topleft ? 'leftabove ' : '')
         \           . (a:vertical ? 'vertical ' : '')
         \           . (a:height > 0 ? a:height : '')
   if !exists('s:dataMap[a:bufname]') || !bufexists(s:dataMap[a:bufname].bufNr)
Данный патч делает еще одно, на мой взгляд, полезное изменение в поведении fuzzyfinder: теперь окно поиска будет открываться не в левом верхнем угла терминала, а вверху окна, в котором осуществляется редактирование.

3. Поддержка ack в grep.vim. ack - это grep для пограммистов. Это очень удобная утилита для поиска текста в директории исходников, она знает о служебных директориях CVS, Subversion и др. и игнорирует их, поддерживает регулярные выражения perl и обладает другими полезными свойствами. В vim существует плагин, который поддерживает ack, однако он реализует простейшую обертку встроенного в vim grep и "мусорит" в stdout. Плагин grep.vim реализован лучше, однако не обладает поддержкой ack. Следующий патч реализует это поддержку в grep.vim (относительно версии grep.vim 1.9):
--- grep.vim 2011-06-26 13:19:02.000000000 +0400
+++ grep.vim.new 2011-06-26 13:22:22.000000000 +0400
@@ -317,6 +317,10 @@
     let Agrep_Path = 'agrep'
 endif
 
+if !exists("Ack_Path")
+    let Ack_Path = 'ack'
+endif
+
 " Location of the find utility
 if !exists("Grep_Find_Path")
     let Grep_Find_Path = 'find'
@@ -731,7 +735,7 @@
         let grep_opt = g:Grep_Default_Options
     endif
 
-    if a:grep_cmd != 'agrep'
+    if a:grep_cmd != 'agrep' && a:grep_cmd != 'ack'
         " Don't display messages about non-existent files
         " Agrep doesn't support the -s option
         let grep_opt = grep_opt . " -s"
@@ -749,6 +753,9 @@
     elseif a:grep_cmd == 'agrep'
         let grep_path = g:Agrep_Path
         let grep_expr_option = ''
+    elseif a:grep_cmd == 'ack'
+        let grep_path = g:Ack_Path
+        let grep_expr_option = ' -H --nogroup --nocolor'
     else
         return
     endif
@@ -763,7 +770,7 @@
                         \ g:Grep_Shell_Quote_Char
     endif
 
-    if filenames == ""
+    if filenames == "" && a:grep_cmd != 'ack'
         if v:version >= 700
             let filenames = input("Search in files: ", g:Grep_Default_Filelist,
                         \ "file")
@@ -777,8 +784,12 @@
 
     " Add /dev/null to the list of filenames, so that grep print the
     " filename and linenumber when grepping in a single file
-    let filenames = filenames . " " . g:Grep_Null_Device
-    let cmd = grep_path . " " . grep_opt . " -n "
+    if a:grep_cmd != 'ack'
+        let filenames = filenames . " " . g:Grep_Null_Device
+        let cmd = grep_path . " " . grep_opt . " -n "
+    else
+        let cmd = grep_path . " " . grep_opt
+    endif
     let cmd = cmd . grep_expr_option . " " . pattern
     let cmd = cmd . " " . filenames
 
@@ -810,6 +821,9 @@
 command! -nargs=* -complete=file Ragrep
             \ call s:RunGrepRecursive('Ragrep', 'agrep', 'set', <f-args>)
 
+command! -nargs=* -complete=file Ack
+            \ call s:RunGrep('Ack', 'ack', 'set', <f-args>)
+
 if v:version >= 700
 command! -nargs=* -complete=file GrepAdd
             \ call s:RunGrep('GrepAdd', 'grep', 'add', <f-args>)
Теперь, для того чтобы выполнить рекурсивный поиск вхождения sample в текущей директории из vim, достаточно ввести команду
:Ack sample
Тот же поиск, но во всех файлах с расширением .cc в текущей директории:
:Ack sample *.cc
4. Навигация между окнами с помощью комбинаций клавиш Ctrl-Arrow. Это очень полезный совет, который можно найти на многих ресурсах в сети. Суть идеи в том, чтобы при наличии нескольких открытых окон переключаться между ними простой комбинацией клавиш Ctrl-Up и Ctrl-Down. Такое поведение требует некоторой настройки, в частности, значение bufhidden должно быть равным delete, чтобы при навигации скрытые буферы (т.е. те файлы, которые ранее редактировались в открытых окнах, но были в дальнейшем скрыты новыми файлами) не открывались в отдельных окнах. Итак, настройки для данного поведения, которые следует поместить в ~/.vimrc выглядят так:
set switchbuf=usetab
autocmd BufEnter * setlocal bufhidden=delete
nmap <C-up>     :sbn<CR>
nmap <C-down>   :sbp<CR>
nmap <C-left>   :tabp<CR>
nmap <C-right>  :tabn<CR>
Последние два маппинга добавляют возможность навигации между табами по комбинациям клавиш Ctrl-Left и Ctrl-Right и не имеют отношения к рассматриваемой далее проблеме.

А проблема заключается в том, что если некоторый файл попал по той или иной причине в список bufhidden, то при его повторном открытии попасть в его окно с помощью комбинаций Ctrl-Up или Ctrl-Down становится невозможно. Это означает, что, скорее всего, vim не удаляет файл из списка bufhidden при его повторном открытии. Лечится это добавлением еще одной авто-команды в ~/.vimrc:
autocmd BufEnter * if empty(&buftype) | setlocal buflisted | endif
Update: Кажется редактор blogger превратил символы табуляции в патчах в пробелы :( Так что для автоматического наложения патчей теперь придется использовать опцию -l, например так:
patch -l -p0 < /path/to/c.vim.patch

понедельник, 30 мая 2011 г.

CERN ROOT и надписи на русском языке

Как это ни странно, но проблема поддержки кириллицы в CERN ROOT действительно существует. Я не буду утверждать, что такая проблема присутствует в версии ROOT для Windows, но в Linux она актуальна. Однако, если учесть как эта поддержка реализована в нативной графической подсистеме ROOT, а также то, насколько глючно реализован Qt бекэнд, который поддерживает более широкий набор шрифтов, чем нативный вариант, такая проблема должна существовать и в Windows.

В качестве примера возьмем простой скрипт, с помощью которого можно создать следующее изображение:


Будем считать, что нашей целью является создание изображения в формате eps или pdf с целью дальнейшей вставки его в статью для публикации. Сам скрипт выглядит следующим образом:

void  createHisto( void )
{
    TCanvas *  c1 = new TCanvas();
    TH1 *      histo = new TH1F( "h1", "DCS vs. Energy", 10, 400., 600. );
    Double_t   content[] = { 0.0, 0.115, 0.145, 0.158, 0.170, 0.182, 0.180,
                             0.172, 0.154, 0.140, 0.132, 0.0 };
    Double_t   errors[] = { 0.0, 0.0015, 0.0012, 0.0008, 0.0016, 0.0023, 0.0018,
                            0.0013, 0.0014, 0.0006, 0.0002, 0.0 };
    histo->SetContent( content );
    histo->SetError( errors );
    histo->GetXaxis()->SetTitle( "E_{cm}, MeV" );
    histo->GetYaxis()->SetTitle( "d#sigma/d#Omega, mb/sr" );
    histo->SetStats( kFALSE );          // do not draw stats box
    histo->Draw();
    c1->SaveAs( "c1.eps" );
}

void histoTest( void )
{
    gROOT->SetStyle( "Plain" );
    gStyle->SetTitleBorderSize( 0 );    // do not draw frames around title
    gStyle->SetTitleOffset( 1.2, "xy" );// do not let axes' titles overlap ticks
    createHisto();
    if ( gROOT->IsBatch() )
        exit( 0 );
}
Обратите внимание, что в подписях к осям гистограммы используется вариант Latex для ROOT, стиль gStyle установлен в Plain, а рамка вокруг титула удалена. Отсутствие рамки вокруг титула упрощает решение задачи в первом и третьем подходах (см. ниже).

Для сохранения картинки наверху в файле c1.eps нужно создать скрипт histoTest.C с указанным содержимым и запустить root в пакетном режиме:
root -b histoTest.C
Пусть нашей задачей будет замена титула изображения на ДСР в зависимости от энергии и меток mb/sr, MeV и cm на мб/ср, МэВ и цм соответственно.

Наивная подстановка русских надписей в скрипт histoTest.C приведет к выводу в изображение c1.eps "кракозябр". Что же не так с ROOT? Нативный графический бэкенд ROOT не поддерживает кириллицу. Точка. Для решения проблемы ниже представлены три подхода, которые приводятся в порядке улучшения качества решения и полноты, хотя ни один из них не является идеальным. Идеи второго и третьего подходов взяты из обсуждений здесь и здесь.
  1. Наиболее простое решение - сохранить исходное изображение в векторном формате svg и отредактировать его в каком-нибудь векторном редакторе, например в inkscape. Минусы здесь очевидны. Редактировать придется вручную. Кроме изменения текста придется изменять обрамляющие его элементы, например компоненты рамки вокруг титула (которую мы предусмотрительно убрали в исходном скрипте).
  2. Использование Qt бэкенда (Qt layer) . Это был бы идеальный вариант, если бы не невообразимая глючность данного бэкенда. Из основных глюков стоит упомянуть: полупрозрачный или полностью прозрачный фон и шрифт абсолютно всех меню, невозможность использовать диалог сохранения файлов (!), невозможность сохранить валидное изображение в форматах eps и pdf (!). В данном подходе пришлось сохранять изображение как Save -> c1.png. Для использования Qt бэкенда нужно записать в файл ~/.rootrc следующие строки:
    Gui.Backend:                $(ROOTGUI)
    Gui.Factory:                $(ROOTGUI)
    Gui.DefaultFont:            -*-helvetica-medium-r-*-*-12-*-*-*-*-*-iso8859-5
    Gui.MenuFont:               -*-helvetica-medium-r-*-*-12-*-*-*-*-*-iso8859-5
    Gui.MenuHiFont:             -*-helvetica-bold-r-*-*-12-*-*-*-*-*-iso8859-5
    Gui.DocFixedFont:           -*-courier-medium-r-*-*-12-*-*-*-*-*-iso8859-5
    Gui.DocPropFont:            -*-helvetica-medium-r-*-*-12-*-*-*-*-*-iso8859-5
    Gui.IconFont:               -*-helvetica-medium-r-*-*-10-*-*-*-*-*-iso8859-5
    Gui.StatusFont:             -*-helvetica-medium-r-*-*-10-*-*-*-*-*-iso8859-5
    
    Использование переменной среды ROOTGUI позволяет сохранить нативный бэкенд по умолчанию (однако придется добавить строку
    export ROOTGUI=native
    
    в ~/.bash_profile). Для того, чтобы запустить root с Qt бэкендом нужно ввести
    ROOTGUI=qt root histoTest_qt.C
    
    Скрипт histoTest_qt.C выглядит так:
    void  createHisto( void )
    {
        //TCanvas *  c1 = new TCanvas();
        TH1 *      histo = new TH1F( "h1", "´ÁÀ Ò ×ÐÒØáØÜÞáâØ Þâ íÝÕàÓØØ",
                                     10, 400., 600. );
        Double_t   content[] = { 0.0, 0.115, 0.145, 0.158, 0.170, 0.182, 0.180,
                                 0.172, 0.154, 0.140, 0.132, 0.0 };
        Double_t   errors[] = { 0.0, 0.0015, 0.0012, 0.0008, 0.0016, 0.0023, 0.0018,
                                0.0013, 0.0014, 0.0006, 0.0002, 0.0 };
        histo->SetContent( content );
        histo->SetError( errors );
        histo->GetXaxis()->SetTitle( "E_{æÜ}, ¼í²" );
        histo->GetYaxis()->SetTitle( "d#sigma/d#Omega, ÜÑ/áà" );
        histo->SetStats( kFALSE );          // do not draw stats box
        histo->Draw();
        //c1->SaveAs( "c1_qt.eps" );
    }
    
    void histoTest_qt( void )
    {
        gROOT->SetStyle( "Plain" );
        gStyle->SetTitleBorderSize( 0 );    // do not draw frames around title
        gStyle->SetTitleOffset( 1.1, "xy" );// do not let axes' titles overlap ticks
        createHisto();
        if ( gROOT->IsBatch() )
            exit( 0 );
    }
    
    Заметили "кракозябры" внутри скрипта? Это еще одно неудобство. Скрипт выведен в общепринятой на сегодня кодировке UTF-8, а был записан в кодировке ISO-8859-5. В vim сохранение файла в данной кодировке обеспечивается командой
    :set fenc=iso-8859-5
    
    Однако при повторной загрузке этого файла в vim появятся всё те же "кракозябры" и новые русские записи не удастся добавить без полного переписывания русских символов заново. Возможно существуют редакторы, в которых проблема перевода в разные кодировки решается проще.

    Еще один интересный момент. Создание канваса c1 в данном варианте закомментировано. Если оставить его незакомментированным, то диапазон оси ординат изменится, и на месте гистограммы окажется чистое белое поле (!). Это еще раз подтверждает тот факт, что с текущей реализацией Qt бэкенда нужно быть очень осторожным.

    Изображение, полученное с помощью Qt бэкенда, выглядит так:


    Заметьте также, что Qt бэкенд применил преобразования локали: вместо точек в вещественных числах на координатных осях стоят запятые. 
  3. Третий подход основан на Latex и двух латеховских модулях - standalone и psfrag. Оба модуля можно найти на ctan.org. В пакет texlive для Fedora 14 они не входят, однако существует отдельный репозиторий texlive, в котором эти модули присутствуют. Информация об этом репозитории здесь. Преимущество данного подхода в том, что он полностью автоматизирован. Скрипт histoTest_latex.C выглядит так:
    void  createHisto( void )
    {
        TCanvas *  c1 = new TCanvas();
        TH1 *      histo = new TH1F( "h1", "CT", 10, 400., 600. );
        Double_t   content[] = { 0.0, 0.115, 0.145, 0.158, 0.170, 0.182, 0.180,
                                 0.172, 0.154, 0.140, 0.132, 0.0 };
        Double_t   errors[] = { 0.0, 0.0015, 0.0012, 0.0008, 0.0016, 0.0023, 0.0018,
                                0.0013, 0.0014, 0.0006, 0.0002, 0.0 };
        histo->SetContent( content );
        histo->SetError( errors );
        histo->GetXaxis()->SetTitle( "CX" );
        histo->GetYaxis()->SetTitle( "CY" );
        histo->SetStats( kFALSE );          // do not draw stats box
        histo->Draw();
        c1->SaveAs( "c1_latex.eps" );
    }
    
    void histoTest_latex( void )
    {
        gROOT->SetStyle( "Plain" );
        gStyle->SetTitleBorderSize( 0 );    // do not draw frames around title
        gStyle->SetTitleOffset( 1.2, "xy" );// do not let axes' titles overlap ticks
        createHisto();
        if ( gROOT->IsBatch() )
            exit( 0 );
    }
    
    Главным отличием от исходного скрипта является то, что оригинальные титул и подписи к осям заменены метками CT, CX и CY, которые будут обрабатываться в теховском скрипте с помощью psfrag. Идея psfrag весьма проста - заменить метки в исходном ps или eps файле указанными значениями. Отмечу сразу, что метки для подстановки следует выбирать короткие, иначе psfrag может сгенерировать ломаный файл dvi. Скрипт c1_latex.tex выглядит так:
    \documentclass{standalone}
    
    \usepackage[T2A]{fontenc}
    \usepackage[utf8x]{inputenc}
    
    \usepackage[dvips]{graphicx,color}
    \usepackage{psfrag}
    
    \begin{document}
    \psfrag{CT}[lt][lt][1.8]{\bf ДСР в зависимости от энергии}
    \psfrag{CX}[rt][rt][1.4]{\bf $\mathbf{E_{\mbox{цм}}}$, МэВ}
    \psfrag{CY}[rt][rt][1.4]{\bf $\mathbf{d\sigma/d\Omega}$, мб/ср}
    \includegraphics{c1_latex.eps}
    \end{document}
    
    О параметрах psfrag можно прочитать в документации к данному пакету; кратко, первый параметр в фигурных скобках является заменяемой меткой, последний параметр в фигурных скобках - подстановкой, первые два параметра в квадратных скобках после метки - позиционными якорями, третий параметр в квадратных скобках - коэффициентом масштабирования шрифта. Пакет standalone позволяет сохранить исходные размеры изображения.

    Русификация надписей может быть произведена с помощью отдельного шелл-скрипта mkrus:
    #!/bin/sh
    # source root macro
    root -b histoTest_latex.C
    # run psfrag
    latex c1_latex
    # make pdf
    dvips c1_latex
    ps2pdf c1_latex.ps
    # delete temporary files
    rm c1_latex.eps c1_latex.ps c1_latex.dvi c1_latex.aux c1_latex.log
    # make eps
    pdftops -eps c1_latex.pdf
    
    Этот скрипт создает два изображения: c1_latex.eps и c1_latex.pdf, которые выглядят так:


    В данном изображении используется шрифт Serif - просто в моем дистрибутиве texlive не нашлось русского Sans Serif шрифта c кодировкой T2A (которая используется в c1_latex.tex). Если необходим шрифт Sans Serif, нужно установить его из внешних источников.

суббота, 28 мая 2011 г.

Fedora 15 и wicd

Так уж получилось, что на моем ноутбуке Lenovo B560 вместо NetworkManager установлен wicd: просто на момент покупки (в феврале этого года) NetworkManager не умел работать с броадкомовским сетевым интерфейсом, установленном в B560 (да и сейчас наверное не умеет); кcтати и дрова для этого драйвера пришлось брать проприетарные - из rpmfusion (broadcom-wl). Как бы то ни было, но wicd мне вполне понравился и менять его обратно на NetworkManager я не собираюсь.

После того, как совсем недавно вышел релиз Fedora 15, я решил опробовать его на данном ноутбуке, перед тем как переносить на десктоп. Теперь я могу сказать совершенно точно, что обновлять десктоп с Fedora 14 до Fedora 15 я не буду, подожду следующего релиза. И, конечно же, огромное спасибо за такое решение хочется сказать разработчикам Gnome 3 :) Ну да ладно, это тема отдельного разговора.

Итак, вернемся к B560 и wicd. После обновления до Fedora 15 на ноутбуке отвалился wi-fi. Апплет wicd-gtk ругался на dbus и отказывался сканировать wi-fi источники. Первое, что удалось установить, это отсутствие модуля wl в списке загруженных модулей ядра. Оказывается, rpmfusion еще не удосужились обновиться до Fedora 15 (на момент написания этих строк репозитория rpmfusion Fedora 15 так и не появилось). В rpmfusion rawhide я этого драйвера тоже на тот момент не нашел (хотя сейчас он там уже присутствует). Поэтому я снес akmod-wl и kmod-wl и переустановил их из russianfedora. Команда modprobe wl вернула броадкомовскую железяку к жизни.

Однако это никак не повлияло на работу апплета wicd-gtk, который упорно ругался на dbus. Как выяснилось, демон wicd, который на Fedora 15 управляется новой системой загрузки systemd, умирал сразу же, как только systemd его стартовал. В багзилле Red Hat имеется соответствующий баг #327829, который был создан в апреле этого года и почему-то ни разу не комментировался. Если кому-то интересно, скрипт systemd для запуска демона wicd находится в файле /lib/systemd/system/wicd.service. Проверка статуса осуществляется командой systemctl status wicd.service, аналогично запуск и остановка - stop и start. Хотя старая добрая service wicd status тоже работает, просто перенаправляя действия на systemctl (обратите внимание, что в systemctl сначала указывается команда, а потом сервис). Как бы то ни было, но возиться с этим мне было некогда, мне нужен был рабочий wi-fi сразу после загрузки.

Очевидное решение - поместить строку /usr/sbin/wicd в файл /etc/rc.local - сработало. Остается ждать, когда разработчики починят баг #327829 и вернуть управление демоном wicd системе systemd.

воскресенье, 15 мая 2011 г.

Сравнение подходов в реализации алгоритмов на C++ и Haskell

Классический пример краткой и ясной реализации алгоритма на Haskell - это быстрая сортировка:
quickSort [] = []
quickSort (x:xs) = quickSort [ y | y <- xs, y < x ] ++ [ x ] ++
                   quickSort [ y | y <- xs, y >= x ]
Обычно рядом с этим кодом приводят какую-нибудь 20-строчную реализацию быстрой сортировки на C или C++. И конечно же сравнение идёт не в пользу C++. Откровенно говоря, здесь заключена немалая доля лукавства: как можно сравнивать высокоуровневые определители списков (list comprehensions - выражения в квадратных скобках в данном примере) с низкоуровневыми итерациями в коде C++? С другой стороны, определители списков являются встроенным механизмом Haskell, поэтому подобное сравнение всё-таки допустимо.

Ниже приводятся реализации одного алгоритма на C++ и Haskell. Алгоритм был взят из книги Ф. Меньшикова "Олимпиадные задачи по программированию". Задача формулируется следующим образом:
Дана последовательность из N целых чисел чисел. Необходимо удалить из последовательности минимальное количество чисел так, чтобы оставшаяся часть последовательности оказалась строго возрастающей. Иными словами нужно найти самую длинную возрастающую подпоследовательность.
Базовый алгоритм для решения этой задачи приводится в этой же книге:
Начиная с первого элемента последовательности и заканчивая последним найти максимальную длину возрастающей подпоследовательности из предшествующих элементов, меньших данного, и прибавить к ней 1. Эта величина будет соответствовать максимальной длине возрастающей подпоследовательности, которую можно построить начиная с 1-ого элемента исходной последовательность вплоть до данного элемента. Так, для 1-ого элемента последовательности из предшествующих элементов не существует, поэтому ему соответствует длина 1. Для произвольного i-ого элемента нужно совершить обратный проход к началу последовательности, найти меньший по значению элемент с максимальным значением длины и прибавить к нему 1. После того как длины найдены (при условии, что они сохранены в некотором вспомогательном массиве), нужно в исходной последовательности найти элемент, которому соответствует  максимальное значение длины и, двигаясь к началу последовательности, находить первые элементы, для которых соответствующая длина уменьшилась на единицу. Эти элементы (вместе с исходным) и будут составлять искомую подпоследовательность наибольшей длины.
Несколько замечаний. Во-первых, существуют модификации этого алгоритма, в которых финальный проход от элемента, которому соответствует максимальная длина возрастающей подпоследовательности к началу исходной последовательности не требуется. Поскольку моей целью является не написание самого оптимального алгоритма, а качественное сравнение подобных реализаций на C++ и Haskell, то я использовал базовый алгоритм. Во-вторых, очевидно, что в базовом варианте класс этого алгоритма соответствует O(n^2), так как присутствуют вложенные итерации (обратные проходы к началу последовательности) внутри базовой итерации по всем элементам. В-третьих, могут существовать несколько подпоследовательностей максимальной длины, нам нужно выбрать любую из них.

Итак, реализация этого алгоритма на C++ выглядит следующим образом:
typedef std::vector< int >  VIn;

typedef VIn::value_type     VInElem;

typedef std::vector< int >  VCnt;

void  findLongestIncSeq( const VIn &  vIn, VIn &  vRes )
{
    VCnt                 vCnt( vIn.size() );
    VCnt::iterator       vCntIt( vCnt.begin() );
    VIn::const_iterator  vMaxIt( vIn.begin() );
    VCnt::iterator       vCntMaxIt( vCnt.begin() );
    int                  maxLen( 0 );

    for ( VIn::const_iterator  k( vIn.begin() ); k != vIn.end(); ++k, ++vCntIt )
    {
        int                           maxVal( 0 );
        VCnt::const_reverse_iterator  vCntBackIt( vCntIt );

        for ( VIn::const_reverse_iterator  l( k ); l != vIn.rend();
                                                            ++l, ++vCntBackIt )
        {
            if ( *k <= *l )
                continue;

            if ( *vCntBackIt > maxVal )
                maxVal = *vCntBackIt;
        }

        *vCntIt = maxVal + 1;
        if ( *vCntIt > maxLen )
        {
            maxLen = *vCntIt;
            vMaxIt = k;
            vCntMaxIt = vCntIt;
        }
    }

    std::cout << "Lengths:  ";
    printVec( vCnt );

    if ( vMaxIt == vIn.begin() )
        return;

    std::stack< VInElem >   reverseSeq;
    VCnt::reverse_iterator  vCntBackIt( vCntMaxIt + 1 );

    for ( VIn::const_reverse_iterator  k( vMaxIt + 1 ); k != vIn.rend();
                                                            ++k, ++vCntBackIt )
    {
        if ( *vCntBackIt == maxLen )
        {
            reverseSeq.push( *k );
            --maxLen;
        }
    }

    while ( ! reverseSeq.empty() )
    {
        vRes.push_back( reverseSeq.top() );
        reverseSeq.pop();
    }
}
Вспомогательная функция printVec() реализуется так:
template  < typename  VElem >
struct  PrintVec
{
    void  operator()( VElem  value )
    {
        std::cout << value << " ";
    }
};

template  < typename  Vec >
void  printVec( Vec  v )
{
    std::for_each( v.begin(), v.end(), PrintVec< typename Vec::value_type >() );
    std::cout << std::endl;
}
В функцию findLongestIncSeq() передаются ссылки на исходную последовательность vIn и последовательность vRes, которую нужно заполнить. Внутри функции создаем вспомогательный массив целых чисел vCnt, в котором будем хранить длины максимальной возрастающей последовательности до i-ого элемента включительно. Внутри первого цикла for происходит основная итерация, в которой заполняется массив vCnt, кроме того попутно вычисляются максимальная длина возрастающей подпоследовательности maxLen и соответствующие ей элементы в vIn и vCnt: vMaxIt и vCntMaxIt - они будут нужны для определения элемента, с которого нужно начать финальный проход по исходной последовательности вниз к первому элементу. Этот финальный проход осуществляется в последнем цикле for функции findLongestIncSeq(), в котором заполняется вспомогательный стек reverseSeq. Теперь, чтобы заполнить искомую последовательность vRes, нужно опустошить reverseSeq и сложить его элементы в vRes, что и происходит в последнем цикле while.

Я не буду приводить функцию main() - это детали. Укажу только, что для тестовой последовательности [67 5 34 89 65 12 90 75 8 9 3] выводится следующий результат:
Original: 67 5 34 89 65 12 90 75 8 9 3 
Lengths:  1 1 2 3 3 2 4 4 2 3 1 
Sequence: 5 34 65 90
В строке Original выводится исходная последовательность, в строке Lengths - вспомогательная последовательность длин возрастающих подпоследовательностей, а в строке Sequence - результирующая возрастающая подпоследовательность максимальной длины.

А теперь внимание. Подобный алгоритм написанный на Haskell (я привожу весь код вместе с тестовой частью, так что его можно сразу скомпилировать):
{-find longest increasing subsequence-}

getPrevLength [] = 0
getPrevLength x  = maximum $ doGetLengths x

doGetLengths []     = []
doGetLengths (x:xs) = getPrevLength [ y | y <- xs, y < x ] + 1 : doGetLengths xs

getLengths = reverse . doGetLengths . reverse

doCutTail [] _                                          = []
doCutTail a@(x:xs) maxLen
    | getPrevLength [ y | y <- xs, y < x ] + 1 < maxLen = doCutTail xs maxLen
    | otherwise                                         = a

cutTail [] = []
cutTail x  = reverse $ doCutTail ( reverse x ) ( maximum $ getLengths x )

doFindLongestIncSec' [] _   = []
doFindLongestIncSec' (x:xs) maxLen
    | maxLen - prevLen == 1 = x : doFindLongestIncSec' xs prevLen
    | otherwise             = doFindLongestIncSec' xs maxLen
        where prevLen = getPrevLength [ y | y <- xs, y < x ] + 1

doFindLongestIncSec [] = []
doFindLongestIncSec x  =
    doFindLongestIncSec' ( reverse x ) ( maximum ( getLengths x ) + 1 )

findLongestIncSec = reverse . doFindLongestIncSec . cutTail

{-do some testing-}

doTest x = do
    putStrLn $ "Original: " ++ show x
    putStrLn $ "Lengths:  " ++ ( show $ getLengths x )
    putStrLn $ "Trimmed:  " ++ ( show $ cutTail x )
    putStrLn $ "Sequence: " ++ ( show $ findLongestIncSec x )

main = mapM_ doTest [ [ 10, 5, 89, 16, 78, 67, 56, 34, 12, 10 ],
                      [ 67, 5, 34, 89, 65, 12, 90, 75, 8, 9, 3 ] ]
Я назвал этот алгоритм подобным, поскольку его невозможно реализовать тем же самым способом, что и в C++ (во всяком случае без использования монад). В Haskell отсутствуют итерации и состояния (то бишь переменные), и это означает, что создать вспомогательный массив длин подпоследовательностей и использовать его на том же уровне программного потока не удастся. По той же причине невозможно сначала запомнить индекс элемента с максимальным значением возрастающей подпоследовательности, а затем использовать его по своему усмотрению в параллельной последовательности вызовов функций. В Haskell возможность параллельной последовательности вызовов функций появляется только с использованием монад (см. реализацию doTest выше), а в общем случае вызовы функций могут быть только вложены друг в друга. Это означает, что обычно решение любой задачи в Haskell представляет собой вызов единственной функции, которая в свою очередь вызывает единственную функцию и т.д. Это создает впечатление решения одним махом. В самом деле, в приведенном коде всё решение заключается в вызове единственной функции findLongestIncSeq x. Параллельные вызовы getLengths и cutTail, организованные в doTest нужны только для демонстрации промежуточных состояний исходной последовательности и никоим образом не влияют на результат финального вызова findLongestIncSeq.

Приведенные выше особенности программирования на Haskell требуют более частого использования рекурсивных вызовов, чем в C++, где рекурсия чаще всего заменяется итерацией. Так, функция getPrevLength, которая используется в программе для поиска длины максимальной подпоследовательности, является косвенно рекурсивной благодаря вызову doGetLengths, которая в свою очередь является рекурсивной и косвенно, и явно. Функция cutTail возвращает подпоследовательность исходной последовательности от начала до последнего элемента, которому соответствует максимальная длина возрастающей подпоследовательности. Основная функция findLongestIncSeq является композицией функций reverse, doFindLongestIncSeq и cutTail - это отражено в ее определении.

Нельзя не заметить, что в коде часто употребляется обращение последовательности с помощью reverse. Если учесть, что скорость reverse должна соответствовать O(n), это должно настораживать. Проблема в том, что семантика конструктора списков Haskell предполагает добавление элементов в начало, а не в конец списка, а в нашем алгоритме чаще всего приходится выделять последний элемент подпоследовательности и проходить вниз к началу списка. Таким образом, от reverse можно избавиться, видоизменив исходный алгоритм. Однако проблема обращения списка не такая уж серьезная, как кажется сначала. Дело в том, что reverse не вызывается рекурсивно, а только счётное количество раз, соответственно общая скорость алгоритма остается прежней - O(n^2).

Еще одно важное замечание. Что если мы захотим изменить тип элементов последовательности? Например, захотим искать максимальную подпоследовательность вещественных чисел или строк. В коде на C++ такой вариант предусмотрен с помощью объявления типов VIn и VInElem - стоит поменять тип элемента вектора и всё должно заработать. В коде на Haskell вообще ничего менять не нужно! И это не потому, что в Haskell нет статической проверки типов данных, она там есть, причем более сильная, чем в C++. Просто в данном случае работает параметрический полиморфизм, реализованный в Haskell.
Осталось привести вывод программы на Haskell:
Original: [10,5,89,16,78,67,56,34,12,10]
Lengths:  [1,1,2,2,3,3,3,3,2,2]
Trimmed:  [10,5,89,16,78,67,56,34]
Sequence: [5,16,34]
Original: [67,5,34,89,65,12,90,75,8,9,3]
Lengths:  [1,1,2,3,3,2,4,4,2,3,1]
Trimmed:  [67,5,34,89,65,12,90,75]
Sequence: [5,34,65,75]
Видно, что последний элемент одной и той же тестовой последовательности отличается от результата программы на C++. Это связано с тем, что в C++ варианте последний проход начинался от первого элемента с максимальным значением возрастающей подпоследовательности, а в Haskell варианте - с последнего. Как я уже говорил, могут существовать несколько возрастающих подпоследовательностей максимальной длины, поэтому оба случая являются правильным решением задачи.