This is a read-only snapshot of the ComputerCraft forums, taken in April 2020.
Antelux's profile picture

States 1.2

Started by Antelux, 12 November 2015 - 02:51 AM
Antelux #1
Posted 12 November 2015 - 03:51 AM
So, while taking a break on MiniatureCraft, I became a bit interesting in multithreading with Lua.
I looked around, and after seeing this, I was inspired by it to make something of my own.

As such, I've created the States API, which can be found here (b4q44wDK).
They're used in a way similar to coroutines. Except, there are a few notable differences:
  • You can easily access the state's global environment.
  • Ability to loop code over and over (Though, I guess you can do the same by adding while true do end to a coroutine)
  • Ability to re-use previously created states.
  • Ability to modify code in the function itself.
  • Faster than default coroutines (How much faster? I'm not sure, I haven't done extensive testing).
However, the following drawbacks should be considered:
  • Much slower creation time when compared to Coroutines, mostly noticeable when making a lot of states (applies only when using strings, functions aren't as slow).
  • Cannot have coroutine.yield() or any function that uses it in the code, meaning, states cannot stop and resume when desired.
  • Code doesn't look as pretty when using an outside editor that recognizes the Lua syntax (applies only when you're giving it a string for the code).
However, as of version 1.2, it is possible to create states that use coroutines, called cstates. Here's how they differ from normal states:
  • Given code may use coroutine.yield(), meaning you can use cstates to create a multitasking environment.
  • cstates are slower than regular states because they use coroutines. Though, coroutines are already pretty fast, so it shouldn't really be too worrying anyway.
Those points are great and all, but why should I use this?
Potentially, you can use states or cstates for:
  • Creating sandboxed environments, which is good for entity systems, loading files which should have access to only certain APIs, GUIs, etc.
  • Running multiple programs cooperatively, making it easy to create a multitasking program/os, not to mention you can easily access their environment.
  • Self evolving code, which may need to modify itself in order to evolve (though, not many people would probably use it for this).
If you still want to use the API, here's the documentation:

State.new(string or function code, table env, bool useCoroutine, string debugName, string copy)
This function will return the state object for you to use.
If the state happens to error, it will instead return nil, errorMessage.

code: If given, it must be either a string with valid lua code within it, or a standard function. Strings shouldn't really be used for this argument unless you're loading a file.
env: If given, it must be a table.This table contains all the values of the global environment the function shall have. If not given, it will automatically use _G. Alternatively, you can give it another state as a parameter, and it will use the given state's environment.
useCoroutine: If given, a cstate will be returned instead of a regular one. Look above for the differences between a cstate and a regular state.
debugName: If given, it will use this for erroring. Useful for when you have multiple states and you need to be able to distinguish the error messages without having to go through the code of the states. Without it, it will automatically assume the name of "state" and the number of states currently created.
copy: Used for the environment table. If given "copy," it will still have all the values of the table given. However, any changes to it's table will not modify the table given (i.e., making x = 10 will not make the given table's x = 10, preventing them from sharing a table.) Though, if given "meta," it will instead make the table shareable in terms of reading it, but not in writing (metatable behavior).

State.version()
This function returns a string of the current version of State.

When you get a state object from State.new(), here are the functions you can call from it:

state.setCode(string or function code)
Allows you to re-use the state, and keep the same environment it had before. Returns nil, errorMessage if the given code has an error.

state.run(args )
This function runs the state. If successful, it will return true, and any values the state returns after it.
If the state errors, it will return nil, errorMessage.

Here are some examples for reference:

local state1, err = State.new(function(self)
	  print("About to modify myself...")
	  self.setCode("print('Modification successful! Also, x = ' ..x); y = 100")
	  return "success"
end)


if not state1 then print("ERROR: " ..err) end
local ok, msg = state1.run(state1)
print(msg)
state1.env.x = 10
local ok, msg = state1.run()
print(state1.env.y)

When ran, the following code will output:

About to modify myself...
success
Modification successful! Also, x = 10
100

I may have given a bad explanation for some of the code/functions, sorta tired. If so, do ask questions so I can clarify things.
P.S. If you find some of the things states can do to be pointless (Such as the modification of the code from within the code itself), do know that I did a lot of this for fun and for proof of concept.

11/13/15 - Version 1.1 - Made it possible for the function State.new() and the state object function state.setCode() to accept functions instead of strings (not sure why I didn't do this first). It's highly recommended you give it a function when creating a new state with code you've made, and to only give it a string as an argument when loading a file to run.

11/21/15 - Version 1.2 - Made it possible for states to use coroutines, called cstates. As always, you can easily attach and access any environment you give them, and they can be re-used as well.
Also, when giving the function State.new() the copy parameter, it's no longer a bool; It can be the string "copy," which prevents the new state from sharing the given environment altogether, or the string "meta," which makes a metatable out of the given environment (The table is shared in terms of reading, but not writing). The code may also be a bit messy.

License - The API is under the Creative Commons Attribution-NonCommercial 4.0 International Public License (a summary of the license can be found here).
Edited on 22 November 2015 - 02:44 PM
Rougeminner #2
Posted 12 November 2015 - 12:20 PM
you know, if i sat down and made my life a lot easier by learning co routines the probably wouldn't grab my attention, i will definitely be downloading this in the near future thanks
Antelux #3
Posted 12 November 2015 - 09:20 PM
Glad to hear someone plans to use this.
MKlegoman357 #4
Posted 13 November 2015 - 07:54 AM
I see one good use of this: an alternative to os.loadAPI(). You can load the code by yourself and you can manage the API's holder table (the environment) however you want.
Konlab #5
Posted 13 November 2015 - 01:14 PM
Very interesting. Are states serializable? It would mean that you can hibernate programs. If yes I will make try to make a normal CC/lua code to lua code using state translator
edit:
suggestions:
Append and Restore functions for more control over states' sourcecode.
edit2: what's the license for this?
Edited on 13 November 2015 - 02:15 PM
Antelux #6
Posted 14 November 2015 - 12:43 AM
I guess the state's environment could be serialized to an extent. Though, the state itself can't really be used to do a "hibernate" feature.
States are meant more for like background processes.

I suppose if I were to use coroutines in the states, it would allow the API to be used more for multitasking of programs.
Here's an example :

local programs = {...}
for i = 1, #programs do
	  programs[i] = State.new(_, programs[i])
	  programs[i].env.term = Buffer.new() -- Really easy now to overwrite the states APIs, use buffer to draw to screen.
end

local selectedProgram = 1
while true do
	  local event, p1, p2, p3 = coroutine.yield()
	  if programs[selectedProgram].status() == "dead" then
			table.remove(programs, selectedProgram )
			selectedProgram = selectedProgram == 1 and 1 or selectedProgram - 1
	  end
	  if #programs == 0 then error("All programs have been terminated.") end
	  programs[selectedProgram].resume(event, p1, p2, p3); programs[selectedProgram].env.term.draw()
end

That would be a trivial, unoptimized and simple implementation, so perhaps I'll make a separate version to allow such.
As far as your Append and Restore functions go, can you be a little more specific, and perhaps provide examples?
Also, as of now, there isn't any license for the API. I have thought about putting it under the creative commons 3.0 one, though.
Edited on 13 November 2015 - 11:45 PM
Antelux #7
Posted 14 November 2015 - 03:21 AM
Made a quick update, details in main post.
Konlab #8
Posted 21 November 2015 - 06:08 PM
By append I mean that the original source code is kept only it is appended to, by restore I mean restoring the original source code. But I realized now that they are completly useless because you just save the original string and concat things to it and you keep track of the string so you can append to it when neccessary.
I am speculating, that maybe making programs that have different GUI states (open menu, open popup menu, no menus open) are very good with states (when you open the menu you change the sourcecode to a menu function and when the menu is closed you just restore the original source code (but with changed global vars)) but I don't know that what benefit it has over calling and then returning (except save-ability)
Antelux #9
Posted 21 November 2015 - 10:28 PM
Oh. Well, yeah, as you said, you can keep track of the strings and such.
Also, the GUI use is a good idea. Though, I've been thinking of using this API for entities in MiniatureCraft, makes things easier to do.
I've also been thinking of using it to load mods in a protected environment.

EDIT: Forgot to update the API, kinda had the 1.2 code on my comp. The main post has been changed to reflect the new updates.
Edited on 21 November 2015 - 09:47 PM