Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

function createFrog() {

  // Tadpole state, can swim but cannot walk.

  return {
    color: 'black',
    size: 10,
    speed: 5,
    direction: 3.14,
    swim: function () {
      // ...
    }
  };
}

function allowFrogToWalk(frog) {

  frog.walk = function () {  

    // ...

  };
}

function allowFrogToJump(frog) {

  frog.jump = function () {

    // ...

  };
}

function makeFrogToxic(frog) {

  frog.intoxicate = function (animal) {

    // ...

  };
}

let frog = createFrog();

// ...

// Then when the frog reaches a certain size: allowFrogToWalk(frog);

// After the frog spends a certain amount of time on land.

allowFrogToJump(frog);

// If frog eats a certain toxic plant.

makeFrogToxic(frog);

// ...

// Let's say our game has many different kinds of animals and we manage them

// from a single place in the code (and they can interact with each other).

// When an animal tries to go on land:

if (animal.walk) {

  // ...
}

// If animal enounters an obstacle:

if (animal.jump) {

  // ...
}

// When an animal tries to go on water:

if (animal.swim) {

  // ...
}

// When two animals touch each other

if (touches(animalA, animalB)) {

  if (animalA.intoxicate) {

    animalA.intoxicate(animalB);

  }

  // ...
}

Doing this with a statically typed language would add a lot of unnecessary complexity. You'd need to create a lot of types/interfaces:

- Animal (which has all the generic properties of an animal)

- WaterAnimal (which inherits from Animal and can swim but not walk)

- LandAnimal (which inherits from Animal and can walk but not swim)

- Tadpole (inherits from WaterAnimal)

- ToxicTadpole (inherits from tadpole but adds intoxicate method)

- YoungFrog (inherits from WaterAnimal and LandAnimal and is constructed from a Tadpole instance, cannot jump)

- ToxicYoungFrog (inherits from YoungFrog but adds intoxicate method)

- MatureFrog (inherits from WaterAnimal and LandAnimal and is constructed from a YoungFrog instance, has the ability to Jump)

- ToxicMatureFrog (inherits from MatureFrog but adds intoxicate method)

As you add more complex functionality, you either have to support more permutations of different types or you have come up with some kind of complex decorator pattern which subverts the whole purpose of having static types to begin with.



The problem here is you have a complex inheritance hierarchy, right? So don't do that, use composition instead. It's super flexible. E.g. (OCaml):

    let create_frog () = object
      method color = "black"
      method size = 10
      method speed = 5
      method direction = 3.14
      method swim = print_endline "Frog swimming"
    end

    let allow_frog_to_walk frog = object
      method walk = print_endline "Frog walking"
      method super = frog
    end

    let allow_frog_to_jump frog = object
      method jump = print_endline "Frog jumping"
      method super = frog
    end

    let make_frog_toxic frog = object
      method intoxicate animal = print_endline ("Intoxicate" ^ animal)
      method super = frog
    end

    let frog = create_frog ()
    let frog = allow_frog_to_walk frog
    let frog = allow_frog_to_jump frog
    let frog = make_frog_toxic frog


This is cool, but I think the OP wanted to be able to change a given instance's capabilities rather than allocate new wrappers around it. But it wasn't very clear.


Yeah so changing capabilities at runtime is essentially dynamic typing, and here I'm suggesting a (IMHO safer–you don't have to deal with mutation and its weird effects) statically-typed alternative to that.


In this case I would use something like std::function in C++. An std::function can also be unititialized and one can test for that using 'if'. Obviously, one does not want an inheritance hierarchy as complicated as the one you sketch. It does seem like a dynamic language maps a bit more direct to this problem. However, if one uses a dynamic language there still is the complication that there is not a single place with a list of all possible attributes that the Animal can have. This is one main pain of dynamic objects that one especially feels when one is trying to find out how something works. When something can potentially be any type one might have to read a great amount of code to find out what operations are available for that thing.

I also have to notice that this is not a very typical problem. I don't seem to remember that I have ever had to model something like this in a programming language.


"Doctor, it hurts when I do this"

"Don't do that then"

This kind of problem with using heavily inheritance-based systems for behavior can be addressed by using entity component systems. Often found in game development. The general guidance these days is to use "is-a" relationships less and "has-a" relationships more.


Yes but my point is that if you allow an instance of a specific class to either have or not have a specific method/behavior at runtime, then you lose type safety which is the whole point of a dynamically typed language.

The point that I'm trying to make is that true compile-time type safety is not possible in my example without some kind of rigid complex inheritance structure that is predefined at compile time.


Thanks for this spec. Here's a complete, runnable implementation in OCaml:

    type walker = WalkFn of (animal -> unit)
    and  jumper = JumpFn of (animal -> unit)
    and  swimmer = SwimFn of (animal -> unit)
    and  intoxicator = ToxicFn of (animal -> animal -> unit)
    and  animal = Animal of animal_data
    and  animal_data = {
      name: string;
      color: string;
      mutable walk: walker option;
      mutable jump: jumper option;
      mutable swim: swimmer option;
      mutable intoxicate: intoxicator option
    }

    let can_walk = function
      | Animal { walk = Some _ } -> true
      | _ -> false

    let walk animal =
      match animal with
      | Animal { walk = Some (WalkFn walk_fn) } -> walk_fn animal
      | _ -> invalid_arg "this animal can't walk"

    let default_walker = function
      | Animal { name; color } -> Printf.printf "the %s %s walks\n" color name

    let allow_to_walk (Animal data) =
      data.walk <- Some (WalkFn default_walker)

    (* --- more of the same for the other actions --- *)
    let can_jump = function
      | Animal { jump = Some _ } -> true
      | _ -> false

    let jump animal =
      match animal with
      | Animal { jump = Some (JumpFn jump_fn) } -> jump_fn animal
      | _ -> invalid_arg "this animal can't jump"

    let default_jumper = function
      | Animal { name; color } -> Printf.printf "the %s %s jumps\n" color name

    let allow_to_jump (Animal data) =
      data.jump <- Some (JumpFn default_jumper)

    let can_swim = function
      | Animal { swim = Some _ } -> true
      | _ -> false

    let swim animal =
      match animal with
      | Animal { swim = Some (SwimFn swim_fn) } -> swim_fn animal
      | _ -> invalid_arg "this animal can't swim"

    let default_swimmer = function
      | Animal { name; color } -> Printf.printf "the %s %s swims\n" color name

    let allow_to_swim (Animal data) =
      data.swim <- Some (SwimFn default_swimmer)

    let can_intoxicate = function
      | Animal { intoxicate = Some _ } -> true
      | _ -> false

    let intoxicate animal victim =
      match animal with
      | Animal { intoxicate = Some (ToxicFn toxic_fn) } -> toxic_fn animal victim   
      | _ -> invalid_arg "this animal can't intoxicate"
  
    let default_intoxicator animal victim =
      match animal, victim with
      | Animal attacker, Animal victim ->
          Printf.printf "the %s %s poisons the %s %s\n"
            attacker.color attacker.name victim.color victim.name
  
    let allow_to_intoxicate (Animal data) =
      data.intoxicate <- Some (ToxicFn default_intoxicator)
    (* --- end of actions --- *)

    let create_frog () =
      Animal {
        name = "frog";
        color = "black";
        walk = None;
        jump = None;
        swim = None;
        intoxicate = None
      }

    let create_cow () =
      Animal {
        name = "cow";
        color = "green";
        walk = None;
        jump = None;
        swim = None;
        intoxicate = None
      }

    let story () =
      let frog = create_frog () in
      Printf.printf "newborn frog, can it walk? %s\n"
        (if can_walk frog then "yes" else "no");
      allow_to_walk frog;
      Printf.printf "and now, can it walk? %s\n"
        (if can_walk frog then "yes" else "no");
      walk frog;
      allow_to_swim frog;
      swim frog;
      allow_to_jump frog;
      jump frog;
      allow_to_intoxicate frog;
      intoxicate frog (create_cow ());
      Printf.printf "the end\n"

    (* now run all this *)
    let _ = story ()
And here's what it looks like when it's run:

    $ ocaml frog_story.ml 
    newborn frog, can it walk? no
    and now, can it walk? yes
    the black frog walks
    the black frog swims
    the black frog jumps
    the black frog poisons the green cow
    the end
It's true that the animal type must have types and fields for all possible actions an animal might ever take. It would be possible to compartmentalize things a bit more, maybe. But for a quick prototype this should work quite well, I think.


Use an Entity Component System (ECS).




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: