7

Right now this is just a crazy idea that I had, but I was able to implement the code and get it working properly. I am not entirely sure of what the use cases would be just yet.

What this code does is create a new Lua script file in the project directory. The ScriptWriter takes as arguments the file name, a table containing any arguments that the script should take when created, and a table containing any instance variables to create by default. My plan is to extend this code to create new functions based on inputs sent in during its creation as well.

What makes this cool is that the new file is both generated and loaded dynamically on the fly. Theoretically you could get this code to generate and load any script imaginable. One use case I can think of is an AI that creates scripts to map out it's functions, and creates new scripts for new situations or environments. At this point, this is all theoretical, though.

Here is the test code that is creating the new script and then immediately loading it and calling functions from it:

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

Here is 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

And here is what the code provided will generate:

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

All of this is generated with these calls:

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

I am not sure if I am correct that this could be useful in certain situations. What I am looking for is feedback on the readability of the code, and following Lua best practices. I would also love to hear whether this approach is a valid one, and whether the way that I have done things will be extensible.

1
  • That's pretty damn cool if you ask me.
    – Phrancis
    Aug 23, 2014 at 1:42

1 Answer 1

8

Why you don't need to generate code

What you are doing here is interesting as a learning exercise. Certainly there are worse ways to learn a language than by generating code in the language, with the language. Outside of that, there may not be many practical applications for generated code in a highly flexible, dynamic language like Lua. Anything you can do with generated code, you can probably do without generating code, in a cleaner and more straightforward way.

When you think of languages where generated code is common, what languages do you think of? Probably classical, strongly-typed languages like Java, C#, and so on. Classes may be generated from a UML model, from a WSDL, or from a wizard in an IDE. It can be useful to generate code for these languages; for example in the case of a WSDL, because you want to have classes that provide the described API, and you want them to just work... you don't want to do the lower-level work of dealing with HTTP traffic and so on, you just want to be able to call the methods provided by the service in a straightforward way.

In languages like that, a lot of work happens at build time. It would be inconvenient or impossible to dynamically build these classes at run time. This is why code generation is necessary. In dynamic languages like Lua, it's a different ballgame. For example, you could easily have a Lua script consume a WSDL and create a bunch of class-like-things on the fly.

A good real-world example of this is the clients that have been written for services implementing the Swagger protocol (essentially, Swagger brings to REST what WSDL is to SOAP). If you look at the canonical Java client, you will see that it generates code from the resource description, to be compiled into your project. If you look at the JavaScript client, you will see that it dynamically generates objects and methods to interface with the service on the fly, because it can. There is no need for generating code (in this scenario, at least) in a dynamic language like JavaScript. The same thing applies to Lua.

What you can do instead

Let's see how we can achieve your goal without generating any code. Breaking the problem down, we need to take some inputs -- the name of a "class," the name of some parameters for the "constructor," and some property names, with initial values, and a getter and setter for each.

Now, let's plan to write a function that can do this. The function will return a value representing the "class." We will store that value in a named variable, so that can be the name of the class; there is no need to pass the name in. So we are left with "constructor" parameters, properties, and getters/setters.

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

Now you can simply do this to get the ScriptIAmMaking from your example, on the fly, without needing to generate any code:

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

Now, generating class-like-things in such a manner may or may not be useful, but the point is that it can be done in a simpler, more concise way, and without generating any code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.