tink_lang

back2dos edited this page Dec 7, 2012 · 9 revisions

The tink_lang package bundles a number of syntactic sugars and is separately available on haxelib.

Currently all extended syntax becomes available to classes implementing tink.lang.Cls. In doing so, these features become available:

Debug output

Prefixing a class/interface with the @:verbose metadata will cause the compiler to output warnings at the position of each field, indicating the fields generated from the syntactic sugar used.

Accessor generation

The @:prop tag is used to create shorthand accessors on fields. As of now, it skips properties, as well as static and explicitly private fields. Usage:

class A implements tink.lang.Cls {
    @:prop var a:Float;
    @:prop(Math.max(param, 5)) var b:Float;
    @:prop(b * 2, b = param / 2) var c:Float;
    @:prop(a + b, _) var d:Float;
    @:read var e:Float;
    @:read( { trace(a); a + c; } ) var f:Float;
}

Compiles to:

class A implements tink.lang.Cls {
    public var a(get_a, set_a):Float;
    public var b(get_b, set_b):Float;
    public var c(get_c, set_c):Float;
    public var d(get_d, null):Float;
    public var e(get_e, null):Float;
    public var f(get_f, null):Float;

    function get_a():Float  return this.a
    function set_a(param:Float):Float return this.a = param
    function get_b():Float return this.b
    function set_b(param:Float):Int return this.b = Math.max(param, 5)
    function get_c():Float return b * 2
    function set_c(param:Float):Float return { b = param / 2; this.c; }
    function get_d():Float return a + b
    function get_e():Float return this.e
    function get_f():Float return { trace(a); a + c; }
}

The Rules in Detail

  • 0 arguments: When used without arguments, a plain getter and setter will be generated.
  • 1 argument: If 1 argument is given, it is used as the body of the setter. The return value of the expression serves as the new value of the field and as the return value of the setter.
  • 2 arguments: If 2 arguments are given, they serve to construct both a getter and a setter. It is assumed, that with a custom getter, the field as such is actually "virtual". Therefore the generated setter will not assign any value to the field and its return value is determined by accessing the getter.
    Please not however, if the second parameter is merely _, then no setter is generated and the field is readonly.
  • Using @:read instead, one can declare readonly fields, with a plain getter, or a custom one, if used with 1 argument.
  • Unlike plain fields, which default to private in Haxe, properties generated by tink_lang default to public since that by far the most common use case. Explicit private visibility will however be respected, so @:prop private foo:Int will yield private var foo(get_foo, set_foo):Int.

Interfaces

This feature also works with interfaces, but implementing Cls on an inteface will propagate the tink_lang language extensions to all implementors of said interface. This might not always be desirable.

Initialization during definition

Tink allows field initialization during definition like so:

class A implements tink.lang.Cls {
      var a:Int = _;//initialize from constructor
      var b:Int = 5;//initialize with expression
      var c:Int = (6);//initialize from constructor with default value
}

Will compile to:

class A implements tink.lang.Cls {
      var a:Int;
      var b:Int;
      var c:Int;
      public function new(a:Int, ?c:Int = 6) {
            this.a = a;
            this.b = 5;
            this.c = c;
      }
}

IMPORTANT: Using this kind of initialization will bypass setters and write to the field directly. If you wish to initialize the field through its setter, you will have to do so in the constructor.

Rules for constructor generation:

  • If a constructor is found, the constructor is conserved.
  • If the existing constructor is explicitly private or public, it will remain private or public. Otherwise it will become public as soon as any parameter needs to be added
  • If no constructor is found, a public empty constructor is created (currently without any super statement). If there is a super-class and the super-class has a constructor, an error is generated.
  • All additional arguments are appended to the argument list in order of appearance.
  • All initialization statements are executed in order of appearance and before the existing constructor body (if any exists)

Extended for loop syntax

Simultaneous loops and fallbacks

With tink_lang, you can iterate over multiple targets at the same time.

Example

var girls = ['Lilly', 'Peggy', 'Sue'];
var boys = ['Peter', 'Paul', 'Joe', 'John', 'Jack'];
for ([girl in girls, boy in boys])
    trace(girl + ' loves ' + boy);
-- OUTPUT:
Lilly loves Peter
Peggy loves Paul
Sue loves Joe

Now that's kind of bad luck for John and Jack. Luckily there's one person they can always lean on:

var girls = ['Lilly', 'Peggy', 'Sue'];
var boys = ['Peter', 'Paul', 'Joe', 'John', 'Jack'];
for ([girl in girls || 'Mommy', boy in boys])
    trace(girl + ' loves ' + boy);
-- OUTPUT:
Lilly loves Peter
Peggy loves Paul
Sue loves Joe
Mommy loves John
Mommy loves Jack

Rules

To iterate over multiple targets, use the syntax for ([v1 in target1, v2 in target2, ...]) body. The iteration will stop as soon as any target is "depleted". You can avoid the depeletion of any target to stop the loop by specifying a fallback through target || fallback. If you specify fallbacks for all targets, the loop will continue until all targets are depleted. It is important to know that the fallback expression will be evaluated every time a fallback is needed, i.e. if it is not referentially transparent, it will lead to a different result every time.

Arbitrary step numerical loops

The following syntax is allow with tink_lang:

for (i += step in min...max) body;
for (i -= step in max...min) body;

This also works for float loops. The type of step will determine whether this is a for loop or an int loop.

The downward loop is symmetrical to the upward loop, i.e. it will yield the same values, only in backward order. A upward loop will always start with min and stop just before max (except in the case of float precision issues), while an upward loop will always end with min, starting just "after" max.

This syntax can be used with simultaneous looping and fallbacks.

Syntactic delegation

Tinkerbell supports syntactic delegation for both fields and methods. The basic idea is, that you can automatically have the delegating class call methods or access properties on the objects it is delegating to. In the simpler of two cases, the class delegates to one of its members. A very simple example:

  class Stack<T> implements tink.lang.Cls {
       @:forward(push, pop, iterator, length) var elements:Array<T>;
       public function new() {
            this.elements = [];
       }
  }

Here, we are forwarding the calls push, pop, iterator as well as the field length to the underlying data-structure.

Another example:

  class OrderedHash<T> implements tink.lang.Cls {
      var keyList:Array<String> = [];
      @:forward var map:Hash<T> = new Hash<T>();
      public function new() {}
      public function set(key:String, value:T) 
           if (!exists(key)) {
                keyList.push(key);
                map.set(key, value);
           }
      public function remove(key:String) return map.remove(key) && keyList.remove(key)
      public function keys() return keyList.iterator()
  }

Filters

As you have seen in the above example, we chose which fields to forward. What we are doing here is matching a field against a filter. The rules:

  • An identifier matches the field with the same name
  • A regular expression matches all fields with matching names
  • A string matches all fields matching it, with the *-character being matching any character sequence, i.e. do* would match all members starting with "do" and *Slot matches all members ending with "Slot"
  • filter_1 | filter_2 and filter_1 || filter_2 match if either filter matches
  • [filter_1, ..., filter_n] matches if either of the filters match
  • filter_1 & filter_2 and filter_1 && filter_2 match if both filters match
  • !filter matches if filter doesn't match

If the @:forward-tag has no arguments, then all fields are matched. Otherwise all fields matching either argument are matched.

Delegating to a member

Usage example:

    //let's take two sample classes
    class Foo {
         public function fooX(x:X):Void;
         public function yFoo():Y;
    }
    class Bar {
         public var barVar:V;
         public function doBar(a:A, b:B, c:C):R;
    }
    //and now we can do
    class FooBar implements tink.lang.Cls {
         @:forward var foo:Foo;
         @:forward var bar:Bar;
    }
    //which corresponds to
    class FooBar implements tink.lang.Cls {
         var foo:Foo;
         var bar:Bar;
         public function fooX(x) return foo.fooX(x)
         public function yFoo() return foo.yFoo()
         @:prop(bar.barVar, bar.barVar = param) var barVar:V;//see accessor generation
         public function doBar(a, b, c) return bar.doBar(a,b,c)
    }

Handling with a method

This kind of forwarding may appear a little strange at first, but let's see this in action:

    //Foo and Bar defined in the example above
    class FooBar2 implements tink.lang.Cls {
         var fields:Hash<Dynamic>;
         @:forward function (foo:Foo, bar:Bar) {
               get: fields.get($name),
               set: fields.set($name, param),
               call: trace('calling '+$name+' on '+$id+' with '+$args)
         }
    }

This becomes (actually this is simplified for your convenience):

  class Foobar2 implements TinkClass {
         var fields:Hash<Dynamic>;
         public function fooX(x:X) trace('calling '+'fooX'+' on '+'foo'+' with '+[x])
         public function yFoo() trace('calling '+'yFoo'+' on '+'foo'+' with '+[])
         @:prop(fields.get('barVar'), fields.set('barVar', param)) var barVar:V;//see accessor generation
         public function doBar(a:A, b:B, c:C) trace('calling '+'doBar'+' on '+'bar'+' with '+[a, b, c])
  }

This feature is still a little rough, but its intention is basically, that you can quite easily roll you own mechanism similar to haxe.remoting.AsyncProxy.

Rules

  • Forward is generated per member in order of appearance
  • If a member with a given name already exists, no forward statement is generated (i.e. if FooBar already had a method fooX in the above statement, the forwarding method would not be generated)
  • As of now no conflict resolution with super classes occurs. OTOH hand syntactic delegation is mostly meant for those who prefer composition over inheritance ;)