General problem I'm having in D with the type system

IntegratedDimensions IntegratedDimensions at gmail.com
Sun May 27 23:05:39 UTC 2018


On Sunday, 27 May 2018 at 21:16:46 UTC, Neia Neutuladh wrote:
> On Sunday, 27 May 2018 at 20:50:14 UTC, IntegratedDimensions 
> wrote:
>> The only problem where it can leak is when we treat an cat as 
>> an animal then put in dog food in to the animal, which is 
>> valid when cat as treated as an animal, then cast back to cat. 
>> Now cat has dog food, which is invalid.
>
> It sounds like you don't want to have a `food` setter in the 
> `Animal` base class. Instead, you want setters in each child 
> class that take the specific type required, and possibly an 
> abstract getter in the base class. You could use a mixin to 
> ease the process of defining appropriate food types, and you 
> could have a method that takes the base `Food` class and does 
> runtime validation.
>
> You might also change `Animal` to `Animal!(TFood : Food)` and 
> go from there. You'd likely need to extract another base class 
> or interface so you can have a collection of arbitrary animals.

The problem with all this is that it is not the correct way. For 
N types you have to scale: Animal(Food, Skin, Eyes, ...).

While not having a specific setter in the Animal class does solve 
the problem of preventing assignment and sorta solve the problem 
by requiring assignment to occur at the proper object it does not 
solve the general problem. If we create an animal we should we 
able to assign it animal food(rather than tools). In a sense this 
goes a bit too far. Remember, a type hierarchy could be much more 
complex and we can have types of types which will then all have 
to follow these "workarounds" resulting if a very complex mess.

life
    domain
       kingdom
          phylum
             class
                order
                   family
                      genus
                         species
                            ...
Within each of these types there are specializations

the human species is a type of species in the homo genus, etc.

Now, how can we model such a thing be providing maximum compile 
time typing structure?

class life; // Sorta like object, something that can be anything 
and contains only the common structure to all things that can be 
considered living

class domain : life;
...

class species : genus;
class human : species;

Now, this is all standard. But all types will *use* other types. 
Humans will use, for example, tools. So we will have another 
complex hierarchy where tools fall somewhere

class tool : object;
class hammer : tool;


so

class human : species
{
     tool[] t;
}


Now, suppose we have bob the house builder, a human,

class houseBuilder : humanWorker;

in which we could add a tool to bob

auto bob = new houseBuilder();
bob.tools ~= new hammer();

All fine an dandy!


This is because tool there is not a natural transformation 
between human and houseBuilder and tool and hammer. There a 
hammer being derived from a tool in no way corresponds to a 
houseBuilder being derived from a human. The "uses" in this case 
is from types to types and objects to objects (humans use tools 
and bob uses a hammer).

For the structure I am talking about, "parallel inheritance" 
there is a natural relationship between types to types and types 
to types that naturally transform in "parallel".

To understand this we need to think about something that 
parallels our taxonomy so that as we inherit through one there is 
a *natural* inheritance through the other and a sort of 
"correspondence"(the natural transformation) that keeps 
everything aligned.  Sorta like a ladder where we can only move 
up and down but each end of a rung.

Life         ->    A
Domain       ->    B
...          ->    ...
Genus        ->    X
Species      ->    Y
Human        ->    Z
houseBuilder ->    _

Now, if your paying attention, Human is actually not part of the 
taxonomy above, it is a specialization of Species.

Unfortunately in D we only have one level of typing rather that 
types of types. We only have a type. The taxonomy above would be 
a type of a type of an object while humans would be a type of 
object. So what happens is the different conceptualizations are 
conflated. Since types can be treated as sets, this is like 
saying that sets of sets are the same as sets. Well, sets of sets 
are sets but not all sets are sets of sets, hence they are not 
exactly the same(it is inclusion rather than equality).

{1,2,3} is not a set of sets. (although, it is true we can treat 
1,2,3 as sets and so we could say it is but but I don't want to 
get in to the sets of things that are not sets problem).

So, really what we have is that the taxonomy is precisely this 
paralleling that goes on:

Life         ->    Life
Domain       ->    Eukaryote
...          ->    ...                    ...           ...
Genus        ->    Homo          _>       ...           ...
Species      ->    Human         _>   houseBuilder  --> bob   --> 
hammer



So, for example, Eukaryote's have cells that build things and 
different ways to class them. Hence "Builders" would have have a 
hierarchy of things that build stuff(A heart cell, like bob, 
builds a heart using a "hammer").

The point with all this is that somethings have natural 
transformations(by design) and some things don't but 
unfortunately in D we must represent it all with classes since we 
have no way to specify any higher order type. A human is 
contained within the type Species but it it is not a sub type of 
species. There is a huge difference. This is why we say "The 
*Human* Species" and not the "Genus Species"(If human where a sub 
type of species then species would be a sub type of genus and we 
could say it and it would make sense). Similarly, bob is of type 
human but house Builder is not but bob is a type of houseBuilder.

There are all kinds of *types* of relationships and they are 
generally confused and conflated. Depending on the problem one 
can get away with it or finagle code to make things work.

The only way D can handle this problem is by keeping class 
hierarchies distinct which is what most people are able to do 
naturally anyways. Even though classes are classes, if they are 
unrelated it is sort of like grouping them in to different type 
sets.

The problem comes from when they are not completely unrelated. In 
D we only have the ability to include as an "element" an object(a 
field), its type(the field type), and template parameters. This 
type of dependency, inclusion, is very open. It really allows 
just about anything to be included as long as the type matches. 
We can even make it more free or less by using template 
parameters:

class A(T)
{
     T a;
}

here A depends on any type T, which makes A more general than 
A!int. Of course, we must always specify T at some point so 
ultimately A is just as restrictive as  A!int, so we only gain 
syntactic sugar using template parameters(everything that can be 
done with template parameters can be done without).

We can do have some tools to restrict T though using constraints:

class A(T)
if (T is int)
{
     T a;
}

and this helps quite a bit since we could do

class A(T)
if (is(T : Q))
{
    T a;
}


but even with all this we can't do something very simple:

class A
{
    T a;
}

class C : A
{
    TT : T a;
}


We can't specify parallel restrictions.

If C inherent from A then TT must inherent from T.

Sure we can use template parameters for a small number of 
inclusions but the code bloat grows potentially exponentially and 
does this really solve the problem?

class bit;

class ebit : bit
{
     bit[8] data;
}

class number
{
    bit[] a;
}

class byte : number
{
    (ebit : bit)[] a;
}

every byte is a number and every ebit is a bit(using the 
composite pattern). They are separate "classes" of type 
relationships;


number      bit

byte        ebit


We can always go up the ladder. A double is a number and an ebit 
is a bit.

We cannot always go down the ladder. Not all numbers are byte and 
not all bits are ebits.

We, though mix and match in one way. A number can use an 
ebit(since it is just a bit). The only case that fails is when a 
byte, treated as a number, uses only a bit. This failure case is 
no different than trying to treat a bit as an ebit or a number as 
a double when they are not.

What the above does is essentially expand every bit to be 8 bits. 
This would scale all number by a factor of 8 bits and 7 of those 
would not be used when treated as a number.


Now, if we can't specify such a nice parallel relation we are 
stuck using bit rather than ebit when ebit is a more natural type 
to use as it relates to byte.

What you should realize is that the failing condition is the same 
condition that fails in general, trying to upcast an object in 
the wrong type. This is always the problem and is no different 
here. We just have two parallel type hierarchies that are 
naturally related and so the up casting is more complex as it 
must deal with both sides(or many sides).

For multiple inclusions the process is just as simple. If we are 
up casting n type hierarchies then each type must be able to up 
cast. Hence for N inclusions there are N+1 potential ways to fail:

class A
{
    T1 a;
    T2 b;
    T3 c;
}

class C : A
{
    TT1 : T1 a;
    TT2 : T2 b;
    TT3 : T3 c;

}


and when casting from an A to a C, it may be that any combination 
of a,b,c contain invalid types which can't be cast. Although, 
with proper design everything should fail together or not fail at 
all.

All this is really no different than the single object case. You 
can always down cast but can only upcast if consistency is 
preserved. Here we just have to check several cases and deal with 
the slight complexity that having multiple cases gives. In the 
singular case we either get the object or null. In the multiple 
case we get an N tuple case that is either the object or null. 
For C above we get the possibilities (C | null, TT1 | null, TT2 | 
null, TT3 | null).















More information about the Digitalmars-d mailing list