Skip to content

Defining New Types

Vinícius Garcia edited this page Mar 4, 2018 · 59 revisions

There are two ways to declare new types: Defining a Primitive Data Type, which is slightly more efficient, and by defining a Compound Data Type which is easier to implement.

Implementing Primitive Data Types is not recommended for three reasons: (1) We expect the built-in primitive types to be enough for most needs (2) there is an upper limit of 29 primitive types and (3) it is easier to insert errors when implementing this one. However, it is possible to implement and a tutorial follows on the end of this page. Just make sure you try first to implement your type using the Compound Data Type method before trying to use the Primitive Data Type.

Creating a Compound Data Type

Note: To read this section it is recommended to read first the pages: Defining New Functions, Defining New Operations and Creating a Built-in Class.

To create a new Compound Data Type is like creating a new built-in class: It is necessary to first define a TokenMap containing the attributes of the type:

  • A TAG attribute just to identify the type.
  • Any built-in functions that you want available for the users of your type (e.g. my_type.toStr())
// The base class for the file objects.
TokenMap BASE_myType;

struct Startup {
  Startup() {
    // Add a tag to identify the type:
    BASE_myType["__type__"] = "my_type";
    
    // Add any functions you believe are necessary, e.g.:
    BASE_myType["__str__"] = CppFunction(&myType_toStr, "str");
  }
} Startup;

Note: to learn how to write functions for your class such as the myType_toStr function above, read Creating a Built in Class.

Then it will be needed to define a constructor for the type, e.g. complex(real_part, complex_part) could be used as the constructor for a complex numbers type and it could be defined like this:

packToken default_myType_constructor(TokenMap scope) {
  TokenMap instance = BASE_myType.getChild();

  // Use arguments from `scope` to set up the new instance of the class:
  instance["real"] = scope["real"];
  instance["imaginary"] = scope["imaginary"];

  return instance;
}

struct Startup {
  MyStartup() {
    TokenMap& global = TokenMap::default_global();
    global["complex"] = CppFunction(&default_myType_constructor,
                                    {"real", "imaginary"}, "complex");
  }
} Startup; 

After that, we would need to create operations to interact with this data type. The operations =, == and != will work by default with any new data type, however, you might have to redefine operations such as Complex + Complex, Complex - Complex using the method described in Defining New Operations, e.g.:

packToken my_complex_sum(const packToken& left,
                         const packToken& right, evaluationData* data) {
  double left_real, left_img;
  double left_real, right_img;

  // Extract the information from both operands:
  TokenMap mleft = left.asMap();

  // Use `find` to search all the inheritance tree
  // for the '__type__' attribute:
  packToken* type = mleft->find("__type__");
  if (type && type->asString() == "my_type") {
    left_real = mleft["real"];
    left_img = mleft["imaginary"];
  } else {
    // If one of the maps is not of the expected type reject it:
    throw Operation::Reject();
    // Or if you know there is no more operations possible using a MAP on
    // the left side and the operator "+", you may throw a proper error report:
    throw undefined_operation(data->op, left, right);
  }

  /* ... do the same to get information from the right token ... */
  
  // Then create a new "complex" instance:
  TokenMap result = BASE_myType.getChild();
  result["real"] = left_real + right_real;
  result["imaginary"] = left_img + right_img;

  return result;
}

Finally, you may register the operations you created on a Startup struct, e.g.:

struct Startup {
  Startup() {
    // If the add op precedence was not defined yet:
    OppMap_t& opp = calculator::Default().opPrecedence;
    opp.add("+", 2);

    // Add the operation function to the default opMap:
    opMap_t& opMap = calculator::Default().opMap;
    opMap.add({MAP, "+", MAP}, &my_complex_sum);
    opMap.add({MAP, "+", NUM}, &my_complex_on_num_sum);
    opMap.add({NUM, "+", MAP}, &my_num_on_complex_sum);
  }
} Startup;

Some notes on this implementation:

  1. You should prefer to declare all your operations in the same startup struct since the order they are defined matters as described on Operation Matching Loop section. Also note that if you want to have multiple startups you will have to choose distinct names for each, e.g. Startup1 and Startup2.

  2. Using the Operator::Reject() exception is useful but should be avoided, since it is inefficient. An option to avoid it is to process all your map operations in a single function, e.g.:

      opMap.add({MAP, ANY_OP, MAP}, &my_map_operations);
      opMap.add({MAP, ANY_OP, NUM}, &my_map_operations);
      opMap.add({NUM, ANY_OP, MAP}, &my_map_operations);

    Then you could use a switch (data->op[0]) { ... } to identify which operator is being used inside the function.

Special meaning attributes

For facilitating the creation of Compound Types there are currently 2 attribute names that are given a special meaning:

  • __type__: Will be used by the global type() function and expects a packToken of type STR.
  • __str__: Will be used by the global str() function and by the packToken::str() function and expects a packToken of type FUNC that returns a string.

The first one is used by the built in function default_type() the second one is used by function packToken_str() both on file builtin-function/functions.h.

Note that if you don't like this feature you can use the approach described on Download and Setuṕ to remove it: copy builtin-features.cpp and the builtin-features directory into your project, modify it by removing the hard-coded references to __type__ and __str__, and then compile it and link to your project. Just remember not to include both the original and the altered versions of builtin-features.o in your project at the same time or the compiler will complain about multiple definitions of the same functions.

Creating a Primitive Data Type

To create a Primitive Data Type it is necessary to use some knowledge of the internal behavior of the calculator, namely the TokenBase*. In this section, we will attempt to ease the learning curve of using this type.

The first step when defining a new primitive type is to choose the C++ structure that will hold this data. For a simple example, let's consider we want to create a new COMPLEX type, to represent complex numerals and perform operations between them. A good way to store this type of data is to keep two real values (i.e. double) one for the real part and one for the imaginary part, e.g.:

struct complex_type {
  double real;
  double img;
};

Then you must make this struct become compatible with the type TokenBase:

#include "shunting-yard.h"
// Make complex_type inherit from TokenBase:
struct complex_type : public TokenBase {
  double real;
  double img;

  // Implementing required virtual function:
  TokenBase* clone() const {
    return new complex_type(*this);
  }
};

Then it will be necessary to (1) choose a uint8_t C++ representation for the type and (2) create a type constructor that will ensure the type is set correctly. To choose the numerical representation it is only necessary to make sure the type does not conflict with the types defined on enum tokType { ... } on the file shunting-yard.h.

Let's choose the numeral 0x24, which does not conflict with the existing ones and is compatible with the convention of keeping the bit 0x20 set for all numeral types:

enum myTypes {
  COMPLEX = 0x24
}

And then implement your type constructor:

struct complex_type : public TokenBase {
  double real;
  double img;
  
  complex_type(double r, double i) : real(r), img(i) {
    this->type = COMPLEX;
  }

  // Implementing required virtual function:
  TokenBase* clone() const {
    return new complex_type(*this);
  }
};

It is also useful to declare a function or macro that attempts to convert a packToken into your type, e.g.:

#include "shunting-yard-exceptions.h"

inline complex_type& asComplex(packToken token) {
  if (token->type != COMPLEX) {
    throw bad_cast("The token `" + token.str() +
                   "` is not of type COMPLEX!");
  }

  // The `packToken::token()` function will
  // return the internal `TokenBase*`:
  return *static_cast<complex_type*>(token.token());
}

Note: Please read the section below on what not to do when working with TokenBase*.

Then finally you have finished declaring your new type. However, you will still need to implement functions and operations to use it.

To exemplify how to do this, let's implement an operator to add two complex numerals:

packToken ComplexAdd(const packToken& p_left,
                     const packToken& p_right, evaluationData* data) {
  complex_type& left = asComplex(p_left);
  complex_type& right = asComplex(p_right);
  return complex_type(left.real + right.real, left.img + right.img);
}

After that, you just need to add these operations to the operations map as described on Defining New Operations page:

struct Startup {
  Startup() {
    OppMap_t& opp = calculator::Default().opPrecedence;
    // This is only necessary if the operator's precedence
    // for "+" was not registered previously. 
    opp.add("+", 2);

    // Link operations to respective operators:
    opMap_t& opMap = calculator::Default().opMap;
    opMap.add({COMPLEX, "+", COMPLEX}, &ComplexAdd);
  }
} Startup;

Teaching packToken::str() how to stringify your type

By default packToken::str() will only work with built-in types. This means that error messages and the default print function will show your type as unknown_type which is not very helpful.

Also, if you try to print your packToken to std::out it will output the same thing, e.g.:

std::cout << packToken(MyType()) << std::endl; // "unknown_type"

It is adivisable to fix this by specializing the behavior of the packToken::str() function for your type, for more info on how to do that please read Stringifying Your Types.

Making it easy to cast to your type

When working with built-in types it is very convenient to be able to cast a packToken to some type, e.g.: packToken(10).asDouble(), however, when working with your own types this would not work.

Fortunately there is an easy way to do implement that so it will work like:

MyType = myPackToken.as<MyType>();

If you want to know more about that please read Casting to Your Types.

What NOT to do when working with TokenBase*

Working with TokenBase* is a little dangerous since you have to concern yourself with memory management. There are some rules that can keep you safe:

  1. Do not delete tokens associated with packTokens:

    TokenBase* my_token = new complex_type(1,2);
    packToken p(my_token); // (When `p` destructs it will delete `my_token`)
    delete my_token; // Will cause a segmentation fault.

    or

    packToken p(new complex_type(1,2));
    delete p.token(); // Will cause a segmentation fault.
  2. If you need you may clone the TokenBase*:

    TokenBase* my_token = new complex_type(1,2);
    packToken p(my_token->clone());
    delete my_token; // It's ok and necessary.

    or

    packToken p(new complex_type(1,2));
    TokenBase* my_clone = p.token()->clone();
    delete my_clone; // It is ok and necessary.
  3. When possible avoid using TokenBase*, prefer instead to convert directly to your type:

    packToken p1(new complex_type(1,2)), p2;
    p2 = p1; // Makes a clone implicitly.
    
    complex_type& c = asComplex(p2.token());
    
    // ... Do whatever you need with c ...
    
    // Make a new packToken with a copy of `c` if you need:
    packToken result(c);

    Also, note that the packToken default constructors should recognize your type, so you can return it as if it was a packToken, e.g.:

    packToken myFunc(TokenMap scope) {
      return complex_type(1,2); // This should work.
    }

One last comment is that while the packToken constructor accepts a TokenBase* it will not work implicitly, i.e.:

packToken myFunc(TokenMap scope) {
  return new complex_type(1,2); // This won't work.
}

The reason for this is to protect you from giving a pointer to packToken by accident. Instead, you must explicitly call the constructor:

packToken myFunc(TokenMap scope) {
  return packToken(new complex_type(1,2)); // This will work.
}

However, you might have noted that just returning the complex_type() instance is easier and has the same effect.