Lattelua — это динамический скриптовый язык, компилируемый в Lua. Он основан на семантике(Парсер/Лексер) MoonScript, но использует JS-подобный синтаксис, что обеспечивает свободное форматирование кода и легкость минификации.
Основные отличия от MoonScript:
- Блочная структура: Использование
{и}вместо отступов. - Свободное форматирование: Игнорирование переносов строк и пробелов.
- Разделители: Использование
;для разделения инструкций (препроцессор заменяет их на перевод строки). - Синтаксис методов: Оператор
::для вызова методов экземпляра. - Switch: Ключевое слово
caseвместоwhen. - Обработка ошибок: Встроенная конструкция
try/catch/finally. - Автономный интерпретатор:
lluaс возможностью листинга компиляции.
- Базовый синтаксис
- Переменные и присваивание
- Типы данных и таблицы
- Управляющие конструкции
- Циклы
- Линейные декораторы
- Функции
- Объектно-ориентированное программирование
- With оператор
- Do оператор
- Обработка ошибок (Try-Catch)
- Автономный интерпретатор
В Lattelua отступы не имеют значения. Группировка выражений происходит с помощью { и }. Символ ; используется как разделитель инструкций, что позволяет писать код в одну строку:
-- Обычная запись
if true {
print("Hello")
}
-- Однострочная запись
if true { print("Hello") }Комментарии игнорируются компилятором. Символ ; внутри комментариев и строк не обрабатывается препроцессором:
-- Это однострочный комментарий
--[[
Это многострочный комментарий.
Он работает точно так же, как в Lua.
В MoonScript такой тип комментариев не поддерживается!
--]]По умолчанию все переменные являются локальными (local):
a = 1 -- local a = 1
str = "hello" -- local str = "hello"
x, y = 10, 20 -- local x, y = 10, 20Доступны операторы быстрого обновления значений: +=, -=, *=, /=, %=, ..=:
count = 0
count += 1 -- count = count + 1
name = "Lattelua"
name ..= " Lang" -- КонкатенацияЧтобы создать глобальную переменную или экспортировать её из модуля, используется ключевое слово export:
export VERSION = "1.0"Это особенно полезно при объявлении того, что будет видно извне в модуле:
-- some module.llua
export some_print
add = (x, y) -> { x + y }
some_print = (x, y) -> {print "Addition is: ", add x, y}
-- some script.llua
require "some_module"
some_module.some_print 5, 10 -- 15
print some_module.add 5, 10 -- errors, `add` not visibleЭкспорт не будет иметь эффекта, если в области видимости уже есть локальная переменная с таким же именем.
В контексте переменных, часто требуется перенести некоторые значения из таблицы/модуля в текущую область как локальные переменные по их имени.
Для этого используется ключевое слово import:
import insert from table -- local insert = table.insertМожно указать несколько имен, каждое через запятую:
import C, Ct, Cmt from lpeg -- local C, Ct, Cmt = lpeg.C, lpeg.Ct, lpeg.CmtИногда требуется, чтобы таблица была передана в качестве self-аргумента. Для сокращения можно добавить префикс :: к имени, чтобы связать функцию с этой таблицей:
t = {
val: 100
add: (value) => {
self.val + value
}
}
import ::add from t
print add 22 -- equivalent to add(t, 22) or t::add(22) num = 123
float = 1.5
str_double = "Text"
str_single = 'Text'
str_multi = [[
multi line
text
]]
bool = true
nothing = nilДопускается смешивать выражения со строковыми литералами, используя #{} синтаксис:
print "This is #{math.random() * 100}% work, I'm sure" -- print("This is " .. tostring(math.random() * 100) .. "% work, I'm sure")Интерполяция строк доступна только в строках, заключенных в двойные кавычки.
Как и в Lua, таблицы заключаются в фигурные скобки:
array = { 1, 2, 3, 4 }В отличие от Lua, присвоение значения ключу в таблице выполняется с помощью : (вместо =):
config = {
port: 8080,
host: "localhost",
list: { 1, 2, 3 },
["key with spaces"]: "some value"
}Перевод строки можно использовать для разделения значений вместо запятой (или и то, и другое):
config = {
port: 8080
host: "localhost"
list: { 1, 2, 3 }
["key with spaces"]: "some value"
}Ключи таблицы могут быть ключевыми словами языка без экранирования:
t = {
do: "do"
end: "end"
}Если создается таблица из переменных и требуется, чтобы ключи совпадали с именами переменных, можно использовать префиксный оператор ::
gender = "male"
age = 25
person = {
:gender -- gender: gender
:age -- age: age
key: "value" -- key: "value"
}
print :gender, :age -- {gender: gender, age: age}Если требуется, чтобы ключ был результатом выражения, можно обернуть его в [], как и в Lua. Также возможно использовать строковый литерал непосредственно в качестве ключа, исключая квадратные скобки. Это полезно, если ключ содержит специальные символы:
t = {
[1 + 2]: "three",
["some value"]: true,
"another some value": false
}Деструктуризация - это способ быстрого извлечения значений из таблицы по их имени или положению в таблицах на основе массива.
vec = { x: 10, y: 20, z: 30 }
{ :x, :y } = vec
print(x, y) -- 10 20
arr = {1, 2, 3}
{f,_,t} = arr
print f, t -- 1 3Это также работает с вложенными структурами данных:
obj = {
array: {1, 2, 3, 4}
properties: {
align: "center"
vec: { x: 10, y: 20, z: 30 }
}
}
{
array: { first, second }
properties: {
:align
vec: { :x, :y }
}
} = obj
-- first, second, align, x, y = obj.array[1], obj.array[2], obj.properties.align, obj.properties.vec.x, obj.properties.vec.y
Обычно значения из таблицы извлекаются и присваиваются локальным переменным, имеющим то же имя, что и ключ. Чтобы избежать повторения, возможно использовать префиксный оператор ::
{:concat, :insert} = table -- local concat, insert = table.concat, table.insertПо сути, это то же самое, что и import, но мы можем переименовать поля, которые хотим извлечь:
{:mix, :max, random: rand } = math -- local mix, max, rand = math.mix, math.max, math.randomДеструктуризация также может проявляться в тех местах, где неявно выполняется присваивание:
array = {
{1, 2, 3, 4}
{5, 6, 7, 8}
}
for {first, second} in *array {
print first, second -- 1 2 & 5 6
}
Генераторы предоставляют удобный синтаксис для создания новой таблицы путем итерации по некоторому существующему объекту и применения выражения к его значениям. Существует два типа генераторов: генератор списка и генератор таблицы. Они оба создают таблицы Lua. Генераторы списков накапливают значения в таблицу, подобную массиву, а генераторы таблиц позволяют устанавливать как ключ, так и значение на каждой итерации.
Следующий пример создаёт копию таблицы элементов, с удвоенными значениями:
array = { 1, 2, 3, 4 }
doubled = [item * 2 for i, item in ipairs array] -- doubled = { 2, 4, 6, 8 }Элементы, включенные в новую таблицу, можно ограничить с помощью when выражения:
iter = ipairs array
slice = [item for i, item in iter when i > 1 and i < 3] -- slice = { 2 }Операторы for и when возможно объединять в цепочки сколько угодно. Единственное требование, чтобы в выражении был хотя бы один оператор for.
Использование нескольких операторов for аналогично использованию вложенных циклов:
x = {4, 5, 6, 7}
y = {9, 2, 3}
points = [{x,y} for x in ipairs x for y in ipairs y]Синтаксис генератора таблиц очень похож, отличается только использованием {} и получением двух значений на каждой итерации:
t ={
gender: "male",
age: 25
}
copy = {k,v for k,v in pairs t}Генераторы таблиц, как и генераторы списков, также поддерживают несколько операторов for и when:
copy = {k,v for k,v in pairs t when k != "gender"}if x > 10 {
print("Big")
} elseif x == 10 {
print("Equal")
} else {
print("Small")
}
-- Unless (если НЕ)
unless ready {
init()
}
-- Тернарный оператор / Однострочный if
val = if check { true; } else { false; }Условные выражения также можно использовать в операторах возврата и присваиваниях:
test = (c)->{
if c {
true
} else {
false
}
}
out = if test true {
"is true"
} else {
"is false"
}
print out -- "is true"Использует ключевое слово case для веток и else для значения по умолчанию:
value = 2
switch value {
case 1
print("One")
case 2
print("Two")
case 1,2,3
print "One..Three"
else
print("Other")
}switch также можно использовать в качестве выражения, тем самым присвоить результат switch переменной:
out = switch value {
case 1
"One"
case 2
"Two"
case 1,2,3
"One..Three"
else
"Other"
}
print out -- "Two"-- Без шага
for i = 1, 10 {
print(i) -- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
}
-- С шагом
for i = 0, 10, 2 {
print(i) -- 0, 2, 4, 6, 8, 10
}t = { a: 1, b: 2 }
for k, v in pairs(t) {
print(k, v)
}Цикл for также можно использовать в качестве выражения. Последний оператор тела цикла преобразуется в выражение и добавляется в таблицу накопительного массива.
Удвоение каждого четного числа:
doubled = for i=1,20 {
if i % 2 == 0 {
i * 2
} else {
i
}
}
print i for _,i in ipairs doubled -- 4, 8, 12, 16, 20, 24, 28, 32, 36, 40Также возможно фильтровать значения, комбинируя выражения цикла for с оператором continue.
Циклы for в конце тела функции не накапливаются в таблице для возвращаемого значения (вместо этого функция вернет nil).
Возможно использовать явный оператор возврата, либо цикл можно преобразовать в генератор списка.
funca = -> {for i=1,10 {i}}
funcb = -> {return [i for i=1,10] }
print funca() -- prints nil
print funcb() -- prints table objectЭто сделано для того, чтобы избежать ненужного создания таблиц для функций, которым не нужно возвращать результаты цикла.
Цикл while также существует в двух вариантах:
i = 10
while i > 0 {
print i
i -= 3
}
while running == true {some_func()}Как и в случае с for, в цикле while также можно использовать выражение. Кроме того, чтобы функция возвращала накопленное значение цикла while, оператор должен быть возвращен явно.
Оператор break прерывает цикл (while или for), в теле которого встречается. В результате выполнения оператора break управление передаётся первой инструкции, следующей непосредственно за оператором цикла.
i = 0
while i < 10 {
break if i > 5
print i
i += 1
}Оператор continue можно использовать для пропуска текущей итерации в цикле.
i = 0
while i < 10 {
continue if i % 2 == 0
print i
i += 1
}Также continue можно использовать с выражениями цикла, чтобы предотвратить накопление этой итерации в результате.
В этом примере массив фильтруется только по четным числам:
array = {1,2,3,4,5,6}
odds = for x in ipairs array {
continue if x % 2 == 1
x
}Все функции создаются с использованием функционального выражения. Простая функция обозначается стрелкой: ->
some_func = ->
some_func() -- call that empty functionТело функции может представлять собой либо один оператор, либо серию, помещенных непосредственно в блок фигурных скобок, сразу после стрелки:
funca = -> {print "hello world"}
funcb = -> {
message = "world"
print "hello #{message}"
}Если функция не имеет аргументов, ее можно вызвать с помощью оператора !, вместо пустых круглых скобок. ! вызов - предпочтительный способ вызова функций без аргументов.
funca!
funcb()Функции с аргументами можно создать, указав перед стрелкой список имен аргументов в круглых скобках:
sum = (a, b) -> {
return a + b
}Для аргументов функции можно указать значения по умолчанию. Аргумент считается пустым, если его значение равно нулю. Любые нулевые аргументы, имеющие значение по умолчанию, будут заменены перед запуском тела функции.
greet = (name = "World") -> {
print("Hello " .. name)
}Значения аргументов по умолчанию вычисляется в теле функции в порядке объявления аргументов. Именно по этой причине значения по умолчанию имеют доступ к ранее объявленным аргументам.
(x=100, y=x+1000) -> {
print x + y
}Функции можно вызывать, перечисляя аргументы после имени выражения, результатом которого является функция. При объединении вызовов функций аргументы применяются к ближайшей функции слева.
sum 10, 20 -- sum(10, 20)
print sum 10, 20 -- print(sum(10, 20))
a b c "a", "b", "c" -- a(b(c("a", "b", "c")))Чтобы избежать двусмысленности при вызове функций, аргументы также можно заключать в круглые скобки. Это необходимо в примере ниже, чтобы гарантировать, что правильные аргументы будут отправлены в правильные функции.
print "x:", sum(10, 20), "y:", sum(30, 40) -- print("x:", sum(10, 20), "y:", sum(30, 40))Между открывающей скобкой и функцией(sum) не должно быть пробела.
Как и в Lua, функции могут возвращать несколько значений. Последний оператор должен представлять собой список значений, разделенных запятыми:
some_func = (x, y) -> {x + y, x - y}
a, b = some_func 10, 20Для создания функций предусмотрен специальный синтаксис =>, который автоматически включает аргумент self.
obj = {
val: 10
update: (num) => {
self.val = num -- self передается автоматически
}
}
obj::update(13)
print obj.val -- val = 13Для удобства операторы цикла for и if можно применять к отдельным операторам в конце строки:
print "hello world" if 1 == 1И с базовыми циклами:
print "value: #{v}" for _, v in ipairs {1,2,3,4,5,6}Класс объявляется с помощью оператора class, за которым следует табличное объявление, в котором перечислены все методы и свойства.
class Animal {
new: (name) => {
self.name = name
}
speak: => {
print(self.name)
}
}Объявление класса также можно использовать как выражение, которое можно присвоить переменной или вернуть явно.
Метод new, если определён, становится конструктором.
Создание экземпляра класса осуществляется путем вызова имени класса в качестве функции.
dog = Animal "woof woof"Все свойства класса являются общими для всех экземпляров. Это нормально для методов, но для других типов объектов могут возникнуть нежелательные результаты:
class Animal {
speech: {}
new: (speech) => {
table.insert self.speech, speech
}
speak: (who) => {
print "#{who} say: #{speech}" for _, speech in ipairs self.speech
}
}
dog = Animal "woof"
cat = Animal "meow"
dog::speak("dog") -- will print both `woof` and `meow`
cat::speak("dog") -- will print both `woof` and `meow`свойство speech является общим для всех экземпляров, поэтому изменения, внесенные в него в одном экземпляре, будут отображаться в другом.
Правильный способ избежать такого поведения - создать изменяемое состояние объекта в конструкторе:
class Animal {
new: (speech) => {
self.speech = {} -- private property for instance
table.insert self.speech, speech
}
speak: (who) => {
print "#{who} say: #{speech}" for _, speech in ipairs self.speech
}
}Ключевое слово extends можно использовать в объявлении класса для наследования свойств и методов другого класса.
class Dog extends Animal {
new: (speech) => {
super(speech)
}
speak: (who)=> {
print("#{who} say: WOOF")
}
}Если в подклассе не определен конструктор, то при создании нового экземпляра вызывается конструктор родительского класса.
Если же конструктор определен, то для вызова конструктора родительского класса можно использовать метод super.
super - это специальное ключевое слово, которое можно использовать двумя способами: как объект или как функцию. super обладает функциональностью только внутри класса.
При вызове в качестве функции super вызовет функцию с тем же именем в родительском классе. В качестве первого аргумента автоматически будет передан текущий объект self.
При использовании super в качестве обычного значения, это ссылка на объект родительского класса.
К super можно обращаться как к любому объекту для получения значений в родительском классе.
При наследовании классом наследника, он отправляет сообщение родительскому классу, вызывая метод __inherited родительского класса, если он существует. Метод принимает два аргумента: наследуемый класс и дочерний класс:
class Animal {
__inherited: (child) => {
print "#{self.__name} was inherited by #{child.__name}"
}
}
class Dog extends Animal{}Концепция ООП в MoonScript не допускает множественное наследование, в LatteLua этот механизм поддерживается через «встраивание».
Каждый класс имеет private-метод implement, который наследует поля и методы встраиваемого класса и использует их так, как если бы они были его собственными.
class Animal {
setname: (n)=>{
@name = n
}
getname: =>{
@name
}
speak: =>
print "#{name} makes sound"
}
}
class Alias {
setalias: (alias) => {
@aliases = alias
}
getalias: =>{
@aliases
}
}
class Dog {
speak: =>{
print "#{@getname!}, niknamed '#{@getalias!}', says: Woof!"
}
implement self, Animal, Alias
}
class Cat {
speak: =>{
print "#{@getname!}, niknamed '#{@getalias!}', says: Meow!"
}
implement self, Animal, Alias
}
c, d = Cat!, Dog!
c::setname "Misty"
c::setalias "Shadow"
d::setname "Rex"
d::setalias "Good Boy"
c::speak! --Misty, niknamed 'Shadow', says: Meow!
d::speak! --Rex, niknamed 'Good Boy', says: Woof!Важно: «встраивание» не является наследованием — это форма композиции, при встраивании не работают механизмы extends и не создаётся новая область видимости, но замыкания всё равно общие.
Блок with позволяет сократить код при множественных обращениях к одному объекту. Внутри блока, свойства начинающиеся с . или методы с ::, относятся к целевому объекту.
user = { name: "John", age: 30 }
user.show = => { print self.name }
with user {
.name ..= " Doe" -- user.name = "John Doe"
::show() -- user:show()
print(.age) -- print(user.age)
}Оператор with также можно использовать как выражение, возвращающее значение, к которому он предоставил доступ:
name = with user {
.name = 'Jane Smith'
}
name::show() -- Jane SmithВыражение в операторе with также может быть присвоением, если требуется дать выражению имя:
name = with n = setmetatable{name: user.name},{__index: user} {
.name = 'John Doe'
}
name::show() -- John Doe
user::show() -- Jane SmithИспользование оператора do работает так же, как и в Lua.
do {
msg = "world"
print "hello #{msg}"
}Оператор do также может использоваться как выражение. Результатом выражения do является последнее выражение в блоке.
print do {
msg = "world"
"hello #{msg}"
}Оператор do возможно использовать для расширения синтаксических конструкций:
do {
print "#{k} => #{v}" if k != "lattelua" else do {
print "\t#{kk} => #{vv}" for kk,vv in pairs lattelua
}
} for k,v in pairs _G
-- for k, v in pairs(_G) do
-- do
-- if k ~= "lattelua" then
-- print(tostring(k) .. " => " .. tostring(v))
-- else
-- do
-- for kk, vv in pairs(lattelua) do
-- print("\t" .. tostring(kk) .. " => " .. tostring(vv))
-- end
-- end
-- end
-- end
-- end
Блок try используется для обработки исключений. Это позволяет тестировать блок кода на наличие ошибок и корректно обрабатывать их, предотвращая неожиданный сбой программы.
try {
-- Код, который вызывает ошибку
error("Boom\!")
} catch {
-- Обработка ошибки (self содержит текст ошибки)
print("Error caught: " .. self)
} finally {
-- Выполняется всегда, если присутствует
print("Cleanup")
}Оператор try также может использоваться как выражение. Результатом выражения try является последнее выражение в блоках try/catch соответственно.
Вместе с LatteLua устанавливается автономный интерпретатор llua. Интерпретатор предназначен как для непосредственного запуска кода LatteLua, так и для компиляции кода в Lua.
llua [options] [script [args]]Опции запуска:
-e code- выполняет строкуcode-l name- подгружает библиотекуname-i- интерактивный режим после выполненияscript-c- скомпилировать и напечатать результат кодlua--- прекратить обработку опций-- выполнитьstdinи прекратить обработку опций