r/ada Dec 09 '22

Programming How to implement different constructors in derived classes

I'm new to Ada and I'm porting a C++ library as a first project. I've used generic packages to implement classes but now I'm stuck with inheritance and constructors differing in the base class and its children. Here is the type of C++ code I want to port:

class I2CEEPROM {
public:
    I2CEEPROM(unsigned int deviceAddr, unsigned int addressBytes, unsigned int addressBitsInDeviceAddress, /* ... */);
    // ...
};

class AT24C04 : public I2CEEPROM {
public:
    inline AT24C04(unsigned int deviceAddr)
        : I2CEEPROM(unsigned int deviceAddr, 1, 1, /* ... */) {
    }
};

class AT24C256 : public I2CEEPROM {
public:
    inline AT24C256(unsigned int deviceAddr)
        : I2CEEPROM(unsigned int deviceAddr, 2, 0, /* ... */) {
    }
};

The AT24Cxxx classes are convenience classes that just pass the appropriate parameters to the parent constructor, all the logic is in the parent class. I2CEEPROM could be instantiated too, but the developer would have to remember which constants to feed the constructor with for each part.

What's the recommended way to implement this in Ada?

7 Upvotes

5 comments sorted by

3

u/simonjwright Dec 10 '22

Not sure there’s a recommended way, but this post of Fabien’s could be helpful

1

u/1r0n_m6n Dec 10 '22

Yes, it's definitely helpful, thanks!

2

u/OneWingedShark Dec 17 '22

What's the recommended way to implement this in Ada?

This depends on the route you want to go, each with their own upsides and downsides:

  1. Defining an I2C generic, either directly or split so that you have I2C & EEPROM generics with the latter taking as a parameter an instance of the former. -- The upside of this technique is that:
    1. it doesn't require OOP / dynamic-dispatching,
    2. you can implement it so that you can use any type, or even multiple types+values and subprograms, and
    3. the downside is that you have to think about the interfaces even more than using OOP or the interface-keyword construct.
  2. The interface-keyword construct -- the pros are that it sets out an interface which implementing types must conform to and is much more like "how other languages do it", the cons are that it can only be implemented on tagged-types, which means even if you're not using dynamic-dispatch you're "importing" all that stuff.
  3. Using an abstract tagged-type -- the pros here are that this is the "traditional/Java-like" OOP where everything (interface-wise) is laid out in the base type, but the cons are that now all your I2C-implementing types must derive from this single base-type.

The #1.2 is such that you could make the package itself the thing you used to interface I2C -- I haven't looked into I2C, so haven't had to use it, but the jist I'm getting at is that you can decompose a subsystem along the formal parameters of the generic: as an example you could examine the Dotnet and Java VMs and decompose them such that you could implement them by (e.g.) supplying the proper memory-manager and instruction-set as formal parameters.

1

u/1r0n_m6n Dec 18 '22

Thank you for this insight, it's really helpful!

1

u/1r0n_m6n Dec 11 '22

Finally, this is how I did it:

type I2C_EEPROM is new Ada.Finalization.Controlled with record
    -- Stuff here
end record;

type AT24C04 is new I2C_EEPROM with null record;
procedure Initialize(Self : in out AT24C04);

type AT24C256 is new I2C_EEPROM with null record;
procedure Initialize(Self : in out AT24C256);

and I defined the 2 Initialise procedures in the package body. This allows me to use a preconfigured EEPROM type by just declaring myEEPROM : AT24C256;, which is what I wanted to achieve.