Multiple Class Inheritance with Eolian

The previous tutorial showed you how a new class can be created by inheriting from an already existing base class. Like many other programming languages Eolian also allows inheriting from more than one place at the same time, with certain restrictions.

This tutorial explains the special kinds of classes that Eolian offers to allow trouble-free multiple inheritance: interfaces and mixins.

Prerequisites

The Risks of Multiple Class Inheritance

Allowing a class to have more than one parent has some caveats, such as the well-known diamond problem. Suppose a class Parent has a method, overridden in different ways by its two children Child1 and Child2, then a new class Grandchild inherits from both Child1 and Child2 and calls that method. Which implementation should be actually called?

C++, for example, allows you to create such hierarchies but forces you to specify which implementation you want to use in ambiguous cases. Most languages however solve the diamond problem by imposing restrictions on the classes from which you can inherit. These restrictions remove the possibility of ambiguous hierarchies and result in arguably cleaner code.

Eolian (and other languages) defines two new kinds of classes, interfaces and mixins and imposes some inheritance rules:

  1. You can only inherit from one regular class, which is indicated with the extends keyword.

  2. You can implement as many interfaces as you want, listing them after the implements keyword.

  3. You can include as many mixins as you want, listing them after the implements keyword.

Inherit, implement and include all mean use functionality from a parent class. Using a different word for each kind of parent class helps keep ambiguity to a minimum.

Interfaces are like classes but they only define methods and contain no implementation. Mixins are classes which contain implementations but they cannot be inherited from, only included. Neither one can instantiated on its own: they are meant to be implemented or included.

The following steps will clarify these concepts.

Step One: Copying the Code from the Previous Tutorial

This tutorial builds on top of the previous one (Class Inheritance with Eolian), so begin by creating a copy of all your existing code to a new folder.

Next rename the main file eo_inherit_main.c to eo_multiinherit_main.c to maintain consistency with the name of this tutorial.

Step Two: Merging the Includes

As you have already learned, the header file for each new class needs to be included in each file that uses that class. In this example you are going to add several classes and objects to the hierarchy created in the previous tutorial, which means a lot of new header files need to be tracked.

A common practice to simplify this situation is to include all header files from a new file, then use only the new file. To this end, create a new file called eo_multiinherit.h:

#include "example_rectangle.eo.h"
#include "example_square.eo.h"

Now use:

#include "eo_multiinherit.h"

Do this instead of example_rectangle.eo.h and example_square.eo.h in all implementation files.

You can now build and run your program to check that everything still works as expected.

Step Three: Defining an Interface

Interfaces are like classes but do not contain any implementation, only contain method definitions. When a class implements an interface (another way of saying that the class inherits from the interface) it is agreeing to provide the implementation for the missing methods. In this way, the interface methods can be called on different unrelated classes, as long as they all implement the interface.

You are now going to define a new interface in Eolian, which will be implemented by the Example.Rectangle class. It does not make much sense to use an interface when only one class is implementing it but you'll define more in the next steps.

The interface will be called Example.Shape and will only define the method area() which previously was part of the Example.Rectangle class.

Create the example_shape.eo file and write its contents:

interface Example.Shape {
   methods {
      area {
         params {
         }
         return: int;
      }
   }
}

As you can see the interface is declared with the keyword interface instead of class as you did for the other classes. The rest of the file should already be familiar to you. It contains a methods block which defines only one method called area().

Next, turn this Eolian file into C code with eolian_gen. Note that since interfaces do not contain implementations, you do not need to generate an implementation file i.e. you could use the -gch switch instead of -gchi to avoid generating example_shape.c.

If you did do this, you'd have to include the boilerplate C file example_shape.eo.c in your build instead of the usual implementation file example_shape.c. For consistency, generate the implementation file. It will only contain the boilerplate C file in any case:

eolian_gen -gchi example_shape.eo

Now edit the includes file eo_multiinherit.h and add the newly generated example_shape.eo.h after the other includes:

#include "example_shape.eo.h"

Step Four: Implementing the Interface

Tell Example.Rectangle that it will be implementing the new Example.Shape interface. Open the example_rectangle.eo file, then:

  • Add implements Example.Shape after extends Efl.Object at the top of the file.

  • Remove all lines in the area { ... } block.

  • Add a new block after the closing brace of the methods block:

     implements {
      Example.Shape.area;
     }

This tells Eolian that the Example.Rectangle class is going to implement the area() method from the Example.Shape interface instead of providing its own.

The completed file should look like this:

class Example.Rectangle extends Efl.Object implements Example.Shape {
   methods {
      @property width {
         set {
         }
         get {
         }
         values {
            width: int;
         }
      }
      @property height {
         set {
         }
         get {
         }
         values {
            height: int;
         }
      }
   }
   implements {
      Example.Shape.area;
   }
}

Run eolian_gen again to obtain the C code:

eolian_gen -gchi example_rectangle.eo -I .

Pay careful attention to the -I switch so it can find the new class you are now including.

eolian_gen will add any missing methods to your already existing implementation file example_rectangle.c but it will not remove existing methods which are no longer needed.

This means that in the implementation file you will now find:

  • An empty method where you must put the implementation for Example.Shape.area(): _example_rectangle_example_shape_area().

  • Your previous implementation of the Example.Rectangle.area() method: _example_rectangle_area().

Simply move the one-line implementation (return pd->width * pd->height;) from the old method to the new one, then completely remove the old one.

It might look like not much has changed but the area method is now available through an interface, which means that all classes implementing it (including Example.Rectangle and its descendants) can be handled in a similar manner.

Step Five: Using the Interface

Since you removed the area() method from Example.Rectangle, the main program will not work anymore. However since Example.Rectangle now implements the Example.Shape interface, you can use its methods, including area().

Replace both uses of example_rectangle_area() with example_shape_area().

NOTE: At this point the _rectangle_create() and _square_create() methods can return their respective types or an Example_Shape * (or even an Eo * as seen in the Introduction to Eo tutorial).

Now build and run the program. Remember to include the new source file example_shape.c:

gcc eo_multiinherit_main.c example_rectangle.c example_square.c example_shape.c `pkg-config --cflags --libs elementary`

Upon execution your terminal should display the same message as at the end of the previous tutorial:

Rectangle is 5x10, area is 50
Square is 7x7, area is 49

Step Six: Adding Another Class

In order to demonstrate the usefulness of the interface once and for all, you'll now add another class unrelated to Example.Rectangle or Example.Square but implementing the same Example.Shape interface.

Start by creating a new Eolian file called example_circle.eo:

class Example.Circle extends Efl.Object implements Example.Shape {
   methods {
      @property radius {
         set {
         }
         get {
         }
         values {
            radius: int;
         }
      }
   }
   implements {
      Example.Shape.area;
   }
}

This looks a lot like example_rectangle.eo but it defines a class named Example.Circle, which has a radius property (instead of width and height). It also implements the area() method from the Example.Shape interface.

Now turn this file into C code with eolian_gen:

eolian_gen -gchi example_circle.eo -I .

Add the new header file example_circle.eo.h to eo_multiinherit.h:

#include "example_circle.eo.h"

Next add the implementation for the new class. Edit example_circle.c then:

  • Replace the automatically generated #include "example_circle.eo.h" with #include "eo_multiinherit.h"

  • Inside the Example_Circle_Data structure add a variable to hold the radius of the circle: int radius;

  • Add the implementation for the setter: pd->radius = radius;

  • Add the implementation for the getter: return pd->radius;

  • Add the implementation for the area() interface method: return (int)(pd->radius * pd->radius * 3.14159f);

After adding some EINA_UNUSED to the unused method parameters the implementation file should look like this:

#define EFL_BETA_API_SUPPORT
#include <Eo.h>
#include "eo_multiinherit.h"
 
typedef struct
{
   int radius;
} Example_Circle_Data;
 
EOLIAN static void
_example_circle_radius_set(Eo *obj EINA_UNUSED, Example_Circle_Data *pd, int radius)
{
   pd->radius = radius;
}
 
EOLIAN static int
_example_circle_radius_get(const Eo *obj EINA_UNUSED , Example_Circle_Data *pd)
{
   return pd->radius;
}
 
EOLIAN static int
_example_circle_example_shape_area(Eo *obj EINA_UNUSED, Example_Circle_Data *pd)
{
   return (int)(pd->radius * pd->radius * 3.14159f);
}
 
#include "example_circle.eo.c"

The next step instantiates this new class to check that everything works as expected.

Step Seven: Using the New Class

Start by adding the method that will instantiate the new class. You should already be familiar with all the required bits:

Example_Circle *
_circle_create()
{
   Example_Circle *circle;
 
   circle = efl_new(EXAMPLE_CIRCLE_CLASS,
                    efl_name_set(efl_added, "Circle"),
                    example_circle_radius_set(efl_added, 5));
 
   return circle;
}

You'll now make use of the fact that all instantiated shapes implement the Example.Shape interface and remove the duplicated printf() calls. Create a new method that will take care of printing:

void
_shape_print(Example_Shape *shape)
{
   printf("Shape named %s has area %d\n",
          efl_name_get(shape), example_shape_area(shape));
}

Notice how it receives an Example_Shape * and uses methods from that interface (and from Efl_Object, like efl_name_get()). This method will not be able to print the width and height of the rectangles as before since it now receives shapes which might not be rectangular.

Next replace the entire content of efl_main():

   Eo *shape;
 
   shape = _circle_create();
   _shape_print(shape);
   efl_unref(shape);
 
   shape = _rectangle_create();
   _shape_print(shape);
   efl_unref(shape);
 
   shape = _square_create();
   _shape_print(shape);
   efl_unref(shape);
 
   efl_exit(0);

As you can see handling the different shapes is now much simpler through the common interface.

Now build and run the program (remember to include example_circle.c in the mix):

gcc eo_multiinherit_main.c example_rectangle.c example_square.c example_shape.c example_circle.c `pkg-config --cflags --libs elementary`

You should see the following in the terminal:

Shape named Circle has area 78
Shape named Rectangle has area 50
Shape named Square has area 49

This concludes the part regarding interfaces. The next steps demonstrate usage of mixins.

Step Eight: Defining a Mixin

Mixins are meant to provide units of functionality. They're ideally small and independent of any other class. You can add new functionality to your class by including different mixins.

You cannot inherit from a mixin however, meaning that they cannot appear after the extends keyword, only after implements. This ensures that the ambiguous diamond pattern described in the introduction to this tutorial does not happen.

You're now going to create a mixin called Example.Colored providing coloring facilities to your classes. Any class including this mixin will have a color property added to it complete with setter and getter. Your class won't be modified since the mixin provides all the implementation.

You will only be adding the mixin to some of the classes, to show how you can mix and match classes and mixins to suit your needs.

Start by creating yet another Eo file: example_colored.eo:

mixin Example.Colored {
   methods {
      @property color {
         get {
         }
         set {
         }
         values {
            red: int;
            green: int;
            blue: int;
         }
      }
   }
}

As you can see it looks like a regular class named Example.Colored providing a single property called color composed of red, green and blue integer values. There's nothing remarkable besides the fact that the keyword mixin is used instead of class.

Turn into C code using the usual eolian_gen command:

eolian_gen -gchi example_colored.eo

Edit the includes file eo_multiinherit.h as before and add the newly generated example_colored.eo.h after the other includes:

#include "example_colored.eo.h"

Step Nine: Implementing the Mixin

The implementation of this mixin is simple, just a standard setter and getter for the property. Open example_colored.c then:

  • Replace the #include "example_colored.eo.h with #include "eo_multiinherit.h"

  • Add int red, green, blue; inside the structure Example_Colored_Data.

  • Implement the setter as: pd->red = red; pd->green = green; pd->blue = blue;

  • Implement the getter as: *red = pd->red; *green = pd->green; *blue = pd->blue;. Add checks for NULL pointers for added security.

The full example_colored.c file should look like this:

#define EFL_BETA_API_SUPPORT
#include <Eo.h>
#include "eo_multiinherit.h"
 
typedef struct
{
   int red, green, blue;
} Example_Colored_Data;
 
EOLIAN static void
_example_colored_color_set(Eo *obj EINA_UNUSED, Example_Colored_Data *pd,
                           int red, int green, int blue)
{
   pd->red = red;
   pd->green = green;
   pd->blue = blue;
}
 
EOLIAN static void
_example_colored_color_get(const Eo *obj EINA_UNUSED, Example_Colored_Data *pd,
                           int *red, int *green, int *blue)
{
   if (red)
     *red = pd->red;
   if (green)
     *green = pd->green;
   if (blue)
     *blue = pd->blue;
}
 
#include "example_colored.eo.c"

Step Ten: Including the Mixin

Modify example_rectangle.eo to include the Example.Colored Mixin. There's nothing to do beyond adding the Mixin name in the inheritance list:

class Example.Rectangle extends Efl.Object implements Example.Shape, Example.Colored {
    [...]
}

The Mixin provides a new property to the class, and takes care of its implementation. Example.Rectangle (and its derived class Example.Square) can start using this new property without further work. You can now generate the C files as usual:

eolian_gen -gchi example_circle.eo -I .

Step 11: Using the Mixin

Modify eo_multiinherit_main.c to make use of this new functionality. There's little to change.

Add a new configuration call to example_colored_color_set() in the creation of the rectangle:

   rectangle = efl_new(EXAMPLE_RECTANGLE_CLASS,
                       efl_name_set(efl_added, "Rectangle"),
                       example_rectangle_width_set(efl_added, 5),
                       example_rectangle_height_set(efl_added, 10),
                       example_colored_color_set(efl_added, 255, 0, 0));

Do the same for for the creation of the square:

   square = efl_new(EXAMPLE_SQUARE_CLASS,
                    efl_name_set(efl_added, "Square"),
                    example_rectangle_width_set(efl_added, 7),
                    example_colored_color_set(efl_added, 64, 64, 64));

There's no need to modify the circle creation since you didn't include the mixin in Example.Circle.

Finish off by printing some additional information in _shape_print() in case the requested shape includes the Example.Colored mixin. Right after the previous printf() call:

   if (efl_isa(shape, EXAMPLE_COLORED_MIXIN))
     {
        int red, green, blue;
 
        example_colored_color_get(shape, &red, &green, &blue);
        printf("  Colored %d, %d, %d\n", red, green, blue);
     }

efl_isa() checks if the Efl.Object passed in the first parameter contains the class specified in the second parameter in its inheritance tree. In other words it returns true if any of the ancestors of the object are of the given class or implement the given interface or include the given mixin. If that is the case, you will print the color information using the color property.

If you do not perform the efl_isa() check you will be assuming that all shapes contain the color property, which means the program will display an error at runtime.

Build your program, including the additional example_colored.c file:

gcc eo_multiinherit_main.c example_rectangle.c example_square.c example_shape.c example_circle.c example_colored.c `pkg-config --cflags --libs elementary`

Upon running, you should see this in your terminal:

Shape named Circle has area 78
Shape named Rectangle has area 50
  Colored 255, 0, 0
Shape named Square has area 49
  Colored 64, 64, 64

As you can see, only the rectangle and the square contain color information since only the rectangle includes the Example.Colored mixin (and the square inherited it).

Summary

In this tutorial you've learned:

  • multiple inheritance is possible with Eolian, with some restrictions.

  • Only one class can be inherited from, but multiple interfaces can be implemented and multiple mixins can be included.

  • How to create interfaces and implement them in other classes.

  • How to create mixins and include them in other classes.

  • How to find if an object has a given class in its inheritance tree using efl_isa().

This tutorial concludes the series about Eolian.

Further Reading

Class Inheritance with Eolian
Introduces class inheritance with Eolian.