On Tuesday, 18 June 2019 at 09:17:09 UTC, Bart wrote:
I'm new to component based programming. I've read it is an alternative to oop for speed. I don't understand how it is possible to have an alternative to oop and still have oop like behavior(polymorphism) nor how to do this. It seems all the great things oop offers(all the design patterns) would be impossible. The benefits are suppose to better reusability as components are more isolated in their dependencies.

Can someone help me understand this a little better and how I'd go about using it in D? Specifically I'm looking at the pros and cons, what are the real similarities and differences to oop, and how one implements them in D(taking in to account D's capabilities).

Thanks.

I don't know a lot about component-based programming per se. (I might use it all the time, I just don't know the term.)

OOP as implemented by Java, C# etc. use virtual functions and inheritance to compose behaviour, also known as polymorphism. This has a slight overhead at runtime. In like >90% of the cases however, you can deduce statically what override/implementation of a function will be called. In D, we have the means to make that deduction at compile time.

The most simple means is duck typing:

int fun(T)(T instance)
{
    return instance.number();
}

We have a templated function, that is: a function that takes an instance of any type. Think of generics, but more liberal. If we call the function with an object:

class Dice
{
    final int number()
    {
        return 6;
    }
}

fun(new Dice());

the compiler generates a template instance, that is, a `fun` that takes a `Dice` instance as its argument. It will then compile as if you have written the Dice type there yourself:

int fun(Dice instance)
{
    return instance.number();
}

This compiles because the class Dice defines a method called `number`. However, we can put any type in there that defines a number() method. In classic OOP, one would use an interface for this

interface Number
{
    int number();
}

class Dice : Number
{
    int number() { return 6; }
}

Using duck typing, the compiler just checks whether the code compiles given the type. If we have other types that implement number, like a Roulette class, we can simply pass an instance of that class to the function and the compiler figures it out. We don't need to define an interface. For large methods, it can be quite unclear what methods a type need to define in order to be passed in.

int fun(T)(T instance) // Only if T has number() method
{
    // Large amount of code here
    return instance.number();
}

In this example we can only pass in a T if it defines a `number()` method. We annotated it in a comment. However, D can also explicitly check it using traits (https://dlang.org/phobos/std_traits.html) or the nuclear option: __traits(compiles, ...), which checks if a certain expression compiles successfully. Doing it the quick and dirty way, we can explicitly define our desired instance interface:

int fun(T)(T instance)
    if(__traits(compiles, {int x = instance.number()} ))
{
    // Large amount of code here
    return instance.number();
}

We add a constraint to our template: our function is valid for any T for which the number() method returns an integer. This is just the surface, you can read https://github.com/PhilippeSigaud/D-templates-tutorial for a technical introduction.

One thing I really like about this is that we preserve the actual type during the whole function. This means that we can call other functions using a strongly-typed argument or do other things based on what type we get. In my personal project, I need to rewrite a bunch of functions that took one type (Card), but now need to take another, reduced type (SimpleCard) as well. The functions currently output cards grouped in sets. Card has some properties (e.g. isFolded, isShown), and SimpleCard is just the value representation (e.g. spades-4). I can use classic inheritance Card extends SimpleCard, but that means I lose my type info along the way. Using duck typing, my functions work for any Card-like object and preserve the type as well. If I need polymorphism, I can achieve that using normal overload rules, e.g.

void fun(CardLike)(CardLike card)
{
    foo(card);
}

void foo(Card card) {}
void foo(SimpleCard card) {}
void foo(FakedCard card) {}

The classes can be kept small - new behaviour can be glued to classes without having impact on existing code. (I once though about why my pet project was progressing so much faster than regular projects. I figured because I rarely changed code - I just added code in different files and changed one line to activate it.)

Reply via email to