Контроль типов аргументов функций в Lua
Это значит, что тип в языке связан не с переменной, а с её значением:
Однако, нередко встречаются случаи, когда хочется жёстко контролировать тип переменной. Самый частый такой случай — проверка аргументов функций.
Рассмотрим наивный пример:
Если перепутать местами аргументы функции repeat, получим ошибку времени выполнения:
«Какой такой for?!» — скажет пользователь нашей функции, увидев это сообщение об ошибке. Функция внезапно перестала быть чёрным ящиком. Пользователю стали видны внутренности.
Ещё хуже будет, если мы случайно забудем передать второй аргумент:
Ошибки не возникло, но поведение потенциально неверное.
Это происходит из-за того, что в Луа внутри функций непереданные аргументы превращаются в nil.
Другая типичная ошибка возникает при вызове методов объектов:
Двоеточие — синтаксический сахар, неявно передающий первым аргументом сам объект, self. Если убрать весь сахар из примера, получим следующее:
Если при вызове метода написать вместо двоеточия точку, self не будет передан:
Слегка отвлечёмсяЕсли в случае с setn сообщение об ошибке достаточно понятно, то ошибка с repeat_message с первого взгляда выглядит мистически.
Что же произошло? Попробуем посмотреть внимательнее в консоли.
В первом случае мы записать в число значение по индексу «n_»:
На что нам совершенно законно ответили:
Во втором случае мы попытались прочесть значение из строки по тому же индексу «n_».
Всё просто. На строковый тип в Луа навешена метатаблица, перенаправляющая операции индексации в таблицу string.
Это позволяет использовать сокращённую запись при работе со строками. Следующие три варианта эквивалентны:
Таким образом, любая операция чтения индекса из строки обращается в таблицу string.
Хорошо ещё, что запись отключена:
В таблице string нет нашего ключа «n_» — поэтому for и ругается, что ему подсунули nil вместо верхней границы:
Но мы отвлеклись.
РешениеИтак, мы хотим контролировать типы аргументов наших функций.
Всё просто, давайте их проверять.
Посмотрим, что получилось:
Уже ближе к делу, но не очень наглядно.
Боремся за наглядностьПопробуем улучшить сообщения об ошибках:
= "number" then <br/> error ( <br/> "bad n type: expected `number', got `" .. type ( n ) <br/> 2 <br/> ) <br/> end <br/> if type ( message )
= "string" then <br/> error ( <br/> "bad message type: expected `string', got `" <br/> .. type ( message ) <br/> 2 <br/> ) <br/> end <br/> <br/> for i = 1 , n do <br/> print ( message ) <br/> end <br/> end
Второй параметр у функции error — уровень на стеке вызовов, на который нужно показать в стектрейсе. Теперь «виновата» не наша функция, а тот, кто её вызвал.
Сообщения об ошибках стали намного лучше:
Но теперь обработка ошибок занимает в пять раз больше полезной части функции.
Боремся за краткостьВынесем обработку ошибок отдельно:
Этим уже можно пользоваться.
Более полная реализация assert_is_* — здесь: typeassert.lua.
Работа с методамиПеределаем теперь реализацию метода:
Сообщение об ошибке выглядит несколько смущающе:
Ошибка с использованием точки вместо двоеточия при вызове метода встречается очень часто, особенно у неопытных пользователей. Практика показывает, что в сообщении для проверки self лучше на неё прямо указывать:
Теперь сообщение об ошибке максимально наглядно:
Мы добились желаемого результата по функциональности, но можно ли ещё повысить удобство использования?
Повышаем удобство использованияХочется наглядно видеть в коде, какого типа должен быть каждый аргумент. Сейчас тип зашит в имя функции assert_is_* и не очень выделяется.
Лучше уметь писать вот так:
Тип каждого аргумента наглядно выделен. Кода нужно меньше, чем в случае с assert_is_*. Описание даже чем-то напоминает Old Style C function declarations (ещё их называют K&R-style):
Но вернёмся к Луа. Теперь, когда мы знаем чего хотим, это можно реализовать.
= expected_type then <br/> error ( <br/> "bad argument #" .. ( ( i + 1 ) / 2 ) <br/> .. " type: expected `" .. expected_type<br/> .. "', got `" .. type ( value ) .. "'" ,<br/> 3 <br/> ) <br/> end <br/> end <br/> end
Попробуем, что получилось:
НедостаткиУ нас пропало настраиваемое сообщение об ошибке, но это не так страшно — чтобы понять, про какой аргумент идёт речь, достаточно его номера.
Нашей функции не хватает проверок на корректность самого вызова — на то, что передано чётное число аргументов, и на то, что все типы правильные. Эти проверки читателю предлагается добавить самостоятельно.
Работа с методамиВариант для методов отличается только тем, что мы должны дополнительно проверить self:
= "table" then <br/> error ( <br/> "bad self (got `" .. type ( v ) .. "'); use `:'" ,<br/> 3 <br/> ) <br/> end <br/> arguments ( . ) <br/> end <br/> <br/> foo = < ><br/> function foo:setn ( n ) <br/> method_arguments ( <br/> self,<br/> "number" , n<br/> ) <br/> self.n_ = n<br/> end
Полную реализацию семейства функций *arguments() можно посмотреть здесь: args.lua.
ЗаключениеМы создали удобный механизм для проверки аргументов функций в Луа. Он позволяет наглядно задать ожидаемые типы аргументов и эффективно проверить соответствие им переданных значений.
Время, потраченное на assert_is_*, тоже не пропадёт зря. Аргументы функций — не единственное место в Луа, в котором нужно контролировать типы. Использование функций семейства assert_is_* делает такой контроль более наглядным.
АльтернативыСуществуют и другие решения. См. Lua Type Checking в Lua-users wiki. Наиболее интересное — решение с декораторами:
Metalua включает расширение types для описания типов переменных (описание).