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

Nick Sabalausky (Abscissa) SeeWebsiteToContactMe at semitwist.com
Mon May 28 03:31:07 UTC 2018


On 05/27/2018 04:50 PM, IntegratedDimensions wrote:
> On Sunday, 27 May 2018 at 18:16:25 UTC, JN wrote:
>> On Sunday, 27 May 2018 at 06:00:30 UTC, IntegratedDimensions wrote:
>>> animal a = new cat();
>>>
>>> a.f = new food()
>>> auto c = cast(cat)a;
>>>
>>>
>>> as now f in cat will be food rather than catfood.
>>
>> I think the problem is in your hierarchy. If Animal can have Food, 
>> that means that any animal should be able to accept any food, without 
>> knowing what kind of food is this. Cat requiring cat food is a leaky 
>> abstraction, the cat shouldn't know nor care what kind of food it 
>> gets, as it's an animal and it will eat any food.
> 
> This is clearly false.
> 
> A Kola does not eat any type of food, nor does a whale or most animals.
> 

Exactly. That's why your hierarchy is wrong. If any Animal COULD eat any 
food, THEN you would want the Animal class to contain a Food object. 
Clearly that's not the case. Clearly, any Animal CANNOT eat any food, 
therefore the Animal class should not contain a Food object. At least, I 
think that's what JN was saying.

Of course, removing Food from the Animal class *does* cause other 
problems: You can no longer use OOP polymorphism to tell any arbitrary 
Animal to eat.

(Part of the problem here is that using base classes inherently involves 
type erasure, which then makes a mess of things.)

Honestly though, the real problem here is using class hierarchies for 
this at all.

Yea, I know, OOP was long held up as a holy grail. The one right way to 
do everything. But it turned out class hierarchies have a lot of 
problems. And you're hitting exactly one such problem: There's many 
modelling scenarios they just don't fit particularly well. (And this 
isn't a D problem, this is just OOP class hierarchies in general.)

D has plenty of other good tools, so it's become more and more common to 
just avoid all that class inheritance. Instead of getting polymorphism 
from class inheritance, get it from templates and/or delegates. Things 
tend to work better that way: it's more flexible AND gives better type 
safety because it doesn't necessitate type erasure.

This does involve approaching things a little bit differently: Instead 
of thinking/modelling in terms of nouns, think more in terms of verbs. 
It's all about what you're *doing*, not what you're modelling. And 
prefer designing things using composition ("has a") over inheritance 
("is a").

So, regarding the Animal/Food/Cat/CatFood example, here is how I would 
approach it:

What's something our program needs to do? For one, it needs to feed an 
animal:

     void feedAnimal(...) {...}

But in order to do that, it needs an animal and a food:

     void feedAnimal(Animal animal, Food food) {
         animal.eat(food);
     }

That's still incomplete. What exactly are these Animal and Food types? 
We haven't made them yet.

There are different kinds of animal and different kinds of food. We have 
two main options for handling different kinds of Animal and Food, each 
with their pros and cons: There can be a single Animal/Food type that 
knows what kind of animal, or each kind of animal/food can be a separate 
type.

A. Single types (many other ways to do this, too):

     struct Animal {
         enum Kind { Cat, Dog, Bird }
         Kind kind;

         // Eating is overridable:
         void delegate(Food) customEat;

         void eat(Food food) {
             enforce(food.kind == this.kind, "Wrong kind of food!");

             if(customEat !is null)
                 customEat(food);
             else
                 writeln("Munch, munch");
         }

         Food preferredFood() {
             return Food(kind);
         }
     }

     struct Food {
         Animal.Kind kind;
     }

     Animal cat() {
         return Animal(Animal.Kind.Cat, (Food f){ writeln("Munch, 
meow"); });
     }
     Animal dog() {
         return Animal(Animal.Kind.Dog, (Food f){ writeln("Munch, 
woof"); });
     }
     Animal bird() {...}

     Animal[] zoo;

B. Separate types (many other ways to do this, too):

     import std.traits : isInstanceOf;
     import std.variant : Variant;

     struct Cat {
         void eat(Food!Cat) { writeln("Munch, meow"); }
     }
     struct Dog {
         void eat(Food!Dog) { writeln("Munch, woof"); }
     }
     struct Bird {...}

     enum isAnimal(T) =
         /+ however you want to determine whether a type is Animal +/

     struct Food(Animal) if(isAnimal!Animal) {}
     enum isFood(T) = isInstanceOf!(Food, T);

     void feedAnimal(Animal, Food)(Animal animal, Food food)
         if(isAnimal!Animal && isFood!Food)
     {
         // Compiler gives error if it's the wrong food:
         animal.eat(food);
     }

     Variant[] zoo;


More information about the Digitalmars-d mailing list