Руководство по написанию кода на Lua | Tarantool
Документация на русском языке
поддерживается сообществом
Contributing Рекомендации Руководство по написанию кода на Lua

Руководство по написанию кода на Lua

Для вдохновения:

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

Дзен языка программирования Python подходит и здесь; используйте его с умом:

Красивое лучше, чем уродливое.
Явное лучше, чем неявное.
Простое лучше, чем сложное.
Сложное лучше, чем запутанное.
Плоское лучше, чем вложенное.
Разреженное лучше, чем плотное.
Читаемость имеет значение.
Особые случаи не настолько особые, чтобы нарушать правила.
При этом практичность важнее безупречности.
Ошибки никогда не должны замалчиваться.
Если не замалчиваются явно.
Встретив двусмысленность, отбрось искушение угадать.
Должен существовать один – и, желательно, только один – очевидный способ сделать это.
Хотя он поначалу может быть и не очевиден.
Сейчас лучше, чем никогда.
Хотя никогда зачастую лучше, чем прямо сейчас.
Если реализацию сложно объяснить – идея плоха.
Если реализацию легко объяснить – идея, возможно, хороша.
Пространства имен – отличная штука! Сделаем побольше!

  • 4 пробела, а не табуляция. Библиотека PIL предлагает использовать два пробела, но разработчик читает код от 4 до 8 часов в день, а различать отступы с 4 пробелами легче. Почему именно пробелы? Соблюдение однородности.

    Можно использовать строки режима (modelines) vim:

    -- vim:ts=4 ss=4 sw=4 expandtab
    
  • Файл должен заканчиваться на один символ переноса строки, но не должен заканчиваться на пустой строке (два символа переноса строки).

  • Отступы всех do/while/for/if/function должны составлять 4 пробела.

  • or/and в if должны быть обрамлены круглыми скобками (). Пример:

    if (a == true and b == false) or (a == false and b == true) then
        <...>
    end -- хорошо
    
    if a == true and b == false or a == false and b == true then
        <...>
    end -- плохо
    
    if a ^ b == true then
    end -- хорошо, но не явно
    
  • Преобразование типов

    Не используйте конкатенацию для конвертации в строку или в число (вместо этого воспользуйтесь tostring/tonumber):

    local a = 123
    a = a .. ''
    -- плохо
    
    local a = 123
    a = tostring(a)
    -- хорошо
    
    local a = '123'
    a = a + 5 -- 128
    -- плохо
    
    local a = '123'
    a = tonumber(a) + 5 -- 128
    -- хорошо
    
  • Постарайтесь избегать несколько вложенных if с общим телом оператора:

    if (a == true and b == false) or (a == false and b == true) then
        do_something()
    end
    -- хорошо
    
    if a == true then
        if b == false then
            do_something()
        end
    if b == true then
        if a == false then
            do_something()
        end
    end
    -- плохо
    
  • Избегайте множества конкатенаций в одном операторе, лучше использовать string.format:

    function say_greeting(period, name)
         local a = "good  " .. period .. ", " .. name
    end
    -- плохо
    
    function say_greeting(period, name)
        local a = string.format("good %s, %s", period, name)
    end
    -- хорошо
    
    local say_greeting_fmt = "good %s, %s"
    function say_greeting(period, name)
        local a = say_greeting_fmt:format(period, name)
    end
    -- лучше всего
    
  • Используйте and/or для указания значений переменных, используемых по умолчанию,

    function(input)
        input = input or 'default_value'
    end -- хорошо
    
    function(input)
        if input == nil then
            input = 'default_value'
        end
    end -- нормально, но избыточно
    
  • операторов if и возврата:

    if a == true then
        return do_something()
    end
    do_other_thing() -- хорошо
    
    if a == true then
        return do_something()
    else
        do_other_thing()
    end -- плохо
    
  • Использование пробелов:

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

      function name (arg1,arg2,...)
      end -- плохо
      
      function name(arg1, arg2, ...)
      end -- хорошо
      
    • добавляйте пробел после маркера комментария

      while true do -- встроенный комментарий
      -- комментарий
      do_something()
      end
      --[[
        многострочный
        комментарий
      ]]--
      
    • примыкающие конструкции

      local thing=1
      thing = thing-1
      thing = thing*1
      thing = 'string'..'s'
      -- плохо
      
      local thing = 1
      thing = thing - 1
      thing = thing * 1
      thing = 'string' .. 's'
      -- хорошо
      
    • добавляйте пробел после запятых в таблицах

      local thing = {1,2,3}
      thing = {1 , 2 , 3}
      thing = {1 ,2 ,3}
      -- плохо
      
      local thing = {1, 2, 3}
      -- хорошо
      
    • используйте пробелы в определениях ассоциативного массива по сторонам от знаков равенства и запятых

      return {1,2,3,4} -- плохо
      return {
          key1 = val1,key2=val2
      } -- плохо
      
      return {
          1, 2, 3, 4
          key1 = val1, key2 = val2,
          key3 = vallll
      } -- хорошо
      

      также можно применить выравнивание:

      return {
          long_key  = 'vaaaaalue',
          key       = 'val',
          something = 'even better'
      }
      
    • также можно добавлять пустые строки (не слишком часто) для выделения групп связанных функций. Пустые строки не стоит добавлять между несколькими связанными программами в одну строку (например, в формальной реализации)

      не слишком часто можно добавлять пустые строки в коде функций, чтобы отделить друг от друга логические части

      if thing then
          -- ...что-то...
      end
      function derp()
          -- ...что-то...
      end
      local wat = 7
      -- плохо
      
      if thing then
          -- ...что-то...
      end
      
      function derp()
          -- ...что-то...
      end
      
      local wat = 7
      -- хорошо
      
    • Удаляйте символы пробела в конце файла (они категорически запрещаются). Для их удаления в vim используйте :s/\s\+$//gc.

Следует избегать глобальных переменных. В исключительных случаях используйте переменную _G для объявления, добавьте префикс или таблицу вместо префикса:

function bad_global_example()
end -- глобальная, очень-очень плохо

function good_local_example()
end
_G.modulename_good_local_example = good_local_example -- локальная, хорошо
_G.modulename = {}
_G.modulename.good_local_example = good_local_example -- локальная, лучше

Всегда добавляйте префиксы во избежание конфликта имен

  • имена переменных/»объектов» и «методов»/функций: snake_case
  • имена «классов»: CamelCase
  • частные переменные/методы (в будущем параметры) объекта начинаются с символа подчеркивания <object>._<name>. Избегайте local function private_methods(self) end
  • логическое именование приветствуется is_<...>, isnt_<...>, has_, hasnt_.
  • для «самых локальных» переменных: - t для таблиц - i, j для индексации - n для подсчета - k, v для получения из pairs() (допускаются, _ если не используются) - i, v is what you get out of ipairs() (допускаются, _ если не используются) - k/key для ключей таблицы - v/val/value для передаваемых значений - x/y/z для общих математических величин - s/str/string для строк - c для односимвольных строк - f/func/cb для функций - status, <rv>.. или ok, <rv>.. для получения из pcall/xpcall - buf, sz – это пара (буфер, размер) - <name>_p для указателей - t0.. для временных отметок - err для ошибок
  • допускается использование сокращений, если они недвусмысленны, и если вы документируете их.
  • глобальные переменные пишутся ЗАГЛАВНЫМИ_БУКВАМИ. Если это системная переменная, для определения используется символ подчеркивания (_G/_VERSION/..)
  • именование модулей – с помощью snake_case (избегайте подчеркивания и дефисов) - „luasql“, а не „Lua-SQL“
  • *_mt и *_methods определяют метатаблицу и таблицу методов

Всегда пользуйтесь круглыми скобками при вызове функций, за исключением множественных случаев (распространенные идиомы в Lua):

  • функции *.cfg{ } (box.cfg/memcached.cfg/..)
  • функция ffi.cdef[[ ]]

Избегайте конструкций такого типа:

  • <func>“<name>“ (особенно избегайте require“..“)
  • function object:method() end (используйте functon object.method(self) end)
  • не вставляйте точку с запятой в качестве символа-разделителя в таблице (только запятые)
  • точки с запятой в конце строки (только для разделения нескольких операторов в одной строке)
  • старайтесь избегать создания ненужных функций (closures/..)

Не начинайте создание модуля с указания лицензии/авторов/описания, это можно сделать в файлах LICENSE/AUTHORS/README соответственно. Для написания модулей используйте один из двух шаблонов (не используйте modules()):

local M = {}

function M.foo()
...
end

function M.bar()
...
end

return M

или

local function foo()
...
end

local function bar()
...
end

return {
foo = foo,
bar = bar,
}

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

Многострочные комментарии: используйте соответствующие скобки (--[[ ]]--) вместо простых (--[[ ]]).

Комментарии к доступным функциям (??):

--- Копирование любой таблицы (поверхностное и глубокое)
-- * deepcopy: копирует все уровни
-- * shallowcopy: копирует только первый уровень
-- Поддержка метаметода __copy для копирования специальных таблиц с метатаблицами
-- @function gsplit
-- @table         inp  оригинальная таблица
-- @shallow[opt]  sep  флаг для поверхностной копии
-- @returns            таблица (копия)

Используйте модуль tap, чтобы написать эффективные тесты. Пример файла с тестом:

#!/usr/bin/env tarantool

local test = require('tap').test('table')
test:plan(31)

do -- check basic table.copy (deepcopy)
    local example_table = {
        {1, 2, 3},
        {"help, I'm very nested", {{{ }}} }
    }

    local copy_table = table.copy(example_table)

    test:is_deeply(
        example_table,
        copy_table,
        "checking, that deepcopy behaves ok"
    )
    test:isnt(
        example_table,
        copy_table,
        "checking, that tables are different"
    )
    test:isnt(
        example_table[1],
        copy_table[1],
        "checking, that tables are different"
    )
    test:isnt(
        example_table[2],
        copy_table[2],
        "checking, that tables are different"
    )
    test:isnt(
        example_table[2][2],
        copy_table[2][2],
        "checking, that tables are different"
    )
    test:isnt(
        example_table[2][2][1],
        copy_table[2][2][1],
        "checking, that tables are different"
    )
end

<...>

os.exit(test:check() and 0 or 1)

После тестирования кода вывод будет примерно таким:

TAP version 13
1..31
ok - checking, that deepcopy behaves ok
ok - checking, that tables are different
ok - checking, that tables are different
ok - checking, that tables are different
ok - checking, that tables are different
ok - checking, that tables are different
...

Принимайте разнообразные значения и выдавайте строго определенные.

В рамках обработки ошибок это означает, что в случае ошибки вы должны предоставить объект ошибки как второе возвращаемое значение. Объектом ошибки может быть строка, Lua-таблица или cdata, в последнем случае должен быть определен метаметод __tostring.

В случае ошибки нулевое значение nil должно быть первым возвращаемым значением. В таком случае ошибку трудно игнорировать.

При проверке возвращаемых значений функции проверяйте сначала первый аргумент. Если это nil, ищите ошибку во втором аргументе:

local data, err = foo()
if not data then
    return nil, err
end
return bar(data)

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

В редких случаях nil можно сделать возвращаемым значением. В таком случае можно сначала проверить ошибку, а потом вернуть значение:

local data, err = foo()
if not err then
    return data
end
return nil, err
Нашли ответ на свой вопрос?
Обратная связь