Getting a list of all methods or fields in a module or an object

To dig into an aggregate, the first step is to get a list of members. Then, we'll be able to look at each individual member and dig as deeply as we need to.

How to do it…

Let's execute the following steps to get a list of all methods or fields in a module or object:

  1. Get a reference to the aggregate. For a struct, class, or enum type, you may use its name directly (or if it is passed to a template, the corresponding template parameter). To get a reference to a module, use mixin("yourmodule.name").
  2. Get a list of the names of all members with __traits(allMembers).
  3. Retrieve the member with __traits(getMember). Use a helper template in the form of alias helper(alias T) = T; and an alias member in the loop to make referring to the member easier.
  4. Using static if and the is expression, filter out any types: static if(is(member)) { /* type */ }.
  5. Then, check for functions with the is expression on typeof(member): else static if(is(typeof(member) == function)) { /* function */ }.
  6. If the member is neither a type nor a function, it may still be a template, a module, a variable, or some other construct added to the language later. If it has a valid .init type, it is a variable; otherwise, some guesswork is needed. If the string value starts with module, you can tell it is a module. If nothing else matches, it is most likely a template.
  7. You may use __traits(compiles) on the getMember function to filter out private variables if you need to.

The following is a program that defines some test structures and then drills down using the preceding steps to show details about all the members of the module:

struct S {
  // variables
  int a;
  int b;
  void delegate() c;
  string d;

  alias e = d;

  // functions
  void foo() {}
  int bar() { return 0; }

  // types
  struct Bar {}
  enum Foo { a, b }
}

template TestTemplate(string s) {
  enum TestTemplate = to!int(s);
}

// the helper for easier aliasing we'll need in step 3
alias helper(alias T) = T;

// This function writes details about all members of what it
// is passed. The string before argument is related to
// indenting the data it prints.
void inspect(alias T)(string before) {
  import std.stdio;
  import std.algorithm;

  // step 2
  foreach(memberName; __traits(allMembers, T)) {
    // step 3
    alias member = helper!(__traits(getMember, T, memberName));
    // step 4 - inspecting types
    static if(is(member)) {
      string specifically;
      static if(is(member == struct))
        specifically = "struct";
      else static if(is(member == class))
        specifically = "class";
      else static if(is(member == enum))
        specifically = "enum";

      writeln(before, memberName, " is a type (", specifically, ")");

      // drill down (step 1 again)
      inspect!member(before ~ "	");
    } else static if(is(typeof(member) == function)) {
      // step 5, inspecting functions
      writeln(before, memberName, " is a function typed ", typeof(member).stringof);
    } else {
      // step 6, everything else
      static if(member.stringof.startsWith("module "))
        writeln(before, memberName, " is a module");
      else static if(is(typeof(member.init)))
        writeln(before, memberName, " is a variable typed ", typeof(member).stringof);
      else
        writeln(before, memberName, " is likely a template");
    }
  }
}
void main() {
  // step 1: we'll start with a reference
  // to the current module, gotten with mixin.
  // Note: __MODULE__ is a special symbol the
  // compiler replaces with the current module name.
  inspect!(mixin(__MODULE__))("");
}

When you run the program, you can see that it reflects the full structure of our code, including indented child members. This is shown in the following output:

object is a module
S is a type (struct)
        a is a variable typed int
        b is a variable typed int
        c is a variable typed void delegate()
        d is a variable typed string
        e is a variable typed string
        foo is a function typed void()
        bar is a function typed int()
        Bar is a type (struct)
        Foo is a type (enum)
                a is a variable typed Foo
                b is a variable typed Foo
TestTemplate is likely a template
helper is likely a template
inspect is likely a template
main is a function typed void()

Note

The output will also include the module object, which is automatically imported by all D modules.

How it works…

The __traits method and the is expression aren't limited to conditional compilation. Each feature has several operations to perform in-depth compile-time reflection. Moreover, the .stringof and .mangleof properties of the most D symbols can be used for a variety of purposes, including reparsing them to gather even more information.

The first and the foremost among them is __traits(allMembers, Aggregate), which returns the names of each member of an aggregate (struct, class, union, enum, or module). Once we have the name of a member, we use __traits(getMember, Aggregate, name) to turn it into a symbol. A symbol is a specific name in the code, a class name, a variable name, and so on.

Working with symbols is different than working with variables. If you declared a new variable (for example, with auto or const), that will create a new symbol instead of working with the existing one. Instead, we alias the new names to the existing symbol. The alias term works in compile-time parameter lists or anywhere a declaration is allowed, including inside functions.

Due to a limitation of the D grammar and parser, we use a simple alias helper to enable its use with more parameters. For example, alias a = __traits(getMember, Foo, bar); will fail with a parse error saying basic type expected, not __traits. The alias helper allows us to easily work around this limitation, saving us from repeating the whole getMember expression every time we want to use it.

Tip

The alias keyword also works when renaming the complex types, for example, binding compile-time parameters to a single name alias toInt = to!int; would let you use toInt anytime you would have used to!int.

Once we have the symbol aliased to a convenient name, we can start to learn more about it. First, we categorize it into three broad areas: types, functions, and others, using static if and the is expression. The first test, static if(is(member)), simply tests if a member represents a valid type, that is, it doesn't check whether it has a valid type; it checks if it is a valid type. This is an important distinction because it is how we differentiate types, including aggregate definitions that are types and can be drilled into directly with the is expression, from variables and functions, which have types that can be retrieved with typeof() to drill in to.

The next major category we test for is functions, with is(typeof(member) == function). Functions get their own category because various special details are available for them that aren't available for other variables, such as parameter information.

Finally, we consider everything else to be other and use a variety of techniques to break it down. To differentiate modules from everything else, we perform a simple check on .stringof to see whether the name starts with the module keyword. If it isn't a module, we test the next broad category by checking if it has a validly typed .init property, which is a feature of all the D variables. If nothing has matched yet, we can assume it is most likely a template by process of elimination. Currently, reflection of templates is very limited, so we can't drill any more into it.

Once we break a symbol into one of the following broad categories, we can dig even deeper with consistent techniques:

  • Types can be further categorized with the is expression, for example, is(member == class) tests whether it is a class declaration, and can also be tested for additional members with __traits(allMembers).
  • Functions can be drilled into by using the std.traits functions, or the prototype can simply be displayed to the user with .stringof.
  • Modules can be drilled into by recursively calling __traits(allMembers) upon it. Keep in mind that the hidden import of objects is listed. So, if you drill down every member, you will also see D runtime code.
  • Variables can be drilled into by examining their name or type at compile time or by value at runtime. Phobos' std.traits module has several members to categorize types as well, for example, isIntegral!(typeof(member)) or isSomeString!(typeof(member)).

The categories have some overlap in techniques, but since there are several differences in what details are available and how to get to them. Categorizing before trying to dig into the reflection will help avoid compile errors.

Tip

You can also call the getMember function on an instance if you want a directly usable reference, such as a member to call with a valid this pointer.

There's more…

Another way to get members of some types, mainly structs and classes, is with the tupleof property. The tupleof property yields a list of all the variables in the type in the same order that they appear in memory, including a hidden context pointer in the case of nested structures. The .tupleof property can be written to and looped over with foreach. Some code written before __traits(allMembers) was added to the language used .stringof in a loop over obj.tupleof to get the names of member variables.

The .tupleof property does not include child types or functions, but does, like other D tuples, have the interesting characteristic of automatically expanding function argument lists, as shown in the following code:

struct Test {
  int a;
  string b;
}

void test(int a, string b) {
  import std.stdio;
  writeln(a, " ", b);
}

void main() {
  Test t;
  t.a = 10;
  t.b = "cool";
  // test(t); // won't compile, Test is not int, string
  test(t.tupleof);// WILL compile!
}

So, while you can use the .tupleof property for reflection, its main strength is in variable manipulation and expansion.

See also

  • Refer to the documentation for the Phobos std.traits module at http://dlang.org/phobos/std_traits.html, which encapsulates and expands upon many of the compile-time reflection capabilities provided by the compiler, often with more readable syntax. Check this first when trying to perform a new reflection-related task. It contains many more useful functions other than the ones mentioned here.
  • Refer to the documentation for the is expression at http://dlang.org/expression.html#IsExpression. Notice that it has seven forms, each with unique capabilities.
  • Refer to the documentation for the __traits feature of the language at http://dlang.org/traits.html.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.67.16