4.10 Struct Methods

At this point you may be thinking something on the line of “hey, since variables and functions are also members of the struct, I should be able to access them the same way than fields, right?”.

So you will want to do:

(poke) var p = Packet  12#B
(poke) p.real_size
(poke) p.corrected_crc

But sorry, this won’t work.

To understand why, think about the struct building process we sketched above. The mapper and constructor functions are derived/compiled from the struct type. You can imagine them to have prototypes like:

Packet_mapper (IOspace, offset) -> Packet value
Packet_constructor (template)   -> Packet value

You can also picture the fields, variables and functions in the struct type specification as being defined inside the bodies of Packet_mapper and Packet_constructor, as their contents get mapped/constructed. For example, let’s see what the mapper does:

Packet_mapper:

  . Map a byte, put it in a local `magic'.
  . Map a byte, put it in a local `size'.
  . Calculate the real size, put it in a local `real_size'.
  . Map an array of real_size bytes, put it in a local `payload'.
  . Map an array of real_size bytes, put it in a local `control'.
  . Compile a function, put it in a local `corrected_crc'.
  . map a byte, call the function in the local `corrected_crc',
    complain if the values are not the same, otherwise put the
    mapped byte in a local `crc'.
  . Build a struct value with the values from the locals `magic',
    `size', `payload', `control' and `crc', and return it.

The pseudo-code for the constructor would be almost identical. Just replace "map a byte" with “construct a byte”.

So you see, both the values for the mapped fields and the values for the variables and functions defined inside the struct type end as locals of the mapping process, but only the values of the fields are actually put in the struct value that is returned in the last step.

This is where methods come in the picture. A method looks very similar to a function, but it is not quite the same thing. Let me show you an example:

load supercrc;

type Packet =
  struct
  {
    byte magic == 0xab;
    byte size;

    var real_size = (size == 0xff ? 0 : size);

    byte[real_size] payload;
    byte[real_size] control;

    fun corrected_crc = int:
    {
      try return calculate_crc (payload, control);
      catch if E_div_by_zero { return 0; }
    }

    int crc = corrected_crc;

    method c_crc = int:
    {
      return corrected_crc;
    }
  };

We have added a method c_crc to our Packet struct type, that just returns the corrected superCRC (patented, TM) of a packet. This can be invoked using dot-notation, once a Packet value is mapped/constructed:

(poke) var p = Packet  12#B
(poke) p.c_crc
0xdeadbeef

Now, the important bit here is that the method returns the corrected crc of a Packet. That’s it, it actually operates on a Packet value. This Packet value gets implicitly passed as an argument whenever a method is invoked.

We can visualize this with the following “pseudo Poke”:

method c_crc = (Packet SELF) int:
{
   return SELF.corrected_crc;
}

Fortunately, poke takes care to recognize when you are referring to fields of this implicit struct value, and does The Right Thing(TM) for you. This includes calling other methods:

method foo = void: { ... }
method bar = void:
{
 [...]
 foo;
}

The corresponding “pseudo-poke” being:

method bar = (Packet SELF) void:
{
 [...]
 SELF.foo ();
}

It is also possible to define methods that modify the contents of struct fields, no problem:

var packet_special = 0xff;

type Packet =
  struct
  {
    byte magic == 0xab;
    byte size;
    [...]

    method set_size = (byte s) void:
    {
      if (s == 0)
        size = packet_special;
      else
        size = s;
    }
  };

This is what is commonly known as a setter. Note, incidentally, how a method can also use regular variables. The Poke compiler knows when to generate a store in a normal variable such as packet_special, and when to generate a set to a SELF field.

Given the different nature of the variables, functions and methods, there are a couple of restrictions:

Something to keep in mind about methods is that they can destroy the integrity of the data stored in a struct. Consider for example the following struct type:

type Packet =
  struct
  {
    byte magic == 0xab;
    byte size : size <= 4096;
    [...]

    method set_size = (byte s) void:
    {
      if (s == 0)
        size = packet_special;
      else
        size = s;
    }
  };

Observe how this new version of Packet has an additional constraint that specifies size should not exceed 4096. However, when the method set_size is executed the constraints are not checked again. This is useful at times, but can also lead to unintended data corruption.

A solution for this problem is to make methods aware of the restrictions. Like in this case:

type Packet =
  struct
  {
    var MAXSIZE = 4096;

    byte magic == 0xab;
    byte size : size <= MAXSIZE;
    [...]

    method set_size = (byte s) void:
    {
      if (s == 0)
        size = packet_special;
      else if (s > MAXSIZE)
        raise E_inval;
      else
        size = s;
    }
  };

Note how we use a variable MAXSIZE in order to avoid hard-coding 4096 twice in the struct definition.

Occasionally it is useful to access to additional properties of a containing struct or union in a method, other than its fields. For this purpose it is possible to refer explicitly to the struct using the SELF keyword. Note that the value referred has type any.

For example, this is how we could print the name of the current alternative in a pretty-printer for an union:

type Foo =
  union int<32>
    {
      int<32> One == 1;
      int<32> Two == 2;
      method _print = void:
      {
        printf ("%s (%v)", SELF'ename (0), SELF'elem (0));
      }
    };