Blog @a-ta.co

Using With statements for EVIL

A fun side-effect of Funky's function and with definitions is that you can define functions with the below formats function thing() with math{ print(add(1, 3)) } function expose(obj) with obj{ print(x, y) } with string with table function messy(){ print(rep("Blah", 3)) } thing() expose({x = 3, y = 17}) messy() Although it lies about the actual structure of these objects, I think they look a lot nicer than their counterparts, So I recommend using them if you ever need them.

1514873664923

Streams and With

So my recent work on funky has been surrounding the idea of streams, something that Node.JS handles easily, but Funky did not. To Remedy this, I spent a lot of work adding stream functionality. Now it's possible to pipe things around like it's no-bodies business, for example: io.stdin().pipe(string.reverse).pipe(io.stdout()) This snippet takes STDIN as it's passed to Funky, reverses it, and writes it to the output. As a note, it does only reverse it in the blocks that it receives it, given the nature of the function.
You can also pipe from and to programs thanks to the new os library. For example: os.execute('dir').pipe('grep \\.js$').pipe(io.stdout()) will list all the javascript files in the current working directory (On UNIX).

Also added is the with keyword, which takes an object as an argument, and executes its codeblock using it as the default scope. For example: with({a = 3}){ print(a) } will print 3. You can also temporarily overwrite global vars, similarly to javascript. Eg. with({print=s=>print(`Me: [s]`)}){ print("Hello, World!") } print("Hello again!") prints...

Me: Hello, World!
Hello again!
The best use I can imagine for this is classes, which I leave as an exercise for the reader, as we've done enough classes for now.

1514873171447

Funky has upside down Switches

Yesterday I had the thought to implement Switch-case statements in Funky. A perfectly fine idea for the type of language that it is.
However, due to the Tokenizer that Funky implements, this can't be done the traditional way, not easily at the very least. The result is this... function identify(t){ switch(t) :case 3 print(3) break; :case 4 print("The number:") print(4) :default print("It's "+(type(t)=="number")?"a number":"something I don't understand.") } identify(3) print("===") identify(4) print("===") identify(5) print("===") identify("Demigorgon") This variant works as apposed to the logical case 3: because Funky would successfully tokenize both case and 3, and only fail to match at the :, which would cause the entire switch to fail, rather than it backtrack. Doing it backwards like this however means it fails immediately, and the next case is then tokenized as normal.
I promise it makes more sense when spend some time with the Tokenizer.

1513836746462

Thems the Breaks

I've added break and return to Funky.
Although there are many situations I'm sure these won't behave as users are expecting, they should work in all the normal ways. This will allow for speed ups in quite a few programs, as well as saving some complex thinking in others.
function gcd(A, B){ while(1){ if(B==0){ return A; } var C = B; B = A % B; A = C; } } This is an implementation of Greatest Common Denominator based on Euclid's method that you can play with Here. Without the ability to return, there would be no way to exit this loop, which would mean you'd need to either use a recursive solution, or actually use the conditional.

These breaks and returns, like most things in funky, can be used in expressions. function honk(){ print("honk: "+return "blink") } print(honk()) This code prints the text "honk: blink" followed by the text "blink". Even though the return is called, it must finish evaluating the current expression. The return value is still stored however, which means only "blink" is returned.
break may also be used instead of return, as that's the best default behaviour I could think to give it, so function honk(){ print("honk: "+break "blink") } print(honk()) works identically.
As loops, ifs and trys all have return values, they're passed from the break or the return as well. print("_"+for(var i=1;i<30;i++){ print(':'+if((i%12)==0){ break i/3; }) }) The above code prints

:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:undefined
:4
_4
This demonstrates that if statements will take the value returned from a break, as do for statements. I'm sure someone will find a practical or horrible way to use this.

1513292155931

Making path style classes.

In some Object Oriented Languages, you must define classes as the entire path of the class, Notably Byond. We can achieve a similar effect with some more function scoping. function MakeClassy(func,parent){ if(!func.new){ func.parent = parent; func.new = function(...args){ var instance = (parent?parent.new():{}); instance.class = func; instance._meta = instance; func.scope = {this=instance,_meta = { _newindex = (obj,name,val)=>{instance[name]=val}, _index = (obj,key)=>{ _G[key]||instance[key] } }}; instance._meta._index = (_,key)=>{ ((key::sub(0,4)!="get_")&&instance["get_"+key])?instance["get_"+key]():undefined } instance._meta._newindex = (_,key,val)=>{ if(instance['set_'+key]) instance['set_'+key](val) else table.rawSet(instance,key,val) } func(...args); instance; } } func } function DefineClass(...chain){ MakeClassy(chain[0]) chain::reduce((a b)=>MakeClassy(b a)) } Object = ()=>{ _toString = ()=>{ var c_class = class; var str = `[class]`; while(c_class.parent){ str = `[c_class.parent].[str]`; c_class = c_class.parent; } str; } instanceOf = tar=>{ var cur = class; var found = false; while(cur){ if(cur == tar) found = true; cur = cur.parent; } found; } } Object.stringify = "Object"; functionbody(){ $ ******************************* $ * CLASS STUFF GOES HERE * $ ******************************* class Ent = ()=>{ x = 0; y = 0; } class Ent Rectangle = ()=>{ x = 0; y = 0; w = 0; h = 0; } class Ent Rectangle Square = ()=>{ size = 0; w = undefined; h = undefined; get_w = @size; get_h = @size; set_w = n=>size=n; set_h = n=>size=n; } class Main = ()=>{ function main(){} } $ ******************************* $ * CLASS STUFF STOP HERE * $ ******************************* } body.scope = {} body.scope._meta = {} var classes = {}; var classChain = {}; body.scope._meta._index = (_,key)=>{ if(key == "class"){ classChain = {Object}; }else{ if(classes[key] && #classChain){ classChain::push(classes[key]) classes[key]; }else{ classes[key]; } } } body.scope._meta._newindex = (_,key,val)=>{ if(#classChain){ classChain::push(val); if(type(val)=="function"){ val.stringify = key; DefineClass(...classChain) classes[key] = val; _G[key] = val; } classChain = {} }else{} } body(); Main.new().main(); As can be seen, this is a lot of boilerplate to make this syntax work. Firstly this defines the class system as we did before, and then defines the Object class. This is conveninet with how the chaining will work, and gives us a generic template. This has two methods, the first is a a method that makes sure objects spawned from this are stringifed in the form "Object.Ent.Rectangle.Box", and the second is a method that allows you to determine if this object is an instance of a particular class, or one of it's parents.

To make the chaining work, we define custom indexing behaviour for the body function. When class is indexed, it prepairs a classChain, pushing Object to it to start. When classes are indexed after it, they're appended to the chain. Finally, a class that doesn't exist yet will be indexed, which won't be appened.
Then, when an assignment happens, it defines the class based on this new function with the current chain as the parents, and stores this new class.

This function doesn't have access to the Global Scope, so it can't do anything but define classes, however the classes behave just like before, so they can still access the global scope. Just to harkon back to Java a bit more, we also call Main.main to give an entry point.

1513141920686

Making classes in Funky

Funky, Unlike JS, doesn't have a native method for defining classes. However, you can make 'instance' objects through functions and metatables, similar to Lua. var _obj = {}; _obj._index = _obj; $ Allow instances of _obj to use methods defined in _obj. _obj._toString = s=>`[s.x], [s.y]` $ Just write the coords when converted to a string. function newInstance(){ var obj = {}; obj.x = 0; obj.y = 0; obj._meta = _obj; obj; } And this works more or less fine, but Funky also lets you set the scope of functions, so with this you can get a bit more creative. function instance(){ x = 0; y = 0; _toString = ()=>`[x] [y]` } instance.new = function(){ var inst = {}; inst.self = inst; inst._meta = inst; instance.scope = inst; instance(); inst; } This particular method has the same result, but everything defined in instance will be set to the new instance of the object. This more closely resembles what one might be used to in say, Java.

However, this doesn't allow one to use global functions like print("blah"), as it can only reference the newly created instance. There are two naïve solutions to this: function instance(){ x = 0; y = 0; _toString = ()=>`[x] [y]` } instance.new = function(){ var inst = {}; inst.self = inst; inst._G = _G; $ Allow indexing of the global table. inst._meta = inst; instance.scope = inst; instance(); inst; } and function instance(){ x = 0; y = 0; _toString = ()=>`[x] [y]` } instance.new = function(){ var inst = {}; inst.self = inst; inst._meta = inst; inst._index = _G; $ Fallback to the global table. instance.scope = inst; instance(); inst; } The first of these lets you index _G, which will let you use _G.print("Hello, World!") inside the class function, but that isn't very good looking.
The second lets you use print("Hello, World!"), but as a result all instances let you also index the global table from them, which is just ugly.

My solution is similar to the second naïve solution, however only effects the scope. function instance(){ x = 0; y = 0; _toString = ()=>`[x] [y]` } instance.new = function(){ var inst = {}; inst.self = inst; inst._meta = inst; instance.scope = {_index = (_,key)=>inst[key]==undefined?_G[key]:inst[key],_newindex = (_,key,val)=>inst[key]=val}; instance(); inst; } The difference here is that instance.scope is a table that only references instance and now also _G. Whenever it's indexed, it searches first for inst[key] and if that's indefined, checks the global table. Because this doesn't effect the instance itself, it means you can reference the global scope from the class function and its methods, but doesn't let you do it from instances themselves.

The method I ended up writing personally was this: function MakeClassy(func,parent){ if(!func.new){ func.parent = parent; func.new = function(...args){ var instance = (parent?parent.new():{}); instance.class = func; instance._meta = instance; func.scope = {this=instance,_meta = { _newindex = (obj,name,val)=>{instance[name]=val}, _index = (obj,key)=>{ _G[key]||instance[key] } }}; instance._meta._index = (_,key)=>{ ((key::sub(0,4)!="get_")&&instance["get_"+key])?instance["get_"+key]():undefined } instance._meta._newindex = (_,key,val)=>{ if(instance['set_'+key]) instance['set_'+key](val) else table.rawSet(instance,key,val) } func(...args); instance; } } func } function DefineClass(...chain){ MakeClassy(chain[0]) chain::reduce((a b)=>MakeClassy(b a)) } Some noticeable differences are:

  • Class constructors are dynamic. You can call Guy = function(){}; MakeClassy(Guy) and it will set Guy.new to a function which builds instances.
  • You can have parents. This simply instances the parent, and then uses that for the new instance
  • You can pass arguments. Arguments from the Class.new function are also passed to the constructor.
  • You can have getters and setters. This is just some nice flavour that means you can set functions like get_two = ()=>1+2, then getting instance.two will return the result of that function. Setters work the same way.
  • DefineClass has been added, which lets you use a chain of functions are arguments, and it will appropriately set parents and define new classes where applicable. Eg. DefineClass(Mob Living Carbon Human) will ensure that all the classes are defined.

1513138154063