Exeteres 24 Опубликовано: 5 января, 2020 (изменено) Вместо вступления: Я не считаю C-подобный синтаксис лучше синтаксиса lua и не буду заставлять вас переписывать все ваши программы на TypeScript! Я просто хочу поделится с вами альтернативой и рассказать про ее преимущества и недостатки. # Что такое TypeScript? TypeScript — язык программирования, представленный Microsoft в 2012 году и позиционируемый как средство разработки веб-приложений. Он создан для расширения JavaScript и он компилируется в JavaScript, но также существует инструмент для преобразования TypeScript кода в Lua. Вам может показаться, что этот транслятор крайне ограничен, но, поверьте мне, его возможности впечатляют. # Почему его стоит попробовать? Я сначала продемонстрирую некоторые возможности TypeScript графически, а потом подробно расскажу про установку и настройку необходимых инструментов. Я покажу вам далеко не все возможности TypeScript, а только самые основные и интересные. Из-за большого размера контент каждого раздела будет скрыт под спойлер. 1. Статический анализ Скрытый текст Системы типов JavaScript и Lua во многом похожи, и потому они имеют некоторые общие проблемы, которые может решить TypeScript. 1.1 Проверка типов Если функция или метод явно требует среди аргументов число, TypeScript не допустит что-то отличное от числа. Lua тоже сообщит об этом, но только после запуска скрипта. 1.2 Проверка существования полей и методов Если вы попытаетесь обратится к несуществующему полю или методу, TypeScript сообщит вам об этом и даже поможет исправить, если вы, например, опечатались. В Lua мы узнаем об этом только после запуска скрипта. 2. Автодополнение Скрытый текст Благодаря статической типизации, редактор кода может подсказывать методы и поля объекта. Или показать параметры, которые принимает метод. Или текст документации. 3. ООП Скрытый текст 3.1 Классы class Player { username: string; constructor(username: string) { this.username = username; } greet() { print("Привет, " + this.username + "!"); } } let alex = new Player("alex"); alex.greet(); // Привет, Alex! Сгенерированный lua код Скрытый текст --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] Player = {} Player.name = "Player" Player.__index = Player Player.prototype = {} Player.prototype.__index = Player.prototype Player.prototype.constructor = Player function Player.new(...) local self = setmetatable({}, Player.prototype) self:____constructor(...) return self end function Player.prototype.____constructor(self, username) self.username = username end function Player.prototype.greet(self) print("Привет, " .. tostring(self.username) .. "!") end local alex = Player.new("alex") alex:greet() 3.2 Модификаторы доступа 3.3 Наследование class Player { username: string; constructor(username: string) { this.username = username; } } class Alex extends Player { constructor() { super("Alex"); } } let alex = new Alex(); print(alex.username); // Alex Сгенерированный lua код Скрытый текст --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] Player = {} Player.name = "Player" Player.__index = Player Player.prototype = {} Player.prototype.__index = Player.prototype Player.prototype.constructor = Player function Player.new(...) local self = setmetatable({}, Player.prototype) self:____constructor(...) return self end function Player.prototype.____constructor(self, username) self.username = username end Alex = {} Alex.name = "Alex" Alex.__index = Alex Alex.prototype = {} Alex.prototype.__index = Alex.prototype Alex.prototype.constructor = Alex Alex.____super = Player setmetatable(Alex, Alex.____super) setmetatable(Alex.prototype, Alex.____super.prototype) function Alex.new(...) local self = setmetatable({}, Alex.prototype) self:____constructor(...) return self end function Alex.prototype.____constructor(self) Player.prototype.____constructor(self, "Alex") end local alex = Alex.new() print(alex.username) 4. Стандартная библиотека и возможности языка Скрытый текст Многие методы стандартных типов TypeScript (такие как массивы и строки) также могут быть транслированы в lua. 4.1 Получение всех четных чисел массива, возведение в квадрат и соединение в строку через запятую const items = [1, 2, 3, 4, 5]; items.push(6); const result = items .filter(x => x % 2 == 0) .map(x => x ** 2) .join(", "); print(result); // 4, 16, 36 Сгенерированный lua код Скрытый текст --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] -- Lua Library inline imports function __TS__ArrayPush(arr, ...) local items = ({...}) for ____, item in ipairs(items) do arr[#arr + 1] = item end return #arr end function __TS__ArrayFilter(arr, callbackfn) local result = {} do local i = 0 while i < #arr do if callbackfn(_G, arr[i + 1], i, arr) then result[#result + 1] = arr[i + 1] end i = i + 1 end end return result end function __TS__ArrayMap(arr, callbackfn) local newArray = {} do local i = 0 while i < #arr do newArray[i + 1] = callbackfn(_G, arr[i + 1], i, arr) i = i + 1 end end return newArray end local items = { 1, 2, 3, 4, 5, } __TS__ArrayPush(items, 6) local result = table.concat(__TS__ArrayMap(__TS__ArrayFilter(items, function(____, x) return x % 2 == 0 end), function(____, x) return x ^ 2 end), ", ") print(result) 4.2 Модули // main.ts import { myFunction } from "./myLibrary"; myFunction(); // myLibrary.ts export function myFunction() { print("Hi from myLibrary"); } Сгенерированный lua код Скрытый текст -- main.lua --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] local ____exports = {} local ____myLibrary = require("myLibrary") local myFunction = ____myLibrary.myFunction myFunction(nil) return ____exports -- myLibrary.lua --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] local ____exports = {} function ____exports.myFunction(self) print("Hi from myLibrary") end return ____exports 4.3 Форматирование строк const text = `2 + 2 = ${2 + 2}`; print(text); // 2 + 2 = 4 Сгенерированный lua код --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] local text = "2 + 2 = " .. tostring(2 + 2) print(text) # Как это работает? Конечно же все не так просто. Компилятор просто так не узнает типы методов и полей объектов, с которыми мы будем работать. Для того, чтобы описать наше окружение необходимо написать так называемые файлы декларации или тайпинги. Хочу сразу вас обрадовать - это не ваша задача. Существует репозиторий с такими декларациями, в котором, на данный момент, существуют типы для большинства API и компонентов OpenOS и библиотеки GUI. От вас требуется только установить все необходимые инструменты и правильно их настроить. # Установка Редактор кода Вы можете использовать любой редактор кода с поддержкой TypeScript. Я рекомендую VSCode, который поддерживает его из коробки. NodeJS Он необходим нам для установки необходимых пакетов (он поставляется с пакетным менеджером npm) и для запуска транспилера. Вы можете скачать последнюю стабильную версию с официального сайта. Использование плагина для VSCode (рекомендуется): Скрытый текст Вы можете установить плагин OpenComputersTS. В нем есть две команды: OC-TS: Init - Создание нового проекта в пустой папке. OC-TS: Mount - Подключение дисков из сохранений minecraft или эмулятора OCEmu в папку dist. Для открытия окна с командами используйте сочетание Ctrl + Shift + P. Создание проекта вручную: Скрытый текст После установки NodeJS у вас должны появится команды npm и node. Создайте новую папку для своего первого проекта Переключитесь в нее, используя терминал и все дальнешие действия выполняйте в ней Создайте npm пакет: npm init. После выполнения этой команды в папке появится файл package.json Добавьте в объект "scripts" в package.json строку "build": "tstl", Установите транспилер npm install --dev typescript-to-lua. После установки первого пакета у вас появится папка node_modules Установите тайпинги npm install --dev @opct/openos Создайте папку src для исходных файлов Создайте файл tsconfig.json со следующим содержимым: { "compilerOptions": { "target": "esnext", "outDir": "dist", "module": "commonjs", "lib": ["esnext"], "strict": true, "moduleResolution": "node", "rootDir": "src", "types": ["lua-types/jit", "@opct/openos"] }, "tstl": { "luaTarget": "JIT" } } Теперь вы можете размещать в src исходный код, например, main.ts со следующим содержимым: import * as component from "component"; import { front } from "sides"; component.redstone.setOutput(front, 1); Как запустить сгенерированный код в OpenComputers? Самый удобный способ для доставки полученного кода - символическая ссылка. Вы можете заменить каталог dist ссылкой на папку диска. # Создание ссылки через терминал # linux / macos ln -s /path/to/disk/home dist # windows (cmd) mklink /j dist C:\path\to\disk\home Диски располагаются в папке .minecraft\saves\{сохранение}\opencomputers. Вам также необходимо отключить параметр filesystem.bufferChanges в файле .minecraft\config\opencomputers\settings.conf После этого вы сможете запускать сгенерированный код прямо в игре. Для компиляции используйте команду npm run build. Сгенерированные lua файлы появятся в папке dist. # Особенности работы транспилера В этой секции я подробно раскажу про недостатки этого подхода, возможные проблемы и способы их решения. 1. Параметр self Скрытый текст Объявление функций Одно из первых, что вы скорее всего попробуете сделать - объявить функцию: function test() {} test(); Однако результат может быть неожиданным: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] function test(self) end test(nil) Зачем-то появился параметр self, который тут казалось бы не нужен. А теперь попробуйте сделать так: const obj = { a: 5, b: 10, sum() { print(this.a + this.b); }, }; obj.sum(); Результат: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] obj = { a = 5, b = 10, sum = function(self) print(self.a + self.b) end } obj:sum() У функции все еще есть self, но в этот раз он необходим для доступа к полям obj. Если мы теперь попробуем оторвать метод sum от объекта и вызвать отдельно: const sum = obj.sum; sum(); то увидем, что транспилер все еще передает nil вместо self: sum = obj.sum sum(nil) Такое поведение связано с тем, что в JavaScript (для которого изначально был создан TypeScript) this (self) есть у любой функции (кроме стрелочных) и для сохранения совместимости с этой особенностью транспилер автоматически генерирует self параметр для любой функции. Если вы считаете такое поведение по умолчанию недопустимым, то можете отключить его: // tsconfig.json { "compilerOptions": { ... }, "tstl": { ..., "noImplicitSelf": false, ... } } Тип self параметра Вы также можете явно задать тип this так же как и тип любого другого параметра, например, назначить ему void: function test(this: void): void {} test(); И тогда транспилер уберет self параметр: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] function test() end test() Особенно это важно при работе с декларациями. Например, мы хотим описать экземпляр некоторого класса: declare interface Person { say(text: string): void; } declare const person: Person; person.say("hi"); И ожидаемо получим: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] person:say("hi") Но если нам понадобится описать какую-либо библиотеку (таблицу с функциями), то такое поведение может быть недопустимо, ведь эти функции не принимают self параметр. Можно вручную задать каждой функции this:void, а можно использовать директиву @noSelf: /** @noSelf */ declare interface MyLibrary { method1(text: string): void; method2(num: number): void; } declare const lib: MyLibrary; lib.method1("text"); lib.method2(123); И тогда эти функции будут вызваны без двоеточия: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] lib.method1("text") lib.method2(123) 2. Множественные значения Скрытый текст В TypeScript не поддерживаются множественные значения. Вместо этого используются кортежи. Например, объявим функцию, которая возвращает несколько значений: function getValues(): [string, number, boolean] { return ["text", 123, true]; } const [str, num, bool] = getValues(); Этот подход сильно напоминает множественные возвращаемые значения и множественное присваивание в Lua. Тем не менее, сейчас кортежи просто превращаются в таблицы: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] function getValues(self) return {"text", 123, true} end str, num, bool = unpack( getValues(nil) ) Чтобы заставить транспилер возвращать множественные значения, необходимо использовать директиву @tupleReturn: /** @tupleReturn */ function getValues(): [string, number, boolean] { return ["text", 123, true]; } const [str, num, bool] = getValues(); И тогда все будет красиво: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] function getValues(self) return "text", 123, true end str, num, bool = getValues(nil) 3. Индексы Скрытый текст Числовые индексы в таблицах Lua начинаются с 1, в то время как в TypeScript с 0. Транспилер сам выполняет преобразование индексов TypeScript в Lua, например: const arr = [1, 2, 3]; print(arr[1]); будет преобразовано в: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] arr = {1, 2, 3} print(arr[2]) И мы в обоих случаях получим на экране второй элемент массива. arr.length является полным аналогом оператора #: const arr = [1, 2, 3]; print(arr.length); Результат: --[[ Generated with https://github.com/TypeScriptToLua/TypeScriptToLua ]] arr = {1, 2, 3} print(#arr) # Ссылки Официальный сайт и документация TypeScript (англ) Серия русских статей по TypeScript Документация TypeScriptToLua (англ) Тайпинги Изменено 23 апреля, 2020 пользователем Exeteres Раздел "Особенности работы транспилера" 6 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 5 января, 2020 1 час назад, Morkoffka сказал: После установки NodeJS у вас должны появится команды npm и node. Создайте новую папку для своего первого проекта Переключитесь в нее, используя терминал и все дальнешие действия выполняйте в ней Создайте npm пакет: npm init. После выполнения этой команды в папке появится файл package.json Добавьте в объект "scripts" в package.json строку "build": "tstl", Установите транспилер npm install --dev typescript-to-lua. После установки первого пакета у вас появится папка node_modules Установите тайпинги npm install --dev @opct/openos Создайте папку src для исходных файлов Создайте файл tsconfig.json со следующим содержимым: А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки? Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Fingercomp 4 409 Опубликовано: 5 января, 2020 На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу. Выхлопной код получается страшный. Что не сильно способствует дебагу. А ещё он неоптимален. В том числе по размеру получающегося скрипта, и минификатор не сильно помогает. Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua. 1 2 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
NEO 542 Опубликовано: 5 января, 2020 1 час назад, Fingercomp сказал: На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу. Выхлопной код получается страшный. Что не сильно способствует дебагу. А ещё он неоптимален. В том числе по размеру получающегося скрипта, и минификатор не сильно помогает. Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua. Тут оптимизатор нужен. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 (изменено) В 06.01.2020 в 00:17, hohserg сказал: А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки? Хорошая идея, я об этом даже не подумал. Можно также добавить в этот плагин автоматический поиск дисков (папки .minecraft\saves\xxx\opencomputers) и их подключение. Я займусь его написанием, не думаю что это слишком сложно. В 06.01.2020 в 01:11, Fingercomp сказал: На тайпскрипте я, конечно, не писал, но пробовал MoonScript. Это такой язык, который транспилируется в Lua. У него тоже есть классы, сахара всякие. Но я на нём больше писать не хочу. Выхлопной код получается страшный. Что не сильно способствует дебагу. А ещё он неоптимален. В том числе по размеру получающегося скрипта, и минификатор не сильно помогает. Здесь, видимо, всё то же. Так что для опенкомпов будет проще всё же писать на Lua. Я видел MoonScript в соседней теме. Он конечно хорош, учитывая что был создан специально для lua, но разве в нем есть статический анализ? Это хоть и не спасает от ошибок в рантайме, но позволяет уберечь программиста от отладки очень глупых ошибок и опечаток и обнаружить их еще до компиляции (или во время нее). Что касается отладки - действительно, код не сильно читаем. Я пока не знаю, насколько это возможно, но хотелось бы иметь отладчик. Технически, транспилер поддерживает карты кода, что позволяет сопоставить исходный typescript и сгенерированный lua код. Я пока не нашел полноценного lua отладчика для opencomputers, но думаю что он есть. В крайнем случае можно использовать библиотеку debug. Осталось лишь найти транспорт между игрой и VSCode, чтобы доставлять отладочные данные и можно будет написать отладчик как плагин для VSCode. Изменено 8 февраля, 2020 пользователем Exeteres 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 6 января, 2020 6 часов назад, Morkoffka сказал: Осталось лишь найти транспорт между игрой и VSCode, чтобы доставлять отладочные данные и можно будет написать отладчик как плагин для VSCode А какие отладочные данные требуются? Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 (изменено) В 06.01.2020 в 16:04, hohserg сказал: А какие отладочные данные требуются? Самые простые: уведомление VSCode об ошибке или брейкпоинте и передача строки, в которой произошла остановка. Если произошла ошибка, можно показать ее текст в редакторе. Сложнее: передача состояния переменных из области видимости того места, где произошла остановка. Это уже более полезная информация, которая позволит не использовать print для отладки. Добавить стек вызовов и это уже можно будет назвать полноценным отладчиком. Изменено 8 февраля, 2020 пользователем Exeteres Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 (изменено) В 06.01.2020 в 00:17, hohserg сказал: А есть плагин для VSCode, чтобы сетапать воркспейс нажатием одной кнопки? Небольшая демонстрация. Скрытый текст Надо исправить некоторые недочеты и добавить поддержку Linux. Потом буду разбираться как загрузить его в каталог расширений и обновлю гайд. Изменено 8 февраля, 2020 пользователем Exeteres 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 6 января, 2020 4 часа назад, Morkoffka сказал: Добавить стек вызовов Стэк вызовов уже есть, его можно получать так: ```ok,err = xpcall(code, debug.traceback, args...)```. Однако, это будет стэк вызовов Lua-кода и нужно как-то получить соответствие TypeScript-коду 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 3 минуты назад, hohserg сказал: нужно как-то получить соответствие TypeScript-коду Да, все так. Я надеюсь, что с этим разберутся карты кода. Кстати, есть идея касательно транспорта. Можно использовать интернет карту. Она же может совершать запросы к localhostу? 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 6 января, 2020 Может конечно. В этом случае плагин ide должен разместить локально свой сервис, к которому программа в игре будет обращаться в случае ошибки Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 1 минуту назад, hohserg сказал: Может конечно. В этом случае плагин ide должен разместить локально свой сервис, к которому программа в игре будет обращаться в случае ошибки Да, так и планировалось. При запуске отладчика плагин будет встраивать в запускаемый файл кусок кода, который устанавливает обработчики ошибок и подключается к серверу. Я правда не знаю какой протокол для этого использовать: можно http, а можно tcp или вебсокеты (если в opencomputers они есть). Тогда можно будет отправлять данные в оба направления, например, чтобы уведомить программу продолжить работу после остановки на брейкпоинте или перезапустится. Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 6 января, 2020 Интернет-карта поддерживает все это, в зависимости от конфига. https://ocdoc.cil.li/component:internet 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 6 января, 2020 (изменено) @hohserg Я обновил гайд и залил плагин в каталог. Изменено 8 февраля, 2020 пользователем Exeteres 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 6 января, 2020 (изменено) Попробовал. OC-TS: Mount че-то не работает - ввожу команду и ничего не происходит. Как выбрать назначение ссылки для dist? ~~~ Может, я че-то не так делаю? Раньше не юзал VSCode Изменено 6 января, 2020 пользователем hohserg 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 7 января, 2020 (изменено) В 07.01.2020 в 02:15, hohserg сказал: Попробовал. OC-TS: Mount че-то не работает - ввожу команду и ничего не происходит. Как выбрать назначение ссылки для dist? ~~~ Может, я че-то не так делаю? Раньше не юзал VSCode Я нашел ошибку (и даже не одну). Я использовал неправильный оператор (return вместо continue), и поэтому он выходит из команды, если хотя бы одно сохранение не содержит папку opencomputers. У меня было только одно сохранение, поэтому я сразу не нашел этот баг. Я исправил его и опубликовал новую версию. Изменено 8 февраля, 2020 пользователем Exeteres Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 7 января, 2020 Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\ Можно вынести это в конфигурацию куда-нить? Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 7 января, 2020 1 минуту назад, hohserg сказал: Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\ Можно вынести это в конфигурацию куда-нить? Да, можно сделать опцию. Так и сделаю. 1 Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
Exeteres Автор темы 24 Опубликовано: 7 января, 2020 (изменено) В 07.01.2020 в 17:12, hohserg сказал: Сохранения у мя лежат в %AppData%\opencomputers\saves\, а не в %AppData%\.minecraft\saves\ Можно вынести это в конфигурацию куда-нить? Добавил. Открыть настройки можно сочетанием Ctrl + , Относительные пути будут разрешены относительно домашней папки (в винде C:\Users\username). Абсолютные модифицированы не будут. Изменено 8 февраля, 2020 пользователем Exeteres Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах
hohserg 197 Опубликовано: 7 января, 2020 Запускаю таску watch, транслированный файл появляется(чекнул через проводник), но в VSCode не отображается, таска watch не завершается Цитата Поделиться сообщением Ссылка на сообщение Поделиться на других сайтах