Версия:

Руководство разработчика / Рекомендации / Руководство по написанию кода на Lua
Руководство разработчика / Рекомендации / Руководство по написанию кода на Lua

Руководство по написанию кода на 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 -- проверка базовой table.copy (глубокая копия)
     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() == true 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
     return nil, err
 end
 return bar(data)

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

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

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