Creazione di nuovi script dinamicamente in Lua

7

In questo momento è solo un'idea folle che ho avuto, ma sono stato in grado di implementare il codice e farlo funzionare correttamente. Non sono del tutto sicuro di quali potrebbero essere i casi d'uso.

Ciò che questo codice fa è creare un nuovo file di script Lua nella directory del progetto. ScriptWriter accetta come argomenti il nome del file, una tabella contenente tutti gli argomenti che lo script deve eseguire al momento della creazione e una tabella contenente le variabili di istanza da creare per impostazione predefinita. Il mio piano è estendere questo codice per creare nuove funzioni basate sugli input inviati durante la sua creazione.

Ciò che rende questo interessante è che il nuovo file è sia generato che caricato dinamicamente al volo. In teoria è possibile ottenere questo codice per generare e caricare qualsiasi script immaginabile. Un caso d'uso che posso pensare è un'intelligenza artificiale che crea script per mappare le sue funzioni e crea nuovi script per nuove situazioni o ambienti. A questo punto, questo è tutto teorico, però.

Ecco il codice di test che sta creando il nuovo script e quindi lo carica immediatamente e chiama da esso le funzioni:

function Card:doScriptWriterThing()
    local scriptName = "ScriptIAmMaking"

    local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
    scripter:makeFileForLoadedSettings()

    local loadedScript = require (scriptName)
    local scriptInstance = loadedScript:new("sayThis")
    print(scriptInstance:get_name()) --will print test
    print(scriptInstance:get_one()) -- will print 1
    scriptInstance:set_one(10000)
    print(scriptInstance:get_one()) -- will print 10000
    print(scriptInstance:get_argumentName()) -- will print sayThis
    scriptInstance:set_argumentName("saySomethingElse")
    print(scriptInstance:get_argumentName()) --will print saySomethingElse
end

Ecco ScriptWriter.lua

local ScriptWriter = {}

local twoSpaceIndent = "  "
local equalsWithSpaces = " = "
local newLine = "\n"

--scriptNameToCreate must be a string
--argumentsForNew and instanceVariablesToCreate must be tables and not nil
function ScriptWriter:new(scriptNameToCreate, argumentsForNew, instanceVariablesToCreate)

    local instance = setmetatable({}, { __index = self })

    instance.name = scriptNameToCreate
    instance.newArguments = argumentsForNew
    instance.instanceVariables = instanceVariablesToCreate
    instance.stringList = {}

    return instance
end
function ScriptWriter:makeFileForLoadedSettings()
    self:buildInstanceMetatable()
    self:buildInstanceCreationMethod()
    self:buildSettersAndGetters()
    self:buildReturn()
    self:writeStringsToFile()
end

--very first line of any script that will have instances
function ScriptWriter:buildInstanceMetatable()
    table.insert(self.stringList, "local " .. self.name .. " = {}" .. newLine)
    table.insert(self.stringList, newLine)
end

--every script made this way needs a new method to create its instances
function ScriptWriter:buildInstanceCreationMethod()
    --new() function declaration
    table.insert(self.stringList, ("function " .. self.name .. ":new("))
    self:buildNewArguments()
    table.insert(self.stringList, ")" .. newLine)

    --first line inside :new() function
    table.insert(self.stringList, twoSpaceIndent .. "local instance = setmetatable({}, { __index = self })" .. newLine)

    --add designated arguments inside :new()
    self:buildNewArgumentVariables()

    --create the instance variables with the loaded values
    for key,value in pairs(self.instanceVariables) do
      table.insert(self.stringList, twoSpaceIndent .. "instance." .. key .. equalsWithSpaces .. value .. newLine)
    end

    --close the :new() function
    table.insert(self.stringList, twoSpaceIndent .. "return instance" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
    table.insert(self.stringList, newLine)
end
function ScriptWriter:buildNewArguments()
     --if there are arguments for :new(), add them
    for key,value in ipairs(self.newArguments) do
      table.insert(self.stringList, value)
      table.insert(self.stringList, ", ") 
    end
    if next(self.newArguments) ~= nil then --makes sure the table is not empty first
      table.remove(self.stringList) --remove the very last element, which will be the extra ", "
    end
end
function ScriptWriter:buildNewArgumentVariables()
    --add the designated arguments to :new()
    for key, value in ipairs(self.newArguments) do
      table.insert(self.stringList, twoSpaceIndent .. "instance." .. value .. equalsWithSpaces .. value .. newLine)
    end
end

--the instance variables need separate code because their names have to be the key and not the argument name
function ScriptWriter:buildSettersAndGetters()
    for key,value in ipairs(self.newArguments) do
        self:buildArgumentSetter(value)
        self:buildArgumentGetter(value)
        table.insert(self.stringList, newLine)
    end
    for key,value in pairs(self.instanceVariables) do
        self:buildInstanceVariableSetter(key, value)
        self:buildInstanceVariableGetter(key, value)
        table.insert(self.stringList, newLine)
    end
end
--code for arguments passed in
function ScriptWriter:buildArgumentSetter(variable)
    table.insert(self.stringList, "function " .. self.name .. ":set_" .. variable .. "(newValue)" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "self." .. variable .. equalsWithSpaces ..  "newValue" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildArgumentGetter(variable)
    table.insert(self.stringList, "function " .. self.name .. ":get_" .. variable .. "()" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. variable .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
--code for instance variable values passed in
function ScriptWriter:buildInstanceVariableSetter(key, variable)
    table.insert(self.stringList, "function " .. self.name .. ":set_" .. key .. "(newValue)" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "self." .. key .. equalsWithSpaces .. "newValue" .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end
function ScriptWriter:buildInstanceVariableGetter(key, variable)
    table.insert(self.stringList, "function " .. self.name .. ":get_" .. key .. "()" .. newLine)
    table.insert(self.stringList, twoSpaceIndent .. "return " .. "self." .. key .. newLine)
    table.insert(self.stringList, "end" .. newLine)
end

--last line of any script that will have instances
function ScriptWriter:buildReturn()
    table.insert(self.stringList, "return " .. self.name)
end

function ScriptWriter:writeStringsToFile()
    local fileName = (self.name .. ".lua")
    file = io.open(fileName, 'w')
    for key,value in ipairs(self.stringList) do
      file:write(value)
    end
    file:close()
end

return ScriptWriter

Ed ecco ciò che il codice fornito genererà:

local ScriptIAmMaking = {}

function ScriptIAmMaking:new(argumentName)
    local instance = setmetatable({}, { __index = self })
    instance.argumentName = argumentName
    instance.name = 'test'
    instance.one = 1
    return instance
end

function ScriptIAmMaking:set_argumentName(newValue)
    self.argumentName = newValue
end
function ScriptIAmMaking:get_argumentName()
    return self.argumentName
end

function ScriptIAmMaking:set_name(newValue)
    self.name = newValue
end
function ScriptIAmMaking:get_name()
    return self.name
end

function ScriptIAmMaking:set_one(newValue)
    self.one = newValue
end
function ScriptIAmMaking:get_one()
    return self.one
end

return ScriptIAmMaking

Tutto questo viene generato con queste chiamate:

local scripter = scriptWriter:new(scriptName, {"argumentName"}, {name = "'test'", one = 1})
scripter:makeFileForLoadedSettings()

Non sono sicuro che sia corretto che ciò possa essere utile in determinate situazioni. Quello che cerco è un feedback sulla leggibilità del codice e sulle best practice Lua. Mi piacerebbe anche sapere se questo approccio è valido e se il modo in cui ho fatto le cose sarà estensibile.

    
posta bazola 23.08.2014 - 01:08
fonte

1 risposta

8

Perché non è necessario generare codice

Quello che stai facendo qui è interessante come esercizio di apprendimento. Certamente ci sono modi peggiori per imparare una lingua piuttosto che generare codice nella lingua, con la lingua. Al di fuori di questo, potrebbero non esserci molte applicazioni pratiche per codice generato in un linguaggio dinamico e flessibile come Lua. Qualunque cosa tu possa fare con il codice generato, puoi probabilmente fare a meno di generare codice, in un modo più pulito e diretto.

Quando pensi alle lingue in cui il codice generato è comune, a quali lingue pensi? Linguaggi probabilmente classici, strongmente tipizzati come Java, C # e così via. Le classi possono essere generate da un modello UML, da un WSDL o da una procedura guidata in un IDE. Può essere utile generare codice per queste lingue; per esempio nel caso di un WSDL, perché vuoi avere classi che forniscono l'API descritta e vuoi che lavori solo ... non vuoi fare il lavoro di livello inferiore di gestire il traffico HTTP e così via, vuoi solo essere in grado di chiamare i metodi forniti dal servizio in modo semplice.

In lingue come questa, un sacco di lavoro avviene al momento della compilazione. Sarebbe scomodo o impossibile creare dinamicamente queste classi in fase di esecuzione. Questo è il motivo per cui la generazione del codice è necessaria. In lingue dinamiche come Lua, è un gioco diverso. Ad esempio, potresti facilmente avere uno script Lua che utilizza un WSDL e creare un gruppo di cose simili alla classe al volo .

Un buon esempio del mondo reale di ciò sono i client che sono stati scritti per servizi che implementano il protocollo Swagger (in sostanza, Swagger porta a REST ciò che WSDL sta per SAPERE). Se guardi il client Java canonico, vedrai che genera codice dalla descrizione della risorsa, da compilare nel tuo progetto. Se guardi il client JavaScript, vedrai che genera dinamicamente oggetti e metodi per interfacciarsi con il servizio al volo, perché può farlo. Non è necessario generare codice (in questo scenario, almeno) in un linguaggio dinamico come JavaScript. La stessa cosa vale per Lua.

Cosa puoi fare invece

Vediamo come possiamo raggiungere il tuo obiettivo senza generare alcun codice. Rompendo il problema, dobbiamo prendere alcuni input: il nome di una "classe", il nome di alcuni parametri per il "costruttore" e alcuni nomi di proprietà, con valori iniziali e un getter e setter per ciascuno.

Ora, pianifichiamo di scrivere una funzione che possa farlo. La funzione restituirà un valore che rappresenta la "classe". Memoreremo quel valore in una variabile denominata, in modo che possa essere il nome della classe; non è necessario passare il nome. Quindi restano parametri, proprietà e getter / setter "costruttore".

local function createClass(parameters, properties)
  local class = {}

  function class:new(...)
    local instance = setmetatable({}, { __index = self })
    local arguments = {...}

    for i, param in ipairs(parameters) do
        instance[param] = arguments[i]
    end

    return instance
  end

  for name, value in pairs(properties) do
      class[name] = value
      class['set_' .. name] = function(self, value)
        self[name] = value
      end
      class['get_' .. name] = function(self)
        return self[name]
      end
  end

  return class
end

Ora puoi semplicemente farlo per ottenere ScriptIAmMaking dal tuo esempio, al volo, senza dover generare alcun codice:

ScriptIAmMaking = createClass({"argumentName"}, {name = "'test'", one = 1})

Ora, generare cose simili alla classe in questo modo può o non può essere utile, ma il punto è che può essere fatto in un modo più semplice, più conciso e senza generare alcun codice.

    
risposta data 23.08.2014 - 06:50
fonte

Leggi altre domande sui tag