. Контроль типов аргументов функций в Lua
Контроль типов аргументов функций в Lua

Контроль типов аргументов функций в 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 для описания типов переменных (описание).

📎📎📎📎📎📎📎📎📎📎