20090330

Скрипты на Хаскеле (пробую писать)

Я, кажется, созрел, чтобы переходить от чтения книжек и статей про Хаскель к попыткам что-то на нём писать самому. Вначале какую-нибудь мелочь. Скрипты, в общем. Поскольку я уже как-то публиковал здесь bash-скрипт rss2lj (кросспост RSS в ЖЖ), то решил в качестве упражнения его переписать и улучшить. Думаю, получилось. В этой заметке расскажу о том, как писал. Ну и о впечатлениях. Скрипт выложен на BitBucket и на Hackage.

Содержание:
 Задача состоит из кучи рутинных операций. Я думаю, именно поэтому, будет полезно и мне на будущее, и другим начинающим и пробующим, увидеть, как они выполняются на Хаскеле. В частности, по ходу дела я разобрался как
  • обрабатывать аргументы коммандной строки,
  • читать и писать файлы,
  • использовать регулярные выражения,
  • отсылать HTTP-запросы,
  • выполнять ввод-вывод в уникоде (UTF-8),
  • получать системное время.
Писать буду как начинающий — начинающим. На словах получается довольно долго, но сам код получился гораздо короче, чем эта статья (около 200 строк, считая комментарии, необязательные декларации типов, пустые строки и декларации импорта внешних модулей).

Хотя Хаскель язык компилируемый и строго типизированный, использовать его для таких дел вполне можно. Код получается примерно такой же, если не более, краткий, как на Python, а компилируется даже на лету достаточно быстро. Есть и особенности. Во-первых, вместо беззаботного duck-typing здесь — строгая типизация. Поэтому писать надо аккуратнее (но и ошибок при исполнении меньше). Однако в Хаскеле эта строгая типизация сделана на основе системы типов Хиндли–Миллнера и, в отличие от C++, под ногами не путается. Во-вторых, чтобы использовать преимущества функционального подхода (например, отложенные вычисления, частичное применение функций) нужно отделять чисто функциональную часть программы от императивных фрагментов. В простейшем случае, это означает необходимость отделить операции ввода-вывода от вычислений (преобразования информации). Переводя на Хаскель: функции ввода-вывода будут иметь монадный тип IO a, остальные же будут чистыми (без IO в типе).

Предварительное описание задачи и подхода

В моём примере можно выделить следущие операции ввода-вывода:
  • получение URL из аргументов командной строки,
  • чтение содержимого RSS или Atom фида по заданному URL,
  • чтение (и потом запись) файла со списком уже обработанных записей,
  • чтение файла с настройками доступа к учётной записи ЖЖ,
  • получение системного времени,
  • коммуникация с ЖЖ по установленному протоколу.
И соответственно следующие преобразования данных:
  • извлечение идентификаторов всех записей в фиде,
  • отсев уже обработанных записей,
  • извлечение заголовков, ссылок и текста оставшихся записей,
  • форматирование записей по заданному шаблону,
  • разбор файла с настройками.
Для разбора произвольных фидов я велосипед изобретать не стал, а воспользовался библиотекой feed. А для всех коммуникаций по HTTP протоколу использовал библиотеку curl (мне понравился её интерфейс). Обе библиотечки нашёл на Hoogle, а установил с помощью cabal. Из остальных зависимостей: нужен модуль Codec.Binary.UTF8.String (в убунту и дебиан он помещён в пакет libghc6-utf8-string-dev), модуль Text.Regex.Posix (соответственно, пакет libghc6-regex-posix-dev). Потом я сейчас заметил, что использовал urlEncode из Network.HTTP (у меня в ~/.cabal), хотя можно было обойтись пакетным escapeURIString (из Network.URI). То есть одна зависимость могла бы быть попроще.

В отдельный модуль я выделил всё, что касается связи связи с ЖЖ и его протокола (файл LjPost.hs). Собственно всю логику скрипта я поместил в другом файле (Feed2Lj.hs). Вспомогательную утилитку для тестирования модуля LjPost я поместил в RunLjPost.hs. Для использования скрипта она не нужна, я её использовал при его написании.

Модуль отправки сообщений в ЖЖ (LjPost)

Использование библиотеки Curl

Как я уже сказал, для работы по HTTP протоколу я использовал библиотечку curl. Соответственно, помещаю в списке импортов
import Network.Curl
а основную функцию оформляю так, всё это достаточно «императивно»:
postToLj ljuser ljpass subj msg = withCurlDo $ do
curl <- initialize
...
Функция withCurlDo должна охватывать все вызовы к curl и отвечает за инициализацию и деинициализацию библиотеки; initialize собственно и позволяет к библиотеке потом обращаться. Собственно HTTP запрос делается так (запрашиваю аутентификационный токен ЖЖ):
  r <- do_curl_ curl ljFlatUrl getChallengeOpts :: IO CurlResponse
Т.е. используем do_curl_, чтобы получить данные HTTP-ответа; результат (HTTP-ответ) связываю (<-) с переменной r; аргументы do_curl_ были определены мной ранее, URL ЖЖ-API
ljFlatUrl = "www.livejournal.com/interface/flat"
и собственно параметры запроса:
getChallengeOpts = CurlPostFields ["mode=getchallenge"] : postFlags
postFlags = [CurlPost True]
Дальнейшие действия определяются логикой протокола ЖЖ.

Разбор ответа ЖЖ

Во flat-протоколе, ответ сервера выглядит так:
ключ_1
значение_1
ключ_2
значение_2
...
Нужно, во-первых, проверять значение ключа success, во-вторых извлекать значения других ключей, для начала ключа challenge.

Поскольку здесь никакого ввода-вывода уже нет, эту часть кода вполне можно написать «функционально». Самый простой и универсальный сделать это, мне кажется, разбить тело ответа (respBody) на строчки (lines), преобразовать их в ассоциативный список (list2alist) и поискать в нём нужный ключ (lookup), получив, может быть (монада Maybe), значение:
lookupLjKey :: String -> CurlResponse -> Maybe String
lookupLjKey k = ( lookup k . list2alist . lines . respBody )
При этом функция преобразования списка в ассоциативный список простая двухстрочная рекурсия:
list2alist :: [a] -> [(a,a)]
list2alist (k:v:rest) = (k,v) : list2alist rest
list2alist _ = []
Всё, мы написали всё необходимое, чтобы разбирать ответы сервера.

Вспомогательная функция, проверяем, успешен ли был запрос (тогда и только тогда, когда в ответе есть ключ success со значением OK):
isSuccess :: CurlResponse -> Bool
isSuccess = (=="OK") . fromMaybe "" . lookupLjKey "success"
Мы определили isSuccess композицией трёх функций. lookupLjKey возвращает монаду Maybe String. Функция fromMaybe достаёт из неё строковое значение. Функция сравнения (==) записана в префиксной форме и сравнивает значение со строкой «OK».

Прошу заметить, что вытащить из монады Maybe собственно значение всегда можно с помощью fromJust, но если там ничего нет (Nothing), то будет возбуждена ошибка. Здесь функция fromMaybe возвращает в такой ситуации значение по умолчанию (пустую строку), но в других местах скрипта я часто использую fromJust без проверок (т.е. при отсутствии значения скрипт будет прерываться). В программах посерьёзнее, я думаю, лучше всегда использовать функции maybe или fromMaybe, позволяющие использовать Maybe-значения, указав для них значения по-умолчанию.

Отправка сообщения в ЖЖ

Возвращаемся к функции postToLj и пишем, что если аутентификационный токе был успешно получен (isSuccess r), взять текущее время (timeopts <- currentTimeOpts, об этом ниже), подготовить запрос для публикациии сообщения (let opts = postOpts ...) и отправить. Результатом функции будет ответ на последний выполненный запрос:
  if (isSuccess r) 
then do
let challenge = fromJust $ lookupLjKey "challenge" r
timeopts <- currentTimeOpts
let opts = postOpts ljuser ljpass challenge subj msg timeopts
r <- do_curl_ curl ljFlatUrl opts :: IO CurlResponse
return r
else return r
Как всегда в Хаскеле, если сказал if — then, говори и else (с тем же типом).

Ещё одно «новичковое» замечание: в блоке do мы связываем переменные с монадным значением с помощью (<-) (это соответствует присваиванию в императивных языках), но определяем переменные чистыми выражениями с помощью (=). Вообще, (=) в Хаскеле почти всегда можно читать как «равно по определению». Как только я это понял — жить стало проще ;-)

Теперь подробности. Чтобы отправить сообщение, нужно сформировать POST-запрос согласно протоколу. В моём примере этим занимается функция
postOpts u p c subj msg topts =
CurlPostFields ("mode=postevent" : (authOpts u p c)
++ ["event=" ++ quoteOpt msg, "subject=" ++ quoteOpt subj,
"lineendings=unix", "ver=1"]
++ topts ) : postFlags
которая аналогичная getChallengeOpts, только список полей, которые нужно отослать, гораздо больше. И есть некоторые тонкости.

Во-первых, нужно защищать («квотировать») некоторые символы в отсылаемых значениях. Их немного, на помощь приходит определение функции с помощью шаблонов аргумента:
quoteOpt ('=':xs) = "%3d" ++ quoteOpt xs
quoteOpt ('&':xs) = "%26" ++ quoteOpt xs
quoteOpt (x:xs) = x : quoteOpt xs
quoteOpt [] = []
Одно дело сделано. Во-вторых, нужно по имени пользователя, паролю и аутентификационному токену подготовить все поля запроса, касающиеся аутентификации:
authOpts u p c = [ "user=" ++ quoteOpt u, "auth_method=challenge",
"auth_challenge=" ++ quoteOpt c,
"auth_response=" ++ quoteOpt (evalResponse c p) ]
Собственно ответ на токен рассчитывается в одну строчку:
evalResponse c p = smd5 ( c ++ (smd5 p) ) where smd5 = md5sum . fromString
Кроме этого нужно импортировать соответствующие функции преобразования уникодной строки в байт-строку UTF-8 и функцию вычисления MD5-суммы:
import Data.ByteString.UTF8 (fromString)
import Data.Digest.OpenSSL.MD5 (md5sum)
И в-третьих, нужно заполнить в запросе поля, касающиеся времени публикации (текущего времени). Импортируем:
import Data.Time
import System.Locale (defaultTimeLocale)
Берём текущее время:
currentTime = do
t <- getCurrentTime
tz <- getCurrentTimeZone
return $ utcToLocalTime tz t
Заметим, что функция эта связана с вводом-выводом и не является «чистой» (не возвращает одно и то же значение всякий раз). По этой причине я предпочёл не вызывать её из «чистой» postOpts, а передать уже готовый список опций, касающихся времени в postOpts из postToLj. Там, напомню, я писал:
timeopts <- currentTimeOpts
а currentTimeOpts определил так:
currentTimeOpts :: IO [String]
currentTimeOpts = do
t <- currentTime
let opts = [ "year=%Y", "mon=%m", "day=%d", "hour=%H", "min=%M" ]
return $ map (flip showTime t) opts
Т.е. взял текущее время и подставил его в каждый из списка форматов (ЖЖ хочет в таков виде). Вспомогательная функция преобразования времени в строку по формату выглядит так:
showTime = formatTime defaultTimeLocale
Эта функция двух (неуказанных) аргументов получена каррированием функции formatTime. В map я меняю местами её аргументы (flip), чтобы формат передавался последним, и «перчу» ещё раз текущим временем.

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

Чтение файла конфигурации

Где-то логин и пароль хранить надо, и самое простое, что приходит в голову, поместить его в файле настроек, написанном в виде
username=мойлогин
password=мойпароль
В коде скрипта указываю путь по-умолчанию к этому файлу:
ljPassFile = "~/.ljpass"
Читаем этот файл и делаем из него знакомый и удобный ассоциативный список:
readPassFile f = do
ljpass <- readFile f
return $ map (\(f,s) -> (f,tail s)) $ map (break (== '=')) $ lines ljpass
Поскольку файл заведомо небольшой, можно использовать простую в обращении readFile. Далее как обычно: режем на строки (lines), каждую строку разбиваем по первому знаку «равно» (map (break (== '='))), правим получившийся ассоциативный список список, откидывая знаки «равно» (λ-функция во втором map). Результат заворачиваем в IO-монаду (return) как того требует тип функции.

Почти готово. Для пущего удобства сделаем себе раскрытие тильды в пути к файлу:
expandhome ('~':'/':p) = do h <- getHomeDirectory ; return (h ++ "/" ++ p)
expandhome p = return p
и собственно функцию, которая, будет нам давать значение любого ключа из файла конфигурации:
readLjSetting key = do
passfile <- expandhome ljPassFile
s <- readPassFile passfile
return (lookup key s)
В этот раз нам надо добавить ещё две декларации импорта:
import IO
import System.Directory (getHomeDirectory)
Последний штрих: в объявлении модуля перечисляем экспортируемые вовне функции, а вспомогательные замалчиваем:
module LjPost (readLjSetting, postToLj, isSuccess, lookupLjKey, putLjKey) where
Наш модуль готов к использованию. Он позволяет нам задавать настройки доступа в файле конфигурации, понимает ЖЖ-протокол, поддерживает challenge-response аутентификацию и позволяет публиковать в ЖЖ сообщения. Меньше 100 строк кода, если не считать комментарии.

Обработка RSS/Atom фида (Feed2Lj)

Переходим к заключительной части рассказа. Скрипт Feed2Lj.hs берёт URL фида из командной строки, настройки ЖЖ из файла с настройками (для него там добавляем третью настройку, имя файла со списком уже обработанных записей), скачивает фид и отсеивает уже обработанные, необработанные преобразует в plain-text, форматирует по образцу и отсылает в ЖЖ, обновляя список обработанных записей. Теперь подробно.

Получение аргументов командной строки

Получить список аргументов просто, его даёт функция getArgs из System.Environment. У нас аргумент один, адрес фида, поэтому может сразу связать нужную переменную (url) с первым элементом списка, проигнорировав остальные:
  url:_ <- getArgs
Такое связывание по шаблону мне кажется очень элегантным приёмом.

Скачивание фида

На помощь опять приходит библиотечка curl. И опять связывание по шаблону, чтобы взять только интересующую нас часть результата:
  (_,rawfeed) <- curlGetString url []

Используем модуль LjPost для чтения настроек

В общем-то вся работа уже сделана, осталось только использовать функцию readLjSetting. У неё тип [Char] -> IO (Maybe [Char]), т.е. по строке она возвращает IO-монаду, внутри которой, может быть строка (значения настройки найдено и считано), а может и не быть (настройка не найдена). Поскольку у нас тут сразу две монады (IO и Maybe), одна в другой, то, чтобы вытащить просто (Just) значение, я поступаю так:
ljuser <- return fromJust `ap` readLjSetting "username"
т.е. функцию fromJust применяю внутри монады IO (ap из Control.Monad). Аналогично с остальными значениями из файла настроек. Кажется немного громоздно с непривычки, но не так уж сложно потом. Уверен, можно написать короче.

Чтение списка обработанных записей

Мой старый bash-скрипт писал ID записей в файл, одно на строчку, поэтому новый скрипт использует тот же формат (и тот же файл). Читаем файл и преобразуем в список строк:
sent_ids <- (return . lines) =<< readFile sentfile
Здесь, чтобы не вводить временную переменную, я явно указал функцию связывания вычислений (=<<). return требуется типом (=<<). Результат эквивалентен записи
tmp <- readFile sentFile
let sent_ids = lines tmp

Отсеиваем обработанные записи

Для начала разберём содержимое фида и подготовим список всех записей. Благодаря библиотечке feed это легко:
  let feed = fromJust $ parseFeedString rawfeed
let items = feedItems feed
Ну а отсеять уже обработанные можно с помощью filter:
  let newitems = reverse $ filter (isNotSent sent_ids) items
Функция-предикат получилась за счёт каррирования isNotSent:
isNotSent sent i = ((snd . fromJust . getItemId) i) `notElem` sent
Буквально: взять просто ID элемента (возможна ошибка), проверить, что не входит в список sent. Сразу подготовим список ID подлежащих обработке записей:
let new_ids = map ( snd . fromJust . getItemId) newitems

Отправляем запись в ЖЖ

Тупо используем уже написанный модуль LjPost. Если даны имя пользователя, пароль, шаблон записи для отправки и собственно запись:
postItem u p t i = do
let message = renderItem t i
let subj = fromJust $ getItemTitle i
r <- postToLj u p subj message
if isSuccess r
then putLjKey "url" r
else putLjKey "errmsg" r
Стоп-стоп-стоп! Какой ещё такой шаблон записи (t) и что делает renderItem? Объясняю: отослать запись нам надо в HTML-е, и хорошо бы можно было менять формат записи, не переделывая весь код. В общем, renderItem — это маленькая template engine, t — её шаблон. Я её опишу в следующих разделах статьи.

Вызываем из main для каждой записи из списка необработанных:
  let t = encodeString "<p>%text%</p><p>( <a href=\"%link%\" title=\"%title%\">дальше</a> )</p>"
mapM_ (postItem ljuser ljpass t) newitems
Здесь мы формируем список IO-действий и их последовательно исполняем (mapM_). То есть последовательно отсылаем все записи из нашего списка. Обратим ещё внимание на encodeString из Codec.Binary.UTF8.String, которая кодирует строку в UTF-8.

Форматирование по шаблону (маленькая template engine)

Напишем нашу маленькую функцию форматирования по шаблону. Пусть, допустим, все параметры шаблона будут представлены как «%параметр%», а спецсимвол «%» будет представлен в шаблоне как «%%». Параметры будет передавать ассоциативным списком, а шаблон — строчкой. На выходе — строчка с подставленными в шаблон параметрами:
renderTemplate _ [] = []
renderTemplate alist s =
let (b,t,a) = s =~ "%[a-z0-9]*%" :: (String,String,String)
tagval t
| t == "%%" = Just "%"
| otherwise = let inner = take (length t - 2) $ drop 1 t
in lookup inner alist
val = tagval t
in if isJust val
then b ++ (fromJust val) ++ renderTemplate alist a
else b ++ t ++ renderTemplate alist a
Функция форматирования сообщения по шаблону готова. В ней мы последовательно «раскусываем» шаблон с помощью регулярных выражений на «текст-до», «тег» и «текст-после». Подставляем на место «тега» (t) значение соответствующего параметра, если есть, или буквальный «%», если тэг пустой. Продолжаем, пока не кончится шаблон.

О регулярных выражениях. Включаем импортом
import Text.Regex.Posix ((=~))
После этого можем в любой строчке искать регулярное выражение:
строка =~ выражение :: возвращаемый тип
Регулярные выражения ведут себя по-разному в зависимости от возвращаемого типа. Мне пока что пригождаются больше всего два из них: Bool для проверки соотвествия строки выражению и тройной кортеж (String,String,String), разрезающий строчку на три части.

Функция форматирования по шаблону готова. Она просто работает со строками (шаблонами) и ассоциативными списками (словарями). А где же обещанная renderItem?

Форматируем запись по шаблону

Итак, renderItem должна получать шаблон и запись из фида, а возвращать строчку. Всё, что делает эта функция — просто достаёт нужные параметры записи, помещает их в ассоциативный список и вызывает функцию форматирования по шаблону renderTemplate. В виде кода это выглядит гораздо понятнее:
renderItem :: String -> Item -> String
renderItem t i =
let title = ( fromJust . getItemTitle ) i
link = ( fromJust . getItemLink ) i
summary = ( takeSentences 5 . eatTags . fromJust . getItemSummary) i
tags = zip [ "title","link","text" ]
[ title, urlEncode link,summary ]
in renderTemplate tags t
Нетривиальна здесь только функция подготовки текста сообщения (summary).

Поскольку я весь текст пересылать не хочу, а хочу только первые несколько предложений, то я вначале преобразую HTML в простой текст (в котором уже нет HTML-тэгов), а затем просто берую первые пять предложений. Таким образом, мне не нужно заботиться о предолжения будут гарантировано законченными.

Функция eatTags использует тот же приём рекурсивного раскусывания строки с помощью регулярных выражений, что и renderTemplate:
eatTags [] = []
eatTags s =
let (b,t,a) = s =~ "</?[^>]*/?>" :: (String,String,String)
in b ++ eatTags a
Все HTML и XHTML теги должны быть этой функцией вырезаны.

Упражнение: изменить функцию так, чтобы тег <img/> выразался не бесследно, а заменялся содержимым его аттрибута alt.

Теперь осталось лишь взять первые n предложений. Возьмём вначале одно:
takeSentence s = 
let ends = ".?!;"
(first,rest) = break (`elem` ends) s
in if not (null rest)
then (first ++ [head rest],tail rest)
else (first,[])
Тут я обошёлся без регулярных выражений, просто задав список разделителей (ends) и раскусывая строку по символу из их числа (break (`elem` ends)). Напоследок присоединяю разделитель, если он есть, к «откушенному» предложению (break прикрепляет его к «остатку»).

Осталось лишь взять первые n штук:
takeSentences n s
| n > 0 = let (s',r) = takeSentence s
in s' ++ takeSentences (n-1) r
| otherwise = ""
Теперь любая запись может быть представлена так, как мы захотим.

Обновляем список обработанных записей

Записи получены, отобраны, отформатированы, отправлены. Осталось только обновить список обработанных. Вначале сохраним предыдущую версию файла (переименованием), а потом запишем на его место новый список:
  renameFile sentfile (sentfile ++ "~")
writeFile sentfile $ unlines (sent_ids ++ new_ids)
Здесь использована функция renameFile из System.Directory.

Заключение

Вот вроде и всё. Можно вызывать получившийся скрипт:
$ runhaskell Feed2Lj.hs URL-вашего-фида
Пробовал пока только с GHC, но, думаю, и с Hugs должно работать. Я, кстати, осознал, что у интерпретатора Hugs есть важное преимущество перед GHC: установка GHC тянет около 100 МБ, а Hugs — всего порядка 10 МБ. Так что как разберусь с Hugs, буду стараться проверять свои скрипты и на нём.

В целом впечатления от опыта «написать на Хаскеле» очень положительные. Во-первых, очень приятно, когда удаётся написать полезную функцию в одну-две строчки. Во-вторых, интересно думать о программе иначе, писать более декларативно. В третьих, очень приятно, когда раз — и работает! (Ну это с любым языком). В четвёртых, мне нравится «математичный» синтаксис Хаскеля, он, по-моему, очень выразителен. Поначалу, пока не знакомо, конечно долго и непривычно, но когда входишь во вкус, получается быстрее и легче.

Кроме, понятно, гугла, большой подмогой является Hoogle. Сообщения GHC довольно подробные и понятные (разбирать ошибки C++-компиляторов про шаблоны гораздо труднее). Радует, что уже сейчас коллекция библиотек весьма богата (кажется, сопоставима с набором библиотек Python в то время, когда я с ним впервые познакомился). С уникодом, опять же, никаких проблем.

Есть и всякие «но»: но в коде других людей мне ещё далеко не всё понятно, но пихать ввод-вывод в любую точку кода в Хаскеле неудобно и не нужно (сделано намеренно, для отладки служит trace из Debug.Trace), но представить порядок ленивых вычислений не всегда легко, но документированы библиотеки в Hackage весьма лаконично (строго, по делу, но не так доходчиво и очевидно для новичков, как, например в Python), но cabal до сих пор нет ни в Debian, ни в Ubuntu.

Но всё равно, мне понравилось. Буду рад замечаниям и вопросам. Уверен, что-то можно было написать лучше (короче, понятнее и выразительнее). Что-то, наверное, забыл объяснить.

20090324

Статистика скачивания библиотек Haskell из Hackage

Вчера, видимо, была впервые опубликована статистика скачивания библиотек Haskell с сайта Hackage. Тем, кого интересуют детали, предлагаю ознакомиться с оригиналом статьи. Я лишь помещу одну картинку:



Учтём, что пользуются Hackage в основном разработчики (конечные пользователи устанавливают дистрибутивные пакеты библиотек). И получается, что количество программистов на Haskell в данный момент очень быстро растёт. Если взглянуть на график в логарифмической шкале (см. источник выше) — вполне себе экспоненциальный рост. Судя по цифрам на графике (порядка 10⁵ скачиваний исходников в месяц), оценить количество активных разработчиков и тех, кто ими скоро станет, можно, по-моему, как порядка 10³ или даже 10⁴.

Для тех, кто ещё не начал разбираться с Haskell. Hackage — это такой централизованный репозиторий разных библиотек для Haskell. Репозиторий предназначен прежде всего для разработчиков и содержит исходники последних версий. Установка нужной библиотеки из Hackage обычно выглядит так:
$ cabal update
$ cabal install название-библиотеки
Дальше скачаются и скомпилируются все зависимости библиотеки, а библиотека будет установлена в ~/.cabal/. Правда, похоже на sudo aptitude update && sudo aptitude install пакет?


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

Первоисточник: One Million Haskell Downloads…

PS. Как известно, для чего только Haskell уже не используется:

20090319

Как вставить формулу LaTeX в блоге или на форуме

Я уже как-то писал о том, как сделать картинку из формулы. Однако есть ещё более простые способы вставить формулу на веб-страницу.

1-й способ. Javascript-библиотека jsTeXrender (yourequations.com). GPLv3, кстати. Исходный код здесь (в том числе, серверной части).

Где-нибудь на странице подгружаем скрипт. Например, вставляем такой код перед </body>:
<script type="text/javascript" src="http://tex.yourequations.com/"></script>
А все формулы LaTeX пишем внутри тегов <pre lang="eq.latex"/> и <code lang="eq.latex">:
<pre lang="eq.latex">
\!i\hbar\frac{\partial}{\partial t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi+V\psi
</pre>
И получаем:

\!i\hbar\frac{\partial}{\partial t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi+V\psi
Плюсы: легко использовать, библиотечку можно разместить у себя. Формулы остаются читаемы даже при выключенном скрипте. При добавлении новых формул ничего настраивать не надо.

2-й способ. Javascript библиотека jsMath. В отличие от описанного в предыдущем способе jsTeXrender, jsMath не требует настроенного сервера для работы, а рисует формулы сама.

Для использования, нужно подгрузить библиотеку:
<script src="путь/к/библиотеке/easy/load.js"></script>
а формулы, по умолчанию можно писать прямо как в LaTeX:
\[
\!i\hbar\frac{\partial}{\partial t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi+V\psi
\]
jsMath находит их сама и перерисовывает как надо. При желании, маркеры начала и конца формулы можно настроить.

Рисует формулы jsMath хорошо. Достаточно сказать, что именно jsMath используется в стандартном интерфейсе блокнота Sage. Дополнительные примеры можно посмотреть на сайте jsMath.

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

3-й способ. Идём на страничу Texify.com. Вводим туда свою формулу и получаем код для вставки на страницу.

texify example

Можно вставлять формулы и не заходя на сайт, URL картинок формируется так: http://www.texify.com/img/формула-LaTeX.gif. Очень удобно, если формулу надо поправить.

Дополнение. Выданный на texify.com адрес картинки с некоторых пор не работает за пределами сайта texify.com, так что картинку придётся сохранить, куда-то выложить и только после этого вставить, куда надо. Всё это лишает Texify.com смысла.

Аналогичный сервис доступен на mathURL.com. Как и tinyurl.com, он генерирует короткие адреса для картинок с формулами. Вот результат

формула нарисованная mathurl.com

Плюсы: минимум усилий для вставки одной-двух формул. Не зависит от скрипта (можно использовать, например, в ЖЖ или на форумах). Минусы: зависимость от внешнего веб-сервиса.

4-й способ. Похож на первые два, только картинку создаёт CGI-программка mimetex. Программка написана на Си, и TeX-а для своей работы не требует. Формула вставляется так:
<img src="http://ваш.сервер/cgi-bin/mimetex.cgi?формула-LaTeX" alt="формула-LaTeX">
а выглядит так:

mimetex example

Плюсы: легко организовать почти на любом сервере, где разрешены CGI-скрипты, не требует на нём установки LaTeX. Никакой зависимости от сторонних веб-сервисов. Минус: нужен свой сервер.

Вариант этого способа — CGI-программка mathtex. В отличие от mimetex, она использует настоящий LaTeX, установленный на сервере, и dvipng, чтобы создавать картинки. В остальном аналогична.

5-й способ. Есть настоящий pastebin для физиков и математиков. mathbin.net. Там можно постить фрагменты текста, и прямо там обсуждать, как на форуме. Это, однако, немного другой жанр.

5-й способ. Недавно возможность отрисовывать формулу LaTeX в виде картинки появилась в Google Charts. Ссылка на картинку формируется так: http://chart.apis.google.com/chart?cht=tx&chl=формула LaTeX. Например:

Google Charts example

(ссылка на картинку: http://chart.apis.google.com/chart?cht=tx&chl=\huge\!i\hbar\frac{\partial}{\partial%20t}\psi=-\frac{\hbar^2}{2m}\nabla^2\psi+V\psi).

6-й способ. Описан в этом блоге. Опять с подключением внешних скриптов, в этот раз предоставленных сайтом watchmath.com, которые, в свою очередь, обращаются к сервису mathcache.appspot.com. Отрисовкой формул занимается экземпляр mathTeX (см. выше), запущенный кем-то на инфраструктуре Amazon. Скрипты же позволяют вставлять формулы как в LaTeX, просто $формула$ или \[ формула \]. Подключаются скрипты так:
<script type="text/javascript"
src="http://mathcache.s3.amazonaws.com/replacemath.js"></script>
<script type="text/javascript">
replaceMath( document.body );
</script>
Пример отрисовки:
пример mathcache.appspot.com
По стилю использования получается похоже на jsMath. Примечание: при использовании этого скрипта все знаки «доллара» на сайте придётся защищать тегом <code>.

Ещё кое-какие способы перечислены здесь.

20090318

Куда писать о программировании?

Дорогие читатели! У меня к вам вопрос.

Как вы смотрите на то, чтобы в этом блоге появлялись также заметки и ссылки, посвящённые программированию? Вряд ли их будет очень много, и скорее всего они будут очень краткие. Вроде «а вот это можно сделать так» или «оказывается, есть такой замечательный проект/библиотека — вот ссылка».

Так сложилось, что этот блог преимущественно «о линукс», в основном об использовании свободного ПО. Однако мои естественные интересы шире. И программировать мне нравится. К тому же это, в некотором смысле, сопутствующая моей работе деятельность.

О тематике таких заметок: в последнее время мне всё более интересны Python, Haskell, в меньше степени мне теперь интересны C, C++, Java. И я надеюсь, мне не придётся писать о несвободных технологиях, в том числе о языках, в названии которых есть «решётка». Так сложилось, что заниматься мне в основном приходится программами вычислительными, несложными веб-интерфейсами и визуализацией данных. Собственно, об этом кое-что здесь уже проскакивало. Как программировать шейдеры я рассказать не сумею :)

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

1) я помещаю сюда отдельные заметки, интересные только программистам, и мечу их отдельным тэгом;
2) я помещаю сюда отдельные заметки, интересные только программистам, и мечу всё остальное отдельным тэгом (и видимо, именно это отдаю в runix.org);
3) я создаю отдельный блог, и вам об этом сообщаю, кому интересно — подпишется.

Мне больше нравятся варианты 1 и 2.

Вот пример ссылок которые я мог бы давать в таких заметках:
Автоматический подбор оптимальных параметров компиляции, дающих самый быстрый код, возможен с помощью генетических алгоритмов, смотрим на проект Acovea.

Why Not Haskell?серьёзно и объективно написанная статья о самом популярном языке программирования. Довольно смешно. В общем-то правильно.

20090317

Нормальный русский шрифт в Tk-приложениях

Обновление: запись частично устарела, python-tk 2.5 уже давно в Debian stable. Так что пересобирать пакеты самостоятельно теперь не надо.

Как известно, есть такая библиотека для графических интерфейсов, Tk. На Tcl/Tk легко писать скрипты с графическим интерфейсом. Так же Tk используется в качестве стандартной (доступной по умолчанию на всех платформах и не требующей установки) графической библиотеки в Python. Приложений написанных с использованием Tk довольно много. Раз читаете этот пост — значит, видели (смотрим, например, aptitude search ~Dpython-tk и aptitude search ~D^tk).

Однако русским пользователям Debian и Ubuntu с Tk явно не повезло. По умолчанию, из коробки, русские буквы в ней отображаются очень криво. Вот, к примеру простейшее приложение-однострочник:
$ echo 'button .b -text "привет, world!" ; pack .b ' | wish
и наблюдаем вот такой результат:



Хороших слов для таких шрифтов не находится. Такое ощущение, русские буквы берутся из какого-то китайского шрифта, при этом никакого сглаживания и хинтинга. Проблема настолько острая, что даже разобрана в LOR Wiki. В случае Tcl/Tk достаточно начать использовать Tk 8.5 вместо 8.4. В Debian/Ubuntu это делается так:
$ sudo update-alternatives --config wish 

Есть 3 альтернатив, которые предоставляют `wish'.

Выбор Альтернатива
-----------------------------------------------
* 1 /usr/bin/wish8.4
+ 2 /usr/bin/wish-default
3 /usr/bin/wish8.5

Нажмите enter, чтобы сохранить значение по умолчанию[*], или введите выбранное число: 3
Используется `/usr/bin/wish8.5' для предоставления `wish'.
Повторяем наш проверочный пример, и получаем вполне пристойный результат:



Понятно, что перед этим нужно поставить пакеты tcl8.5 и tk8.5. Это минутное дело.

Однако приложениям на Python, использующим Tk, вышеописанный фокус не поможет. В Debian (Lenny) и Ubuntu (Hardy, Intrepid) пакет python-tk собран с поддержкой только tk8.4, то есть по-умолчанию такие приложения выглядят вот так:



Чтобы это исправить, нужно поставить пакет python-tk собранный с поддержкой Tk 8.5. В Debian такой пакет есть, но только в experimental. Я скачал оттуда pytnon-tk 2.5.4-1 и нужный ему blt 2.4z-4 вручную и поставил. Никаких лишних зависимостей они не тянут, а Tk-приложения обретают новую жизнь:



В Ubuntu (Intrepid) ситуация похожая. Пакет python-tk собранный с Tk 8.5 повляется лишь в Jaunty. И сразу python-tk 2.6.1-0ubuntu1. Конечно, в таких случаях религия предписывает пересобирать пакет самостоятельно.

20090316

Wacom Bamboo Fun в Debian Lenny

Начну с приятного. Планшет Bamboo Fun* в Debian Lenny поддерживается, в смысле, что собирать самостоятельно драйвера Wacom не надо. Годятся дистрибутивные. У меня стоят соответственно пакеты wacom-tools и xserver-xorg-input-wacom версии 0.7.9.3-2.

* а вот владельцам Bamboo One, похоже, повезло меньше, судя по-всему им придётся установить драйвера самостоятельно, поддержка Bamboo One заявлена лишь начиная с wacom-tools 0.8.2.2


Однако для нормального использования, нужно внести настройки планшета в /etc/X11/xorg.conf. Во-первых, я добавил туда раздел ServerLayot, которого по-умолчанию там не было:

Section "ServerLayout"
Identifier "Default Layout"
Screen "Default Screen"
InputDevice "Configured Mouse"
InputDevice "Generic Keyboard"

InputDevice "stylus" "SendCoreEvents"
InputDevice "eraser" "SendCoreEvents"
InputDevice "cursor" "SendCoreEvents"
InputDevice "pad"
EndSection


Во-вторых, добавил описания для все четырёх устройств ввода планшета (оба конца пера, указатель курсора и кнопки планшета).

Section "InputDevice"
Identifier "stylus"
Driver "wacom"
Option "Type" "stylus"
Option "USB" "on"
Option "Threshold" "10"
Option "Device" "/dev/input/wacom"
EndSection

Section "InputDevice"
Identifier "eraser"
Driver "wacom"
Option "Type" "eraser"
Option "USB" "on"
Option "Threshold" "10"
Option "Device" "/dev/input/wacom"
EndSection

Section "InputDevice"
Identifier "cursor"
Driver "wacom"
Option "Type" "cursor"
Option "USB" "on"
Option "Threshold" "10"
Option "Device" "/dev/input/wacom"
EndSection

Section "InputDevice"
Identifier "pad"
Driver "wacom"
Option "Device" "/dev/input/wacom"
Option "Type" "pad"
Option "USB" "on"
EndSection


После этого нужно перезапустить графическую подсистему («иксы»). Т.е., если используется графический экран входа в систему, то подключить планшет нужно ещё до появления этого экрана. Если курсор там управляется планшетом нормально — входить и работать. Если же планшет там не подхватился, то, не входя в систему, нажать Ctrl + Alt + Backspace. Графическая система перезапустится. После этого планшет должен уж точно подхватиться.

Внимание! Если планшет отключить (выдернуть из USB-порта), то придётся выходить из системы, чтобы он заработал опять. Не очень удобно, но так вот пока сделано... Зато работает и стёрка, и перо, и кнопки. В Убунту по-умолчанию планшет подхватывается сам, но чтобы заработала и стёрка, и кнопки, придётся всё равно сделать как в Debian.

После этого в используемом приложении (Gimp, Inkscape) нужно указать, какие устройства планшета надо включить. Конкретно в Gimp 2.4 нужно пойти в Файл, Настроить, Устройства ввода, Настроить дополн. устройства ввода. Далее последовательно выбрать все устройства планшета (stylus, eraser, cursor, pad) и включить каждое из них, установив режим «Экран» или «Окно». Нажать Сохранить.

Включить планшет в Gimp

Дополнительные возможности регулировки нажима кисти есть в новом Gimp 2.6, но в Debian Lenny старый добрый 2.4. В нём всё минималистично:

Настройки чувствительности кисти в Gimp 2.4

Утилитки wacomctl в Debian Lenny нет. Впрочем, может и к лучшему. Настроить параметры планшета и запрограммировать его кнопки можно из коммандной строки с помощью xsetwacom. Есть также графические утилитки Gnome Tablet Apps, которых в Lenny тоже нет, но поставить несложно. Выглядят они вот так:

Gnome Tablet Apps: регулируем чувствительностью планшета к нажатию

Однако вернёмся к не такой яркой, но универсальной утилитке xsetwacom. Ниже я расскажу, как регулировать чувствительность к нажатию. Для себя я описываю его «гаммой» нажатия. Гамма равная 1 соответствует настройке по умолчанию, гамма равная 2 соответствует очень сильной чувствительности к слабым нажатиям, гамма равная 0 соответствует очень слабой чувствительности к слабым нажатиям.

Параметры кривой чувствительности планшета вычисляются для данной гаммы длинной командой, которую я его сделал алиасом (pressgamma ниже). В ~/.bashrc поместил следующее:
alias pressgamma="awk 'BEGIN{ b=3.14*ARGV[1]*0.25; x=int(50*1.41*cos(b)); y=int(50*1.41*sin(b)); print x, y, 100-y, 100-x; }'"
function setwacomgamma { xsetwacom set stylus PressCurve `pressgamma $1` ; }
Теперь, посмотреть параметры кривой чувствительности для произвольной гаммы,
$ pressgamma 0.7
60 36 64 40
А установить нужную чувствительность планшета можно командой
xsetwacom set stylus PressCurve `pressgamma 1.2`
или просто
setwacomgamma 1.2


Дополнение. На сайте Wacom можно зарегистрировать свой планшет указав операционную систему Linux.

20090312

Стоит ли переходить на Ext4?

Я вообще скептически отношусь ко всяким новым файловым системам. Видимо, сказывается то, что когда-то очень давно потерял данные на ReiserFS. А в линуксе, как известно, грядут Ext4 и btrfs. Знаю я о них мало, разве что слышал. Как я понимаю, основные достоинства Ext4 — возможность создавать файлы по 16 терабайт и создавать до 32000 подкаталогов в каждом каталоге. Мне, честно говоря, этого пока не надо. А ещё обещается более быстрая работа по сравнению с Ext3. Ну ещё что-то вроде дефрагментации на лету. Однако, тут открылись интересные подробности.

В дискуссии на слэшдоте и в баг-репорте убунту обсуждается, что мол при использовании ext4 у многих приложения (в частности, KDE) теряются фрагменты файлов. Разработчики ext4 разрулили вопрос, объяснив, что разработчики приложений (кроме Emacs) сами виноваты, мол надо перед закрытием любого файла обязательно вручную fsync вызвать, потому что...

А потому что в Ext4 после закрытия файла до фактической записи данных на диск может проходить до 150 секунд! (две с половиной минуты!). И если в этот период компьютер выключится (или сломается ядро, или возникнет ещё какая-та ситуация, которая может помешать записи данных), то данные будут полностью или частично потеряны. К слову сказать, в Ext3 запись данных тоже отложенная, но в Ext3 этот период — разумные 5 секунд.

Так что обсуждаемая проблема вроде и не проблема Ext4 как таковой, но такая «фича», которую надо иметь в виду.

Исходя из этого, вот какое у меня сложилось мнение:

1) время задержки записи 150 секунд для домашнего/настольного применения неприемлимо: мне вот не раз приходилось сохранять результаты за несколько секунд до разряда аккумулятора, да и работа с ИБП для меня всегда была скорее исключением, чем правилом; при домашнем/настольном использовании очень высока вероятность, что закрыв одно приложение, пользователь откроет какое-нибудь другое, которое может создать препятствия своевременной записи на диск (скажем, забьёт весь ввод-вывод проверкой торрентов или захватом видео), а также высока вероятность, что за эти 2,5 минуты пользователь успеет что-нибудь такое в машину засунуть, что вызовет аппаратный сбой (скажем, воткнёт PCMCIA или USB устройство со сбойным драйвером);

2) авторы многих приложений ещё не скоро (если вообще когда-нибудь) станут перезаписывать файлы «грамотно», как в Emacs, c fsync и переименованием; предложение же всем мелким утилиткам и скриптам полагаться на транзакционные хранилища (BerkleyDB, SQLite, реляционные СУБД) тоже утопично и совсем не в духе unix; так что ещё долго при использовании Ext4 следует ожидать сбоев и потери данных во всех файлах, которые только были открыты за последние две с половиной минуты;

3) преимущества ext4 при больших объёмах данных скорее всего ещё не один год будут малоактуальны для домашнего пользователя; а возможный выигрыш в отзывчивости не стоит риска потери данных;

4) ни в коем случае не следует пытаться использовать Ext4 на внешних устройствах (USB-дисках и им подобным), которые могут быть внезапно отключены от системы.

Так что я лично торопиться с разными Ext4/Btrfs пока не буду. И друзьям не посоветую. Думаю, первые годы их пользователи шеи себе наломают. А использовать эти системы нужно лишь там, где они действительно нужны. Ну вот, скажем, там, где есть файлы по 16 терабайт. Где есть ИБП и дизель-генераторы. Где в компьютер не засовывают 10 раз на дню новое железо.

PS. Да, я думаю, время задержки записи, вероятно, можно настроить. Однако есть ли смысл, если Ext3 и так неплохо работает?

20090309

PyAMG: алгебраические многосеточные методы в Python

Хочу отметить, что на моём горизонте сегодня появилась библиотека PyAMG для алгебраических многосеточных методов. Для Python.

Служит для решения систем линейных уравнений. Может использоваться в качестве решателя в FiPy или Dolfin/FEniCS. Ну и где угодно. Преимущество этих методов в практически постоянной экспоненциальной скорости сходимости для систем любого размера. Простыми словами: количество вычислений на каждый следующий точно вычисленный десятичный знак постоянно и не зависит от размера решаемой системы.

Примеры на сайте весьма интересны. Стиль примеров вполне питонический. В зависимостях практически только SciPy, NumPy и Pylab. Надо будет попробовать.

Также по теме: обзор свободных программ для численных расчётов.

20090307

eeePC 901: переназначение курсорных клавиш, Shift и PageUp/PageDown

В продолжение к моей прошлой заметке об установке Debian на eeePC 901. Благодаря малым габаритам и скромному весу машинки я часто прихватываю с собой eeePC вместо полноразмерного ноутбука. Однако маленькие размеры имеют и изъян: клавиатура тоже маленькая.

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

1) клавиши PageUp и PageDown доступны только в комбинации с клавишей Fn, то есть требуют двух рук и лишнего пальца;
2) клавиша Shift укорочена, а там, где должна быть её левая часть, находится курсорная клавиша «вверх».

Вот схема этой раскладки по умолчанию:

eeePC 901: расположение клавиш по умолчанию

Поскольку два основных приложения для меня — терминал и браузер, и в обоих вкладки переключаются по Ctrl + PageUp/PageDown, и ими же или ими же с Shift я делаю прокрутку, то жать каждый раз Fn в другой части клавиатуры — неудобно.

Поэтому придумал я сделать так, чтобы нажимать PageUp/PageDown без Fn, а курсорные с Fn. Скрипт для xmodmap у меня такой:
! map PgDown/PgUp/Home/End to cursor keys
keycode 104 = Next
keycode 98 = Prior
keycode 100 = Home
keycode 102 = End
! map cursors keys to PgDown/PgUp/Home/End
keycode 105 = Down
keycode 99 = Up
keycode 97 = Left
keycode 103 = Right
И соответственно схема раскладки:
eeePC 901: PageUp/PageDown доступны без Fn
Попользовался, оказалось довольно удобно. Во всяком случае, в браузере и терминале. Если курсорные клавиши нужны редко. При использовании MPlayer, наверное, лучше оставить обычную раскладку.

Однако я по-прежнему продолжал иногда попадать мизинцем не в Shift, а в клавишу PageUp («вверх»), при этом курсор, естественно перескакивает на страницу вверх. Очень раздражает при наборе текста. Всё таки я, видимо, пользуюсь левым краем обычного Shift, тянуться ближе.

Соответственно, возникла идея вообще поменять Shift и PageUp местами. Соответственно, скрипт для xmodmap:
remove Shift = Shift_R
remove Control = Control_R

! remap PgDown/PgUp/Home/End/Shift
keycode 104 = Next
keycode 98 = Shift_R
keycode 100 = Home
keycode 102 = End
keycode 62 = Prior
! remap cursors keys and Control
keycode 105 = Down
keycode 99 = Control_R
keycode 97 = Left
keycode 103 = Right
keycode 109 = Up

add Shift = Shift_R
add Control = Control_R
Вроде работает. Раскладка получается такой:
eeePC 901: PageUp/PageDown доступны без Fn, Shift слева от кнопки PageUp

Как использовать: сохранить команды файл, и выполнить xmodmap файл-с-переназначением-клавиш. Чтобы применить постоянно, поместить команды в ~/.xmodmaprc. Я включаю их пока вручную, когда надо.

Если модель нетбука у вас другая, или другая клавиатура (мало ли, в другой стране купленная), то, наверное, лучше вначале посмотреть правильные коды клавиш с помощью утилиты xev, а уж потом соответствующим образом поправить команды.

20090305

Свёртки вместо режима структуры в Vim

На этот пост меня подвигла записка дебианщика про древоредакторы. Собственно, это редакторы для иерархически организованных заметок в которых так называемый режим структуры («Outline mode») сделан основным. Специальные редакторы — это, конечно, замечательно, но в 90% случаев всё можно сделать гораздо проще. В обычном текстовом редакторе Emacs* Vim.
* ну в Emacs вроде тоже можно :-) вот

Итак, простая идея:

1. пишем все заметки в простой текстовый файл;
2. структуру/иерархию заметок указываем с помощью заголовков разного уровня; для определённости, помечаем заголовки как в Markdown (# заголовок, ## подзаголовок, ### подподзаголовок, и т.д.);
3. используем режим свёртки в редакторе, чтобы имитировать режим структуры, т.е. сворачиваем каждый раздел и подраздел, показывая только его заголовок.

Нетривиален только пункт 3. В Vim для этого можно создать файл ~/.vim/syntax/mkdfold.vim примерно такого содержания:
" Markdown section regions to be folded
syn region mkdSec1 start=/^\s*#[^#].*/ end=/^\s*#[^#].*/me=s-1 fold
syn region mkdSec2 start=/^\s*##[^#].*/ end=/^\s*#\{1,2\}[^#].*/me=s-1 fold containedin=mkdSec1
syn region mkdSec3 start=/^\s*###[^#].*/ end=/^\s*#\{1,3\}[^#].*/me=s-1 fold containedin=mkdSec1,mkdSec2
Он определяет диапазоны подразделов, которые будут «сворачиваться». В этом примере от заголовка до заголовка того же или более высокого уровня.

Теперь пишем простой текстовый файл в котором первой строкой ставим modeline, примерно такого содержания:
vim: set ft=mkdfold foldmethod=syntax ai si ts=3 sw=3 :

И пишем дальше в нём свои заметки:
# Заметки
## Способы организовать иерархические заметки
* fold mode как органайзер (Vim)
* outline mode (куча редакторов)
* спецредакторы (см. virens)
## Как организовать синтаксические свёртки в Vim
* файл с синтаксисом, определяющий начало и конец разделов и их вложенность
* modeline в редактируемом файле, которая включит этот синтаксис и свёртки
Переоткрываем файл, и всё, пользуемся обычными командами свёртки, чтобы скрыть лишнее. zM — свернуть всё. zR — развернуть всё. zj и zk — следующая и предыдущая свёртка соответственно. za — свернуть/развернуть свёртку под курсором. Ну и много других (:help fold-commands).

Выглядит это примерно так:
+-- 10 строк: # Дела------------------------------------------------------------
# Заметки
+--- 4 строк: ## Способы организовать иерархические заметки--------------------
+--- 3 строк: ## Как организовать синтаксические свёртки в Vim-----------------


Плюсы такого подхода: 1) используется обычный редактор (ничего ставить и ни к чему привыкать не надо), 2) используется plain-text (при желании, заметки можно открыть в любом другом редакторе). Минусы: 1) ну не все используют Vim и не всем нравятся его свёртки, 2) возможно, специализированные средства более удобны.

Я ещё подумал, и идея мне эта ещё больше понравилась. Можно приспособить для ведения списков дел. Надо только добавить подсветку важных дел (восклицательный знак на конце) и сделанных/отложенных/отменённых.

А вообще, всякую мелочь и ерунду я записываю в TiddlyWiki. Уже давно, и очень доволен. Мне это удобнее, чем организовывать заметки иерархически.

Дополнение: как выяснилось, такой подход можно значительно развить, смотрим цикл заметок тов. tengu-crow про ведение «блога» в текстовом файле (с помощью Vim!) и связанный с ним цикл заметок тов. olly cat про использование свёрток в Vim для ведения файла заметок. Спасибо за ссылки!

Есть ещё специализированный плагин VimOutliner, но это уже более глубокое развитие идеи. Я хотел показать, что минимальный функционал для ведения записок есть и в штатной конфигурации.

20090304

Перенос данных из Microsoft Money в KMyMoney

Полагаю, кому-то может быть полезно: про перенос данных из Microsoft Money в KMyMoney. Понятным языком написанная инструкция.