- (S1) Следуейте рекомендациям для выбранного языка программирования: F#, Haskell, Scala. Можете использовать и другие гайд-лайны, указывая на них ссылку в шапке вашего решения. То же требуется и для других языков.
- (S1) В Haskell, Scala и F# принят camelCase / PascalCase, в Lisp — lisp-case. В camelCase чаще всего пишут функции (кроме конструкторов типов и активных шаблонов), а в PascalCase — типы.
- (S1) Не забывайте пробелы вокруг бинарных операторов, внутри конструкторов списка и между функцией и аргументами.
- (S1) Обычно лучше не писать скобки, если их можно опустить.
- (S1) Старайтесь давать функциям, особенно для top level, осмысленные имена.
- (S2) Не нужно указывать типы функций, если код и так компилируется (для F#): лучше написать такой код и дать переменным такие имена, чтобы тип легко считывался программистами. Синтаксически в F# типы сильно перегружают код и скорее усложняют его чтение.
- (S2) Не нужно указывать типы .NET-стиле: например, вместо
List<'a>
илиlist<'a>
лучше писать'a list
. Допустимые исключения:Lazy<'t>
.
- (L1) Не оставляйте выражения, которые можно вычислить тривиально:
cond && true
,cond && false
,if cond then true else false
,cond = true
,cond = false
,if cond1 then true else cond2
и так далее. - (L1) Лямбда-функция
fun x -> f x
, гдеf
— просто какая-то функция, упрощается доf
. - (L1) Предпочитайте паттерн-матчинг условным выражениям.
- (L2) Не забывайте, что паттерн-матчинг по последнему аргументу функции можно переписать через
function
. Более того, обыычно аргумент, по которому идет сопоставление (как правило, это типы-контейнеры) намеренно ставят на последнее место; не только чтобы можно было писатьfunction
, но и чтобы было удобнее использовать каррирование: так, например, можно очень просто определить функцию частичных суммList.fold (+) 0
. - (L2) Помните, что в паттерн-матчинге есть guard'ы (условия
when
). Кроме того, Вложенный паттерн-матчинг или паттерн-матчинг, в котором каждая ветка разворачивается в условное выражение, чаще всего сокращается до простого паттерн-матчинга с guard'ами. - (L2) Запись
if count > 0 then foo ()
читается заметно проще, чем такая или аналогичная черезfunction
:match count with | 0 -> () | _ -> foo ()
- (L3) Если часть шаблона не используется, заменяйте ее на символ
_
. Кроме того, рекомендую подбирать имена для шаблонов-констант таким образом, чтобы они не перекрывали имена аргументов функций (добавляйте штрих'
). Если же вы, скажем, сопоставляетеn
cn
, то можно заменить шаблон на_
, а в правой части использоватьn
из аргументов. - (L4) Нередко встречается такая конструкция: объявление списка, затем новая константа, где к этому списку применили какую-то одну функцию и так далее. Вместо это лучше реализовать то же самое в виде конвейера: передавать список в одну функцию, затем сразу же результат в третью и так далее; для этого есть операторы
|>
и<|
. Но здесь нужно следить за балансом: каждая из функций должна быть достаточно простой, чтобы код легко считывался. Если этого не происходит — нужны константы с понятными именами для промежуточных вычислений. - (L4) Как правило, конвейер лучше читается, если его записать по строчно, начиная с
|>
каждую функцию. Исключения допустимы, если весь конвейер помещается в одну строку (пусть 80 символов, а лучше меньше).
- (E1) Каждый раз при написании рекурсивной функции подумайте, получилась ли рекурсия хвостовой и можно ли ее такой сделать. Если можно, то лучше сделать.
- (E2) Старайтесь не использовать в рекурсии конкатенацию списков, если второй список имеет константную длину (чаще всего это один элеимент). Конкатенация работает за линию от длины первого списка, значит, вы получаете потенциальный квадрат в асимптотике. Проще добавлять элементы в начало списка, а в конце сделать за линию разворот списка.
- (E3) Не используйте
List.item
там, где можно обойтись обходом списка.List.item
имеет линейную сложность, поэтому, скорее всего, если индекс растет с каждой итерацией, сложность вашего решения умножается на одну линию. Кроме того, вместо дажеList.item 0
лучше писатьList.head
. Первый вариант формально имеет линейную сложность, второй — константную (и он компактнее и идейно более правильный). Поэтому при беглом анализе асимптотики в коде сList.head
возникает меньше вопросов.
- (D1) The Don't Repeat Yourself Principle. Не допускается дублирование логики. Его можно ликвидировать с дополнительной абстракции. В нашем случае почти всегда это новая функция.
- (D2) Функция, объявленная во внешней области видимости, доступна при импорте всем пользователям. Поэтому если у нее среди аргументов есть аккумулятор, пользователю придется угадывать, как функция реализована, чтобы подставить правильное значение в этот аргумент. Аккуратнее будет написать вложенную функцию, которая имеет аккумуляторы, а во внешней оставить только полезные для интерфейса аргументы и вызов функции-реализации.
- (D3) Не пишите излишне абстрактный код. Каждая функция должна иметь конкретную задачу: как-то обработать коллекцию, что-то посчитать и так далее. Пример плохой функции:
forLoop
с семинара. По ней функции непонятно, как ей пользоваться, она больше похожа на синтаксическую конструкцию языка, чем на собственно функцию (так и была придумана: показать, как делать циклы).