Архив рубрики: Haskell

Немного о каррировании в Haskell

Читая М. Липовача "Изучай Haskell во имя добра!", я некоторое время не понимал, чем частичное применение отличается от каррирования. Потратил некоторое время на разбор данного вопроса и набросал себе "шпаргалку" по обозначенной теме.


В Haskell функции без параметров называются определениями (definition) или именами (name).

func :: String
func = "Haskell"

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

func :: Int -> Double -> Char -> Bool

то Haskell воспринимает её следующим образом:

func :: Int -> (Double -> (Char -> Bool))

Т.е. функция func принимает параметр типа Int и возвращает новую функцию, которая принимает очередной параметр - типа Double и возвращает другую новую функцию, принимающую параметр типа Char и возвращающую значение типа Bool.
Преобразование функции от многих аргументов в функцию, берущую свои аргументы по одному называется каррированием. Haskell автоматически выполняет каррирование всех функций, принимающих более одного параметра. Именно благодаря каррированию становится возможным частичное применение функций, а так же создание сечений. В свою очередь, частичное применение делает возможным существование бесточечной нотации.

Примечание

В Haskell не существует такого понятия, как частичное применение функции. Существует применение функции (без "частично"). Если мы говорим (для удобства восприятия), что функция f :: Int -> Int -> Int имеет два аргумента, (что с технической точки зрения не является корректным), то мы можем так же сказать (снова для удобства восприятия), что f 5 - это частично применённая функция (что так же не будет корректно технически).

Пример

func :: (Num a) => a -> a -> a -> a
func a b c d = a + b + c + d

ghci

Частичное применение:
λ: let n = func 2 3
λ: let m = n 10
λ: let g = m 7
λ: g
22
Сечения:
λ: let a = (/2)
λ: a 10
5.0
λ: let b = (15/)
λ: b 5
3.0
Бесточечная нотация:
odd' :: Integral a => a -> Bool
odd' = odd
ghci:
λ: odd' 5
True
λ: odd' 4
False

Каррирование и декаррирование

В стандартном модуле Data.Tuple определены, помимо прочего, следующие функции:
curry :: ((a, b) -> c) -> a -> b -> c
uncurry :: (a -> b -> c) -> (a, b) -> c
Функция curry преобразовывает некаррированную функцию в каррированную.
Функция uncurry преобразовывает каррированную функцию в некаррированную.

Пример


msg :: Int -> Bool -> String
msg n True = show $ n `div` 2
msg n _ = show $ n * 2

ghci


λ: let n = msg
λ: let m = uncurry n
λ: :t n
n :: Int -> Bool -> String
λ: :t m
m :: (Int, Bool) -> String
λ: n 5 True
"2"
λ: m (5,True)
"2"
λ: let k = curry m
λ: :t k
k :: Int -> Bool -> String
λ: k 5 True
"2"

Об именах в Haskell

Имя любого идентификатора в Haskell начинается с буквы, за которой следует ноль или более букв, цифр, символов подчёркивания _ и одинарной кавычки '. В качестве буквы рассматриваются только латинские символы в интервалах a..z и A..Z. Символ _ принято считать буквой, в следствии чего имя функции может начинаться с этого символа, но не может состоять только из него, в виду того, что в образцах Haskell он обозначает любое значение. Имена функций, составленные не из символов набора ascSymbol, обязательно должны начинаться со строчной буквы или символа _. Имена пространств имён, типов данных, конструкторов данных и классов типов составленные не из символов набора ascSymbol должны начинаться с прописной буквы. В данной заметке даётся некоторая информация об использовании символов набора ascSymbol в идентификаторах Haskell.

"Специальные" символы

Согласно § 2.2 стандарта Haskell 2010 специальными (special) считаются следующие символы:
( ) , ; [ ] ` { }
Там же отдельно определён и следующий набор символов, именованный как ascSymbol:
! # $ % & * + . / < = > ? @ \ ˆ | - ˜ :
Буквенно-цифровые символы в стандарте выделены в отдельные наборы: ascSmall, ascLarge, uniSmall, uniLarge, ascDigit, uniDigit и т.д.
Иногда специальными ошибочно называют символы, входящие в набор ascSymbol. Например, давая определение оператору порой говорят, что их имена состоят только из специальных символов. На самом же деле специальные символы вовсе не могут использоваться в составе имён операторов (да и вообще в составе любых имён).
Из символов набора ascSymbol разрешено формировать любые имена за исключением следующих, зарезервированных:
.. : :: = \ | <- -> @ ~ =>

Функция или оператор?

Оператором в Haskell называется любая функция, вызванная в инфиксной форме, либо частично применённая посредством сечений. Т.о. можно ли назвать ту или иную функцию оператором зависит от контекста её использования. В следующих примерах функции elem и * являются операторами:
λ: 5 `elem` [0..10]
True
λ: 4 * 10
40
Некоторая пользовательская функция для использования её в качестве сечений:
mySomeFunc :: Integral a => a -> a -> a
_ `mySomeFunc` 0 = error "Zero division."
a `mySomeFunc` b = a `div` b
Используем функцию mySomeFunc в качестве сечений, т.е. в данном контексте она является оператором:
λ: let n = (8 `mySomeFunc`)
λ: let m = (`mySomeFunc` 2)
λ: n 2
4
λ: m 10
5
Функция вызванная в префиксной форме записи не является оператором в данном контексте использования. В следующих примерах функции elem и * не являются операторами:
λ: elem 5 [0..10]
True
λ: (*) 4 10
40

Инфиксная и префиксная формы

Если имя функции состоит из символов набора ascSymbol, то при указании её сигнатуры такое имя необходимо заключать в круглые скобки.
(###) :: Int -> Int -> Int -- Сигнатура функции
Если имя функции состоит из символов набора ascSymbol, и её определение даётся в префиксной форме записи, то это имя так же необходимо задавать в круглых скобках.
(@@@) a = (a +) -- Определение функции в префиксной форме
Если имя функции состоит не из символов набора ascSymbol, и её определение даётся в инфиксной форме записи, то имя необходимо обосабливать символами обратной кавычки `.
a `myFunc` b = a * b -- Определение функции в инфиксной форме
Примеры использование инфиксной и префиксной форм записей в коде определения функций:
(###) :: Int -> Int -> Int -- Сигнатура функции
a ### b = a * b -- Определение функции в инфиксной форме

(@@@) :: Int -> Int -> Int -- Сигнатура функции
(@@@) a = (a +) -- Определение функции в префиксной форме

myFunc :: Int -> Int -> Int -- Сигнатура функции
a `myFunc` b = a * b -- Определение функции в инфиксной форме

myFunc' :: Int -> Int -> Int -- Сигнатура функции
myFunc' a = (a -) -- Определение функции в префиксной форме

В инфиксной форме можно вызывать любую функцию, количество параметров которой больше одного, например:
λ: ((+) `foldr` 0) [1..10]
55
Или вот к примеру функция, принимающая четыре параметра:
someFunc :: Int -> Int -> Int -> Int -> Int
someFunc a b c d = a + b + c + d
Префиксный и инфиксный варианты её вызова:
λ: someFunc 1 2 3 4
10
λ: (1 `someFunc` 2) 3 4
10

Имена конструкторов данных

Как уже отмечалось выше - имена конструкторов данных начинаются с прописной буквы, состоят из буквенно-цифровых символов, а так же символов _ и ' (при необходимости). Однако допускается и система наименований, схожая с той, которая применяется по отношению к функциям...
Стандартом Haskell разрешается формировать имена конструкторов данных из символов набора ascSymbol. Подобно обычным функциям, такие конструкторы могут использоваться как в инфиксной, так и в префиксной форме. Кроме того, их имена обязательно должны начинаться с символа : (двоеточие). Т.е. если вы где-то в коде увидели нечто вроде 123 :#$% "ASDF", то сразу же можете быть уверенными в том, что перед вами вызов конструктора :#$% с параметрами 123 и "ASDF".

data Symbolic n
= Constant n
| Variable String
| Symbolic n :=> Symbolic n
| Symbolic n :<= Symbolic n
| (:<=>) (Symbolic n) (Symbolic n)
deriving Show
Cоздадим в ghci несколько экземпляров типа Symbolic, используя разные конструкторы данных:
λ: let a = Constant 10; b = Variable "Hello"
λ: let n = a :=> b; m = a :<= b; k = a :<=> b
λ: n
Constant 10 :=> Variable "Hello"
λ: m
Constant 10 :<= Variable "Hello"
λ: k
(:<=>) (Constant 10) (Variable "Hello")

Имена конструкторов типов

По умолчанию, имена конструкторов типов не могут состоять из символов набора ascSymbol. Однако можно принудительно разрешить использование таких имён для конструкторов типов. Это делается либо путём указания опции -XTypeOperators при вызове ghc или ghci, либо путём добавления в начало hs-файла следующей строки:
{-# LANGUAGE TypeOperators #-}
В отличие от конструктора данных, имя конструктора типа не обязано начинаться с символа : (двоеточие).

{-# LANGUAGE TypeOperators #-}
data a @# b -- Конструктор типа
-- Конструкторы данных:
= XLeft a
| XRight b
| (a @# b) :$% (a @# b)
| (a @# b) :!~ (a @# b)
| (:!~>) (a @# b) (a @# b) (a @# b)
deriving Show
Пробуем создать экземпляры нашего типа данных:
λ: let a = XLeft 10; b = XRight "ABCD"
λ: let c = a :$% b; d = b :!~ a;
λ: let e = (:!~>) a a a
λ: c
XLeft 10 :$% XRight "ABCD"
λ: d
XRight "ABCD" :!~ XLeft 10
λ: e
(:!~>) (XLeft 10) (XLeft 10) (XLeft 10)

Имена образцов

Имена образцов так же могут состоять из символов набора ascSymbol.
λ: let ($%%) = (*)
λ: :t ($%%)
($%%) :: Num a => a -> a -> a
λ: 5 $%% 2
10
λ: ($%%) 3 4
12
λ: let (###) = (3 +)
λ: (###) 2
5

UPD
Имена функциям можно назначать используя и другие символы Unicode. Например, в книгах порой можно встретить в качестве имён функций символы математических операторов. Например, можно написать такую функцию:

(∀) :: (a -> b) -> [a] -> [b]
f
[] = []
f
(x:xs) = f x : f xs
Эта функция успешно загрузится в ghci, но вызвать её будет проблематично, в виду того, что в cmd.exe и powershell.exe может оказаться затруднительным ввести в командную строку символ . У меня не получилось сделать это ни через буфер обмена (соответствующий пункт в контекстном меню), ни через комбинацию клавиш (Alt + 8704). Не помогает и использование шрифта Lucida console, и предварительный вызов команды chcp.com 65001.

Тем не менее, в некоторых книгам можно увидеть весьма активное использование математических символов в качестве имён функций в исходном коде Haskell. Например, в книге Ричарда Бёрда "Жемчужины проектирования алгоритмов. Функциональный подход. С примерами на Haskell".

Подводя итоги

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

Об отступах в коде Haskell

Отступы - они бывают разными. Два hs-файла могут совершенно одинаково визуально выглядеть в текстовом редакторе, однако один из них при этом компилироваться не будет.

Как известно, отступы в коде могут выполняться как пробелами, так и табуляторами. Обратите внимание на следующие два скрина, снятых с Notepad++:




Второй вариант отличается от первого только тем, что между ключевым словом let и образцом sideArea вместо пробела стоит табулятор. Однако визуально выравнивание [форматирование] выглядит совершенно идентично:


Этот код был переписан мною из книги М. Липовачи. Понятное дело, что на бумаге не разобрать, где пробелы, а где табуляторы. Изначально я написал вариант, показанный мною на первом скрине. Визуально это в точности соответствовало тому, как обозначенный код выглядел в книге. Однако попытка загрузить этот код в ghci завершилась неудачей:

λ: :l src
[1 of 1] Compiling Main             ( src.hs, interpreted )

src.hs:4:25: parse error on input `='
Failed, modules loaded: none.
λ:

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

λ: :l src
[1 of 1] Compiling Main             ( src.hs, interpreted )
Ok, modules loaded: Main.
λ: cylinder 10.0 20.0
1884.9555921538758
λ:

Если в настройках текстового редактора включить опцию автоматической замены табуляторов на соответствующее количество пробелов:


то такой файл так же будет успешно загружен.

Нельзя сказать, что какой-то из  трёх обозначенных выше вариантов не отформатирован - они все отформатированы. Однако в Haskell, как видим, форматирование форматированию рознь, даже если визуально никакой разницы не видно. Вот такая может быть "музыка"...

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

Проблема с обновлением cabal

Проблема: попытки обновить cabal не приводят к появлению более новой версии программы.

Изначально я проверил текущую версию cabal:

C:UsersАндрей>cabal --version
cabal-install version 1.18.0.5
using version 1.18.1.3 of the Cabal library

Затем отправил запрос на проверку наличия более новой версии:

C:UsersАндрейReal>cabal update
Downloading the latest package list from hackage.haskell.org
Note: there is a new version of cabal-install available.
To upgrade, run: cabal install cabal-install

Как видим, более новая версия существует. Запускаю команду обновления до более свежей версии:

C:UsersАндрейReal>cabal install cabal-install
Resolving dependencies...
Notice: installing into a sandbox located at
C:UsersАндрейReal.cabal-sandbox
Configuring cabal-install-1.20.0.6...
Building cabal-install-1.20.0.6...
Installed cabal-install-1.20.0.6

Смотрю номер обновлённой версии cabal:

C:UsersАндрей>cabal --version
cabal-install version 1.18.0.5
using version 1.18.1.3 of the Cabal library

Как видим, ничего не изменилось - номер тот же, что и до обновления. Снова запускаю проверку наличия более новой версии:

C:UsersАндрейReal>cabal update
Downloading the latest package list from hackage.haskell.org
Note: there is a new version of cabal-install available.
To upgrade, run: cabal install cabal-install

Замкнутый круг: вижу сообщение о наличии более свежей версии cabal с предложением выполнить обновление. Попытка выполнить обновление с правами администратора положительного результата не дала - получаю ту же самую проблему.

В Интернете нашёл причину такого поведения и способ его исправления: нужно в системной переменной PATH прописать значение %AppData%cabalbin перед значением %PROGRAMFILES%Haskell Platform...bin.

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

PS C:UsersАндрей> cabal --version
cabal-install version 1.20.0.6
using version 1.20.0.2 of the Cabal library
PS C:UsersАндрей> cabal update
Downloading the latest package list from hackage.haskell.org
Skipping download: Local and remote files match.



О пользе возможности частичного применения функции

Маленький пример на тему практической пользы возможности частичного применения функций.


Предположим, что имеется некоторая функция, выводящая приветствие:

printHello::(String->String)->String->String
printHello f x = "Hello, " ++ f x ++ "!"

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

Поприветствуем Васю:

ghci> printHello (x->x) "Vasya"
"Hello, Vasya!"

А что если мы захотим поприветствовать Васю более официально: с указанием его инициалов и фамилии? Пусть у нас даже имеется специальная функция для формирования нужной строки:

shortName::String->String->String->String
shortName [] _ xs = xs
shortName (x:_) [] xs = x : ". " ++ xs
shortName (x:_) (y:_) xs = x : '.' : y : ". " ++ xs

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

ghci> shortName "Vasiliy" "Vasilievich" "Vasiliev"
"V.V. Vasiliev"
ghci> shortName "Vasiliy" "" "Vasiliev"
"V. Vasiliev"
ghci> shortName "" "Vasilievich" "Vasiliev"
"Vasiliev"
ghci> shortName "" "" "Vasiliev"
"Vasiliev"

Заметьте, сигнатура функции shortName отличается от сигнатуры той функции, которая первым параметром передаётся в printHello. В виду этого мы могли бы снова прибегнуть к помощи лямбда-выражения и использовать shortName при вызове printHello например так:

ghci> printHello (x->x) $ shortName "Vasiliy" "Vasilievich" "Vasiliev"
"Hello, V.V. Vasiliev!"

Однако, благодаря возможности частичного применения функций, мы спокойно можем передать shortName первым параметром с двумя аргументами вместо трёх (третий [фамилия] будет передан ей функцией printHello):

ghci> let n = shortName "Vasiliy" "Vasilievich"
ghci> printHello n "Vasiliev"
"Hello, V.V. Vasiliev!"

или так:

ghci> printHello (shortName "Vasiliy" "Vasilievich") "Vasiliev"
"Hello, V.V. Vasiliev!"

Поскольку в данном случае в shortName передаётся два параметра вместо трёх, то генерируется промежуточная функция, сигнатура которой совпадает с ожидаемой. Этой сгенерированной функции printHello передаёт в качестве параметра фамилию, что собственно и приводит к запуску функции shortName. В результате получаем ожидаемую строку.

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

Настройка графического окружения Linux на основе xmonad и xmobar

Так совпало, что меня заинтересовали две вещи: язык программирования Haskell и фреймовые (или тайловые) оконные менеджеры под Linux. И тут я обнаружил замечательный тайловый менеджер, написанный на Haskell. Я не смог пройти мимо и установил себе xmonad.
Эта статья будет не совсем обычная для этого блога. Здесь практически не будет программирования. Я просто покажу Вам как настроить окружение на основе оконного менеджера xmonad и панелей xmobar.

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

Сразу после установки xmonad, Вы, скорее всего, не сможете даже запустить какое-либо приложение. Вам придётся сходить в терминал и прочесть man xmonad, где описаны все дефолтные сочетания клавиш. Дабы облегчить Вам задачу, сразу же приведу основные, без которых работать в среде невозможно:
  • Mod+p: открыть dmenu для запуска приложений;
  • Mod+Shift+Enter: открыть терминал;
  • Mod+Shift+c: закрыть текущее окно;
  • Mod+j: перевести фокус на следующее окно;
  • Mod+k: перевести фокус на предыдущее окно;
  • Mod+[1-9]: перейти на рабочее пространство [1-9];
  • Mod+Shift+[1-9]: перекинуть текущее окно на рабочее пространство [1-9];
  • Mod+q: перезагрузить xmonad;
  • Mod+Shift+q: выйти из xmonad.
Mod по умолчанию -- клавиша Alt.
Никаких баров с xmonad не поставляется, но этот оконный менеджер хорошо интегрируется с xmobar и dzen2.

Базовая настройка xmonad

Для начала настроим оконный менеджер, а затем перейдём к барам, трею и дополнительным скриптам.
Xmonad настраивается путём описания настроек на языке Haskell. Если Вы не знакомы с этим замечательным языком, я советую Вам исправить эту ситуацию как можно скорее. Без базового понимания Haskell, Вы сможете только скопировать готовый конфиг и кастомизировать его по аналогии с уже написанным кодом. В то время как зная Haskel, Вам откроются безграничные просторы для фантазии.
Настройки xmonad должны находиться в файле ~/.xmonad/xmonad.hs.
Минимальный файл конфигурации должен содержать функцию main, в которой происходит вызов функции xmonad с передачей структуры XConfig, содержащей конфигурацию.
import XMonad

main = do xmonad $ defaultConfig
XConfig проще всего получить из функции defaultConfig и на основе настроек по умолчанию создать свои собственные.
Первое, что мне захотелось сделать -- это изменить клавишу-модификатор  с Alt на Super, так как клавиша Alt используется многими другими приложениями. Сделать это очень легко, достаточно назначить полю modMask значение типа KeyMask, в нашем случае это mod4Mask.
import XMonad

main = do
xmonad $ defaultConfig {
modMask = mod4Mask }

Настройка xmobar и trayer

Теперь, когда мы знаем, как настраивать xmonad, было бы не плохо обзавестись тулбаром, который покажет время, загрузку CPU, а самое главное, информацию о том, где (в контексте xmonad) мы сейчас находимся. Кроме того, было бы здорово убрать почтовый клиент, jabber и прочие фоновые задачи в трей.
Xmobar -- это бар, который написан на haskell специально для xmonad, но может работать и без него. Настройки xmobar можно передавать через параметры напрямую или через конфигурационный файл. Я предпочитаю второй способ.
Для отображения трея будем использовать программу trayer.
Основная идея заключается в том, что я хочу, чтобы у меня справа отображались часы и раскладка клавиатуры, слева индикаторы загрузки процессора и памяти и состояние xmonad. Между правой и левой панелью я хочу поместить трей. Для реализации этой идеи мне понадобится запустить два экземпляра xmobar.
Мой монитор имеете ширину 1920 пикселов, поэтому я буду исходить из неё.
Конфигурационный файл xmobar имеет синтаксис конструктора с именованными полями языка haskell. Все его поля очень подробно описаны в man xmobar, поэтому я лишь кратко опишу свои файлы.
Итак, я создал каталог ~/local/conf и положил туда два файла xmobar_left и xmobar_right. Вот содержимое файла xmobar_right.
Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 1690 , ypos = 0, width = 230, height = 20 },
commands = [
Run Date "%a %b %_d %Y %H:%M:%S" "date" 10,
Run Kbd [("us", "US"), ("ru", "RU")]
],
template = "<fc=green>%kbd%</fc> %date%"
}
Назначение полей font, bgColor и fgColor не требует объяснений. Содержимое поля position было установлено опытным путём. Эксперименты показали, что для отображения даты и раскладки клавиатуры потребуется ровно 230 пикселов. Так как трей я хочу сделать шириной в 250 пикселов, то начальная позиция правой панели будет в точке 1690 по оси X. Высота бара будет состовлять 20 пикселов.
Самые важные поля в конфиге -- это commands и template. Поле commands представляет собой список команд, которые xmobar должен запустить. Список поддерживаемых команд можно найти в man xmobar. Команды имеют параметры. Команда Date принимает три аргумента: формат даты, переменная, которая будет использоваться в шаблоне для доступа к значению команды и частоту обновления в десятых долях секунды. Команда Kdb принемает список двуместных кортежей, описывающий доступные раскладки клавиатуры. Первый элемент кортежа -- это наименование раскладки, а второй -- отображаемое имя. В шаблоне к значению этой команды можно обратиться по предопределённому имени kdb.
В параметре template записывается шаблон выводимого текста. Переменные из параметра commands нужно использовать, обернув их в символы %. Кроме переменных можно использовать простой текст и разметку, указывающую цвет символов.
Далее приведу конфиг для левой панели.
Config { font = "xft:DejaVu Sans Mono:size=10:bold:antialias=true",
bgColor = "#000000",
fgColor = "#BBBBBB",
position = Static { xpos = 0 , ypos = 0, width = 1530, height = 20 },
commands = [ Run Cpu ["-S", "True", "-t", "CPU: <total>", "-L","5","-H","40","--normal","#FFFF00","--high","#FF0000"] 10,
Run Memory ["-S", "True", "-t","RAM: <usedratio> (<used>MiB of <total>MiB)"] 10,
Run XMonadLog
],
template = "%cpu% %memory% %XMonadLog%"
}
Здесь используются команды Cpu, Memory и XMonadLog. Команде Cpu передаётся два параметра. Первый параметр -- это набор опций для отображения. Шаблон передаётся через опцию -t (опция и её значение указываются в подряд следующих элементах списка). Доступны следующие переменные: total, bar, user, nice, system, idle, iowait, значение которых известно любому линуксойду. Опция -S в True просит xmobar показывать знак единицы измерения величины (в данном случае %). Опции -L и -H задают нижний и верхний предел для раскрашивания выводимого значения соответственно цветами в оциях --normal и --high. До значения -L цвет текста будет по умолчанию, между -L и -H цвет будет браться из опции --normal, для значений выше и равных -H будет использоваться цвет --high. Вторым параметром команде Cpu передаётся частота обновления в десятых долях секунды.
Команда Memory аналогична команде Cpu, только отображает загрузку памяти. Для этой команды доступны переменные total, free, buffer, cache, rest, used, usedratio, usedbar и freebar.
Самая интересная команда, которая тут используется -- это команда XMonadLog. Она не принимает никаких аргументов, а для её использования нам придётся кое-что написать в конфигурационном файле xmonad.
Запуск trayer`а достаточно тривиален.
trayer --edge top --align right --margin 230  --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true
Думаю, что тут всё понятно и я не буду останавливаться на этой команде.
Теперь помещаем в файл ~/.xsession необходимые команды для запуска xmonad, xmobar`ов и trayer`а.
xmobar ~/local/conf/xmobar_left &
xmobar ~/local/conf/xmobar_right &
trayer --edge top --align right --margin 230 --widthtype pixel --width 200 --heighttype pixel --height 20 --tint 0x0 --alpha 0 --transparent true &
exec xmonad

Полная настройка xmonad

Если Вы перезапустите xmonad, то увидите, что окна перекрывают Ваш бар. Структура данных XConfig содержит несколько полей, которым можно передать функцию обработки некоторых событий. Для того, чтобы бары не перекрывались, следует определить функцию layoutHook и обернуть лейауты в конструктор AvoidStruts при помощи функции avoidStruts. Мы можем использовать лейауты по умолчанию, но лучше определим свой собственный список. Это позволит добавить собственные лейауты и настроить дефолтные.
import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts }

Новый код выделен полужирным шрифтом. Итак, мы создаём 4 лейаута:
  1. Tall имеет 2 столбца. В левом отображаются основные окна, а в правом вспомогательные. Конструктор принимает три аргумента. Первый -- количество окон в основном (левом) столбце. Второй -- процент, на который изменяется размер столбцов при увеличении и уменьшении. Последний -- отношение ширины столбцов по умолчанию;
  2. StackTile. Все окна располагаются в вертикальном стеке. Параметры означают то же самое, что и в конструкторе Tall;
  3. simpleTabbed -- функция, которая создаёт лейаут, в котором все окна максимально развёрнуты, а сверху экрана появляются вкладки с их заголовками;
  4. Full. Все окна максимально развёрнуты.
Лейауты объединяются оператором |||. Объединённые лейауты передаём функции smartBorders, которая убирает рамку окна, если оно одно. Далее вызывается функция avoidStruts для предотвращения перекрытия баров. Полноэкранный лейаут должен перекрывать бары, поэтому он не должен попадать под действие avoidStruts. А так как окно там всегда одно, то вместо smartBorders, мы применяем к этому режиму функцию noBorders для предотвращения отрисовки рамки. Документацию и список доступных лейаутов можно найти здесь.

Ещё одна беда заключается в том, что панель trayer принимает фокус и присутствует лишь на одном workspace`е. Чтобы решить эту проблему, нужно добавить в обработчик manageHook функцию manageDocks.
import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks }
Хуки, как видно, объединяются оператором <+>. Кроме управления барами, в обработчике manageHook можно управлять поведением различных окон. Все доступные операции приведены в документации здесь. Я покажу, на примере своего конфига, как делать определённые окна плавающими.
myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook
Здесь я получаю класс окна при помощи функции className и сравниваю его с образцом оператором =?. Если образец и класс совпадают, то окно передаётся функции doFload оператором -->, которая делает окно плавающим. Класс окна можно получить командой
$ xprop | grep CLASS
Кроме класса окна можно использовать имя приложения или заголовок окна. Все доступные операции описаны в документации.

В обработчике manageHook можно ловить окна и перенаправлять их на другой workspace по его ID функцией doShift. Идентификаторы (они же имена) передаются в XConfig в параметре workspaces, который является списком строк.
myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
workspaces = myWorkspaces,
layoutHook = myLayouts,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }
Конечно же Вы можете назвать ваши рабочие пространства как хотите. Эти имена будут отображаться в нашем xmobar.

Вот и пришло время настроить тот самый лог xmonad, который мы использовали в левом xmobar. Как сказано в man xmobar, мы должны указать в параметре logHook такое выражение
logHook = dynamicLogString defaultPP >>= xmonadPropLog
И это будет работать. Но вывод, предоставленный функцией defaultPP отображается одним цветом и немного неудобно (для меня) отформатирован. Функция dynamicLogString принимает экземпляр типа PP (pretty-printing) и отдаёт строку, отформатированную согласно этому параметру. Создать экземпляр PP лучше всего функцией xmobarPP.
myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }
logHook = myLog >>= xmonadPropLog,
Для работы этого кода необходимо подключить модуль XMonad.Hooks.DynamicLog.
Итак, лог выглядит следующим образом:
<workspace`ы> : <имя лейаута> : <заголовок текущего окна>
Разделителем по умолчанию между воркспейсами служит пробел, а между воркспейсами, именем лейаута и именем окна -- двоеточие, окружённое пробелами. Разделители меня устраивают. В структуре PP мы меняем следующие параметры:
  • ppCurrent: имя текущего workspace`а. Его я отображаю зелёным цветом при помощи функции xmobarColor и оборачиваю в символы [ и ] стандартной функцией haskell wrap;
  • ppTile: заголовок текущего окна;
  • ppHidden: имена воркспейсов, на которых есть окна, но которые не являются текущими;
  • ppHiddenNoWindows: не текущие воркспейсы, на которых нет окон (по умолчанию не отображаются вообще);
  • ppLayout: имя лейаута.

Раз уж мы начали заниматься красивосятми, то, мне кажется, нужно поменять и рамку окон xmonad. Сделать это можно передачей параметров borderWidth, normalBorderColor и focusedBorderColor в XConfig.
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",

workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook }

Я использую клавиатуру с кучей мультимедийных клавиш и хотел бы использовать некоторые из них в повседневной работе. Xmonad позволяет очень легко привязать обработчики к любым клавишам. Привязки клавиш  передаются в XConfig параметром keys.
myKeys x = M.union (keys defaultConfig x) (keysToAdd x) 
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]
main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }
Для работы кода требуется импортировать следующие модули:
import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M
Привязки клавиш представляют собой карту кортежей из модификатора, клавиши и обработчика. Существует несколько способов создать такую карту, но я предпочитаю использовать функцию mkKeymap. Функция mkKeymap принимает список кортежей состоящий из строкового обозначения клавиатурного сочетания в стиле emacs и обработчика. Получить имена клавиш можно с помощью программы xev или подглядеть их в минибуфере emacs.
Функция spawn запускает внешнее приложение.
Для управления окнами используются два модуля: XMonad.Operations и XMonad.StackSet. Первый предоставляет API высокого уровня для работы с окнами, а второй позволяет работать непосредственно со стеком окон и экранами.
Функция focusDown принимает стек окон, перемещает фокус на следующее окно и возвращает новый стек. Но обработчики клавиатурных сочетаний должны возвращать тип X (). Здесь нам поможет функция windows, которая принимает функцию, принимающую и возвращающую стек окон, и возвращает нужный нам X ().
Чтобы закрыть текущее окно, достаточно применить функцию kill. Она сразу же возвращает необходимое значение, поэтому оборачивать её ни во что не нужно.
Получив карту клавиатурных сочетаний, её нужно объединить с картой по умолчанию. Для этого используется функция union из модуля Data.Map.

Xmobar, по умолчанию, предоставляет одно сочетание клавиш для вызова терминала -- Mod+Shift+Enter. Изменить терминал можно через параметр terminal.

Полный конфигурационный файл ~/.xmonad/xmonad.hs

import XMonad
import XMonad.Hooks.ManageDocks
import XMonad.Layout.NoBorders
import XMonad.Layout.Tabbed
import XMonad.Layout.StackTile
import XMonad.Hooks.DynamicLog
import XMonad.Util.EZConfig
import XMonad.Operations
import qualified XMonad.StackSet as W
import qualified Data.Map as M

myWorkspaces = [ "1", "2", "3", "4", "5", "6", "7", "8", "9" ]

myLayouts = ( avoidStruts $ smartBorders $
Tall 1 (3/100) (1/2) |||
StackTile 1 (3/100) (1/2) |||
simpleTabbed ) |||
noBorders Full

myManageHook = composeAll [
className =? "Gtk-recordmydesktop" --> doFloat,
className =? "Xmessage" --> doFloat,
className =? "Gxmessage" --> doFloat,
className =? "Galculator" --> doFloat,
className =? "Gksu" --> doFloat ]

myLog = dynamicLogString xmobarPP {
ppCurrent = xmobarColor "green" "" . wrap "[" "]",
ppTitle = xmobarColor "lightblue" "" . wrap "[" "]",
ppHidden = xmobarColor "yellow" "",
ppHiddenNoWindows = xmobarColor "darkgray" "",
ppLayout = xmobarColor "orange" "" }

myKeys x = M.union (keys defaultConfig x) (keysToAdd x)
where
keysToAdd = c -> mkKeymap c $ [
("<XF86HomePage>", spawn "x-www-browser"),
("<XF86AudioRaiseVolume>", spawn "~/local/bin/pactrl.sh inc"),
("<XF86AudioLowerVolume>", spawn "~/local/bin/pactrl.sh dec"),
("<XF86AudioMute>", spawn "~/local/bin/pactrl.sh mute"),
("<XF86AudioPlay>", spawn "xterm -e cmus"),
("<XF86Mail>", spawn "icedove"),
("<XF86Search>", spawn "pcmanfm"),
("<XF86Calculator>", spawn "galculator"),
("<XF86Launch5>", spawn "emacs"),
("<XF86Launch6>", spawn "gthumb"),
("<XF86Favorites>", spawn "gksu halt"),
("M-<XF86Favorites>", spawn "gksu reboot"),
("<Print>", spawn "~/local/bin/printscreen.sh"),
("M1-<Tab>", windows W.focusDown),
("M1-<F4>", kill) ]

main = do
xmonad $ defaultConfig {
modMask = mod4Mask,
terminal = "xterm",
borderWidth = 3,
normalBorderColor = "gray",
focusedBorderColor = "red",
workspaces = myWorkspaces,
layoutHook = myLayouts,
logHook = myLog >>= xmonadPropLog,
manageHook = manageHook defaultConfig <+> manageDocks <+> myManageHook,
keys = myKeys }

Немного скриптов

Как Вы могли заметить, я привязал к клавишам два скрипта: один регулирует громкость PulseAudio, другой снимает скриншоты. Приведу их тут с кратким описанием для полноты картины.
#!/bin/bash

VOLUME_FILE=/tmp/pa_volume
MUTE_FILE=/tmp/pa_mute

function doMute() {
if [ -f $MUTE_FILE ];
then
pactl set-sink-mute 0 0
rm -rf $MUTE_FILE
else
pactl set-sink-mute 0 1
touch $MUTE_FILE
fi
}

function doChangeVolume() {
if [ ! -f $VOLUME_FILE ];
then
echo 100 > $VOLUME_FILE
fi
VOLUME=`cat $VOLUME_FILE`
NEW_VOLUME=$((VOLUME+$1))
if [ $NEW_VOLUME -lt 0 ];
then
exit 0
fi
pactl set-sink-volume 0 $NEW_VOLUME%
echo $NEW_VOLUME > $VOLUME_FILE
}

case $1 in
inc)
doChangeVolume 2
;;
dec)
doChangeVolume -2
;;
mute)
doMute
;;
esac
exit 0
Скрипт использует утилиту pactl для изменения громкости. Можно было бы использовать её напрямую в xmonad.hs, но существуют три проблемы:
  1. Из-за ошибки в программе, невозможно передать ей отрицательное значение для изменения громкости; она воспринимает его как неизвестный параметр;
  2. После того, как громкость достигла нуля, pactl продолжает её уменьшать, после чего придётся увеличивать её несколько раз, чтобы достичь нуля;
  3. Программа не предоставляет возможности узнать, в каком состоянии находится mute.
Примечание: по прошествии времени после публикации статьи, все проблемы с pactl решены.
  • Первая решается отменой восприятия "-2%" как параметра с помощью команды pactl set-sink-volume 0 -- -2%;
  • Вторая проблема решена в последней версии pulseaudio;
  • Последняя проблема не актуальна, так как появилась возможность попросить переключение состояния mute командой pactl set-sink-mute 0 toggle.
Скрипт записывает текущую громкость (в процентах) в файл и при поступлении команды либо увеличивает её, либо уменьшает. Для индикации состояния mute используется создание и удаление файла с предопределённым именем.

Для снятия скриншота используется конвеер
xwd -root | convert - <filename>
Скрипт нужен лишь для того, чтобы определить имя файла и показать его имя в диалоге с предложением открыть в просмоторщике.
#!/bin/bash

FILEPATH=~/
FILENAME=snapshot
FILEEXT=.png
FULLFILENAME=$FILEPATH$FILENAME$FILEEXT

if [ -f /usr/bin/gxmessage ];
then
XMESSAGE=gxmessage
else
XMESSAGE=xmessage
fi

for((I=1; ;I++)); do
if [ ! -f $FULLFILENAME ]; then
break
fi
FULLFILENAME=$FILEPATH$FILENAME$I$FILEEXT
done

xwd -root | convert - $FULLFILENAME
$XMESSAGE "Snapshot has been saved to $FULLFILENAME" -center -default Ok -buttons Ok:0,Open:1

if [ $? -eq 1 ];
then
gthumb $FULLFILENAME
fi

На скриншоте Вы могли заметить, что у меня на фоне корневого окна установлены обои. Делается это при помощи утилиты feh в два этапа. Первый -- это непосредственно установка изображения
$ feh --bg-scale <имя файла>
А второй -- восстановление изображения после перезапуска X`ов. Для этого нужно поместить такую команду в файл ~/.xsession
eval $(cat ~/.fehbg)