Using Reflect and Type

Introduction

This tutorial demonstrates how to use reflection. The Reflect class allows code to view and modify the structure of data types at runtime. The Type class contains functions that let us determine variable types and instantiate objects by name at runtime. Both classes are made up of static methods.

Variable Type Checks


This example does a few type checks on variables of different types. We check to see if some variables are objects or functions. Then we get variable types as strings. Then we get the class names of the variables that are objects.
class TypeCheckEx
{
  public function new()
  {}

  public static function func()
  {}

  public static function main()
  {
    // set some variables
    var obj = new TypeCheckEx();
    var anon = {one:1, two:2.2, three: "three"};
    var one = 1;
    var str = "string";

    // detect if a variable is an object
    trace("object check:");
    trace("  obj is an object: " + Reflect.isObject(obj));
    trace("  anon is an object: " + Reflect.isObject(anon));
    trace("  1 is an object: " + Reflect.isObject(one));

    // detect if a variable is a function
    trace("function check:");
    trace("  str is a function: " + Reflect.isFunction(str));
    trace("  func is a function: " + Reflect.isFunction(func));

    // get type
    trace("type lookup:");
    trace("  one is a: " + Type.typeof(one));
    trace("  anon is a: " + Type.typeof(anon));
    trace("  func is a: " + Type.typeof(func));
    trace("  str is a: " + Type.typeof(str));
    trace("  obj is a: " + Type.typeof(obj));

    // get class name
    trace("class name lookup:");
    trace("  str is a: " + Type.getClassName(Type.getClass(str)));
    trace("  obj is a: " + Type.getClassName(Type.getClass(obj)));
  }
}

Compile and run with:

haxe -neko ex1.n -main TypeCheckEx
neko ex1.n

The output should be:

TypeCheckEx.hx:18: object check:
TypeCheckEx.hx:19:   obj is an object: true
TypeCheckEx.hx:20:   anon is an object: true
TypeCheckEx.hx:21:   1 is an object: false
TypeCheckEx.hx:24: function check:
TypeCheckEx.hx:25:   str is a function: false
TypeCheckEx.hx:26:   func is a function: true
TypeCheckEx.hx:29: type lookup:
TypeCheckEx.hx:30:   one is a: TInt
TypeCheckEx.hx:31:   anon is a: TObject
TypeCheckEx.hx:32:   func is a: TFunction
TypeCheckEx.hx:33:   str is a: TClass({ ... })
TypeCheckEx.hx:34:   obj is a: TClass({ ... })
TypeCheckEx.hx:37: class name lookup:
TypeCheckEx.hx:38:   str is a: String
TypeCheckEx.hx:39:   obj is a: TypeCheckEx

See ValueType for the list of names returned by Type.typeof.

TEnum and TClass contain type parameters and function prototypes which were removed to simplify the output above.

More Variable Type Checks

Haxe offers even more tools for checking types. This example demonstrates the usage of the type and Std.is functions. The type function outputs the type of the given variable to the console at compile time. This output is formatted like a trace statement. Std.is returns true if the given variable is of the given type, or is a subclass of that type. The type is specified using the actual class name, not the ValueType value.

import Type;

class TypeCheckEx2
{
  public function new() {}

  public static function main()
  {
    var a : Int = 1;    // type explicitly set
    var b = 2.2;        // type inferred
    var c : String = "str";
    var d = new TypeCheckEx2();

    // these output to stdout during compile
    type(a);
    type(b);
    type(c);
    type(d);

    // a is an int and float
    trace("a is an Int: " + Std.is(a, Int));
    trace("a is a Float: " + Std.is(a, Float));

    // b is only a float
    trace("b is an Int: " + Std.is(b, Int));
    trace("b is a Float: " + Std.is(b, Float));

    // c is a string
    trace("c is a String: " + Std.is(c, String));

    // d is a TypeCheckEx2
    trace("b is a TypeCheckEx2: " + Std.is(d, TypeCheckEx2));
  }
}

Compile with:

haxe -neko ex2.n -main TypeCheckEx2 

The output from compile should be:

TypeCheckEx2.hx:16: characters 9-10 : Int
TypeCheckEx2.hx:17: characters 9-10 : Float
TypeCheckEx2.hx:18: characters 9-10 : String
TypeCheckEx2.hx:19: characters 9-10 : TypeCheckEx2

Run with:

neko ex2.n

The output should be:

TypeCheckEx2.hx:22: a is an Int: true
TypeCheckEx2.hx:23: a is a Float: true
TypeCheckEx2.hx:26: b is an Int: false
TypeCheckEx2.hx:27: b is a Float: true
TypeCheckEx2.hx:30: c is a String: true
TypeCheckEx2.hx:33: b is a TypeCheckEx2: true

Note that a is both an Int and a Float. This is because Int is a subclass of Float.

Anonymous Types

In this example, we create an object with anonymous type, look through its fields and copy it. Note that Reflect.fields returns a list of field names. To get the field values, we must call Reflect.field with the field name.

The example goes on to modify the original object (capitalize it) and print both the original and the copy. Since Reflect.copy doesn't make deep copies, our modification of the original object's four.one field also affects the copy. This happens because both variable's four field point to the same object.

class AnonEx
{
  public static function main()
  {
    // create an object with an anonymous type
    var anon = {one:1, two:2.2, three: "three", four:{one:"one"}};

    // detect if an object has a specified field
    // note that notSet is not seen
    trace("field check:");
    trace("  anon has field one: " +
                     Reflect.hasField(anon, "one"));
    trace("  anon has field five: " +
                     Reflect.hasField(anon, "five"));

    // print fields from the anonymous object
    trace("anonymous type fields:");
    for( ff in Reflect.fields(anon) )
      trace("  anon." + ff + " = " + Reflect.field(anon, ff));

    // copy it
    var anonCopy = Reflect.copy(anon);

    // modify the original
    anon.two = 2.2222; // does not affect copy
    anon.three = "THREE"; // does not affect copy
    anon.four.one = "ONE"; // affects copy

    // print the original
    trace("modified type fields:");
    for( ff in Reflect.fields(anon) )
      trace("  anon." + ff + " = " + Reflect.field(anon, ff));

    // print the copy
    trace("anonymous copy type fields:");
    for( ff in Reflect.fields(anonCopy) )
      trace("  anon." + ff + " = " + Reflect.field(anonCopy, ff));
  }
}

Compile and run with:

haxe -neko ex3.n -main AnonEx
neko ex3.n

The output should be:

AnonEx.hx:10: field check:
AnonEx.hx:11:   anon has field one: true
AnonEx.hx:13:   anon has field five: false
AnonEx.hx:17: anonymous type fields:
AnonEx.hx:19:   anon.four = { one => one }
AnonEx.hx:19:   anon.one = 1
AnonEx.hx:19:   anon.two = 2.2
AnonEx.hx:19:   anon.three = three
AnonEx.hx:30: modified type fields:
AnonEx.hx:32:   anon.four = { one => ONE }
AnonEx.hx:32:   anon.one = 1
AnonEx.hx:32:   anon.two = 2.2222
AnonEx.hx:32:   anon.three = THREE
AnonEx.hx:35: anonymous copy type fields:
AnonEx.hx:37:   anon.four = { one => ONE }
AnonEx.hx:37:   anon.one = 1
AnonEx.hx:37:   anon.two = 2.2
AnonEx.hx:37:   anon.three = three

Classes

This example instantiates an object using the class name as a string literal. This is necessary if the desired type is not known until runtime. Then the example accesses the object's fields and methods by name using strings. Then it removes a field and adds another to the object.

Note that Reflect.fields only returns fields that have been set, whereas Type.getInstanceFields returns all fields and methods. Also note that Reflect.fields allows access to private fields.

The result of printing the value of a method is "#function:n" where n is the number of arguments it takes.

class ObjEx
{
  // a private field
  private var privateNum : Int;

  // a public property
  public var publicString(default,null) : String;

  // reflection ignores this
  public var notSet : String;

  // initialize fields
  public function new()
  {
    privateNum = 2;
    publicString = "car";
  }

  // an example method. adds two numbers
  public function sum(a,b)
  {
    return a+b;
  }

  // an example method
  public function getHi()
  {
    return "Hi";
  }

  public static function main()
  {
    // set some variables
    var obj = new ObjEx();
    var str = "string";
    var sum = obj.sum;

    // create class by name
    trace("\ncreate class by name:");
    var obj2 = Type.createInstance(Type.resolveClass("ObjEx"),[]);
    trace(" obj2.publicString : " + obj2.publicString);
    trace(" obj2.getHi() : " + obj2.getHi());

    // detect if an object has a specified field
    // note that notSet is not seen
    trace("field check:");
    trace("  obj has sum field: " +
                     Reflect.hasField(obj, "notSet"));
    trace("  obj has publicString field: " +
                     Reflect.hasField(obj, "publicString"));

    // print all fields and methods
    trace("all class fields:");
    var allFields = Type.getInstanceFields(Type.resolveClass("ObjEx"));
    for( ff in allFields )
      trace("  obj." + ff + " = " + Reflect.field(obj, ff));

    // print all methods
    trace("methods:");
    var isFunction = function(name)
      { return Reflect.isFunction(Reflect.field(obj,name)); }
    var functions = Lambda.filter(allFields, isFunction);
    for( ff in functions )
      trace("  obj." + ff + "()");

    // print data fields
    // note that notSet is skipped, and the private field is visible
    trace("data fields:");
    for( ff in Reflect.fields(obj) )
      trace("  obj." + ff + " = " + Reflect.field(obj, ff));

    // delete a field. create another one
    Reflect.deleteField(obj, "publicString");
    Reflect.setField(obj, "newString", "boat");

    // print fields again
    trace("fields after modifying:");
    for( ff in Reflect.fields(obj) )
      trace("  obj." + ff + " = " + Reflect.field(obj, ff));

    // call a method
    // note that arguments cannot be optional when called this way
    trace("call methods:");
    trace("  obj.sum(1,2): " +
                     Reflect.callMethod(obj, Reflect.field(obj,"sum"), [1,2]));
    trace("  obj.getHi(): " +
                     Reflect.callMethod(obj, Reflect.field(obj,"getHi"), []));
  }
}

Compile and run with:

haxe -neko ex4.n -main ObjEx
neko ex4.n

The output should be:

ObjEx.hx:39: create class by name:
ObjEx.hx:41:  obj2.publicString : car
ObjEx.hx:42:  obj2.getHi() : Hi
ObjEx.hx:46: field check:
ObjEx.hx:47:   obj has sum field: false
ObjEx.hx:49:   obj has publicString field: true
ObjEx.hx:53: all class fields:
ObjEx.hx:56:   obj.sum = #function:2
ObjEx.hx:56:   obj.privateNum = 2
ObjEx.hx:56:   obj.getHi = #function:0
ObjEx.hx:56:   obj.publicString = car
ObjEx.hx:56:   obj.notSet = null
ObjEx.hx:59: methods:
ObjEx.hx:64:   obj.sum()
ObjEx.hx:64:   obj.getHi()
ObjEx.hx:68: data fields:
ObjEx.hx:70:   obj.privateNum = 2
ObjEx.hx:70:   obj.publicString = car
ObjEx.hx:77: fields after modifying:
ObjEx.hx:79:   obj.newString = boat
ObjEx.hx:79:   obj.privateNum = 2
ObjEx.hx:83: call methods:
ObjEx.hx:84:   obj.sum(1,2): 3
ObjEx.hx:86:   obj.getHi(): Hi

VarArgs

This example shows how to create a function that takes a variable number of arguments. In the example, we call the sub function twice. The first time, we pass in an Array of Dynamic values, as the function's definition specifies. Then we get a vararg handle to the function (varArgSub), and call it passing in each argument separately. The output of each call is identical.

The values of the arguments we use are a variety of types. In the example we are just printing them, so their types don't matter, but if we were doing more with them it is a good idea to check their types before accessing them to make sure they're what the function expects. Since they're declared Dynamic the compiler isn't doing any type checking for us.

class VarArgsEx
{
  public static function main()
  {
    // call sub normally
    var input : Array<Dynamic> = [1,2,3,[4,5,6],"cat"];
    trace("sub called normally");
    sub(input);

    // call with var arg
    var varArgSub = Reflect.makeVarArgs(sub);
    trace("sub called with var arg");
    varArgSub(1,2,3,[4,5,6],"cat");
  }

  private static function sub(val:Array<Dynamic>) : Dynamic
  {
    for( ii in val )
      trace("  param: " + ii);
  }
}

Compile and run with:

haxe -neko varargs.n -main VarArgsEx
neko varargs.n

The output should be:

VarArgsEx.hx:7: sub called normally
VarArgsEx.hx:19:   param: 1
VarArgsEx.hx:19:   param: 2
VarArgsEx.hx:19:   param: 3
VarArgsEx.hx:19:   param: [4, 5, 6]
VarArgsEx.hx:19:   param: cat
VarArgsEx.hx:12: sub called with var arg
VarArgsEx.hx:19:   param: 1
VarArgsEx.hx:19:   param: 2
VarArgsEx.hx:19:   param: 3
VarArgsEx.hx:19:   param: [4, 5, 6]
VarArgsEx.hx:19:   param: cat

Type Inference


We use type inference in these examples to improve code readability, but we could have declared each variable's type explicitly. The following are alternate ways to define variables from the above examples that specify the types explicitly.

From TypeCheckEx, obj could be defined as:

    var obj : TypeCheckEx = new TypeCheckEx();

anon could be defined as:

    var anon : {one:Int, two:Float, three:String} = {one:1, two:2.2, three: "three"};

one could be defined as:

    var one : Int = 1;

str could be defined as:

    var str : String = "string";

From ObjEx, the sum method could be defined as:

  public function sum(a:Int, b:Int) : Int
  {
    return a+b;
  }

version #13758, modified 2012-04-15 08:48:13 by elyon