Serguey Zefirov (thesz) wrote,
Serguey Zefirov
thesz

Немного про стиль кодирования на Хаскеле.

Тут случилась ветка с критикой Хаскеля и разбором стиля кодирования.

К ней я и хочу добавить несколько слов.

Я использую Хаскель в качестве основного инструмента на протяжении двух лет с небольшими перерывами на C/C /Tcl/язык спецвычислителя. Область применения - моделирование аппаратуры, DSEL-ы для небольших задач разного рода и разбор текстов программ на разных языках, примерно в таком порядке частоты использования.

Стиль использования, в общем и целом, сложился. За редкими исключениями, конечно, но я собираюсь описать и сам стиль, и причины, почему так, и что еще не устаканилось окончательно.

Итак.

В самом начале модуля я стараюсь дать его название и краткое описание в стиле HadDoc:
-- | VeImpStuff.hs
-- A module for miscellaneous, nevertheless very important, stuff
Вроде, мелочь, но лично меня напрягает видеть сперва лицензионное соглашение, потом ряд инклудов-импортов, потом вспомогательные функции и потом уже комментарии, по которым можно догадаться о назначении текста программы в файле. Несмотря на то, что я помню о содержимом практически всех моих программ, я хочу дать читающим код некий намек, туда ли они вообще смотрят. Лицензия и копирайты подождут (а их в моем коде и нет;).

Поскольку мы переходим к списку экспортируемой информации, то пара слов о списках.

Списки практически везде в perl-стиле:
module VImpStuff (
         veImpFunc1
        ,VeImpData(..)
    ) where
Функции и типы данных - camel cased. Предикаты - isCamelCasedCondition, редко что-либо другое.

Сам список экспорта отсутствует (экспортируется все) ровно до того момента, пока я не завершу работы над модулем хотя бы на две трети. Тогда список фиксируется и, скорее всего, он уже потребует мало внимания.

Далее у нас идут импорты. Импорты у меня идут вперемешку, хотя я стараюсь группировать библиотечные импорты (Data.Byte и Data.List) и импорты частей программы. Порядок следования этих групп не фиксирован, но, я думаю, это уже такая мелочь.

Отдельно стоит отметить Data.Map и Data.Set. Их желательно импортировать с указанием имени (import qualified Data.Map [as M]), поскольку у них многие имена пересекаются. С другими модулями у меня было меньше проблем в этом плане.

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

Теперь мы переходим к самому тексту.

Значительные части программы, обьединенные по смыслу, отделяются длинными комментариями с обьяснением смысла группы.

Функции пишутся как обычно. Сперва разбор по образцу, потом защита предикатами (guards), к одному образцу прилагается where, если необходимо. Ключевое слово where отстоит на табуляцию и пишется на отдельной строке, относящиеся к нему определения - еще на табуляцию. Примерно то же относится и к let, только в этом случае let не выносится на отдельную строку, а in - выносится. Пара let-in может быть и не отделена строками, но только для одного короткого определения: f x f = let sq x = x*x in sq x+sq  y.

К do относятся ровно те же правила, что и к let. Только у do нет пары, как in у let.

Строки программы следует держать длиной менее 80 символов за исключениями сообщений об ошибках, отладочной печати и тогда, когда это не совсем возможно. В последнем случае я рефакторю программу, если только не тороплюсь. Индентация where и его внутренностей и размер табуляции в 8 символов помогают получить рефакторинг пораньше.

Это я утащил из "c style guide" для ядра Линукса, использовал в Си, теперь перенес в Хаскель. ;)

Общий вид таков:
align n
	| isPow2 n = do
		mem@(Mem {memCurrAddr = currAddr}) <- get
		put $ mem {memCurrAddr = alignFunc currAddr}
	| otherwise = error $ "Invalid alignment " ++ show n ++ ", must be power of 2."
	where
		isPow2 0 = False
		isPow2 1 = True
		isPow2 (-1) = False	-- just in case.
		isPow2 n = isPow2 (div n 2)
		mask = n-1
		alignFunc x
			| x == masked = x
			| otherwise   = masked   n
			where
				masked = x .&. complement mask


Собственно, where и in служат своеобразными разделителями параграфов.

Если where (и in) оставлять на той же строке, что и выражение, то теряется читабельность:
		alignFunc x = masked   n where
				masked = x .&. complement mask
Point-free стиль ((-) . (*2)) я использую редко. Но конвейерный стиль - с $, - наоборот.

Это связано с тем, что с point-free стилем наталкиваешься на monomorphism restriction чаще, чем этого хотелось бы:
-- гарантированная ошибка:
f = return . Just

-- Ошибки не будет:
f x = return $ Just x
Собственно, это можно преодолеть с помощью указания типа функции, но я не люблю так делать потому, что это некоторым образом фиксирует тип и его потом наверняка придется править.

Кстати.

Указания сигнатур типов функция верхнего уровня я не использую. Точнее, использую в редких случаях, когда я точно знаю, что и как будет работать, когда есть спецификация (хотя бы в уме). Но даже в этом случае высока вероятность того, что я удалю тип и буду некоторое время работать без его указания.

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

А до этого можно воспользоваться REPL и экспериментировать с типами через ":t" вот, примерно, так:
Prelude> :t (*)
(*) :: (Num a) => a -> a -> a
Prelude> :t (*) . (+2)
(*) . ( 2) :: (Num a) => a -> a -> a
Prelude> :t ((*) . (+2)) 5
((*) . ( 2)) 5 :: (Num a) => a -> a
Prelude> :t ((*) . (+2)) 5 10
((*) . ( 2)) 5 10 :: (Num a) => a
Prelude> ((*) . (+2)) 5 10
70
Prelude> ((*) . (+2)) 5 20
140
Prelude>
(уж больно мне (*) . (+2) в душу запало;)

Лямбды я, как и lomeo, выношу в отдельные определения. Это серьезно улучшает читаемость. За исключением совсем коротких тривиальных преобразований наподобие (\(a,b,c) -> (a,b,sum/c)).

Комбинаторный стиль с использованием операторов я использую достаточно редко. Самое часто использование - это в печати отчетов. Например, надо сформировать список пар (label,value), и потом их отобразить, как unlines (map (\(label,value) -> label ++ ": " ++ show value) lvList). За небольшим исключением: для списков иногда (не всегда!) необходимо выводить длину. Тогда получится своеобразная "запятая" в паре-тройке вариантов:
infix 0 %%, %%! -- приоритет 0, как у другой запятой, $

label %% value = label ++ ": " ++ show value
label %! values = label ++ ": {" ++ show (length values)    "} " ++ show values
Как-то так.

Однако собрать монаду для выполнения действий над состоянием - на раз. Монадический комбинаторный стиль, наоборот, встречается часто, особенно, во всяких DSEL. А как еще сфомировать образ памяти, например?

Ну, и последнее.

Для функций я иногда применяю суффиксы. В моделировании существует два вида функций - преобразование состояний и функции соединения проводов.

Первые имеют суффикс F (function), вторые - C (circuit).

Остальные (вспомогательные) функции именуются, как обычно.

Это неплохая практика, недавно начал использовать, вполне доволен. Расходы невелики, а читаемость повышается.
Tags: Хаскель, стиль кодирования
Subscribe
  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your reply will be screened

    Your IP address will be recorded 

  • 43 comments