evincarofautumn / hap-hs

Haskell [re]implementation of Hap, a simple event-based programming language.
MIT License
12 stars 2 forks source link

Type and object system #2

Open evincarofautumn opened 4 years ago

evincarofautumn commented 4 years ago

I think program organisation will not scale well to larger sizes without some form of type checking. I think some subset of the following features make sense:

While every value should have an intrinsic type, it should be possible to give variables a dynamic type. For dynamically typed values, it should be possible to check or assert that they have the correct type, providing static type information to the type checker.

Typechecking the program should produce warnings about type errors without preventing the program from running. A type mismatch at runtime should produce an error and pause the program for debugging. In strict mode, these warnings can be turned into errors.

It should be possible to define objects (“entities”), as a form of property that provides some combination of fields, commands, and queries. I would like entities to declare the fields they expect or provide, with optional defaults; define methods for commands and queries and events for how they update, render, or respond to input; and be automatically linked together. Sketch:

entity Position {
  has x: int;
  has y: int;
}

entity Velocity {
  has vx: int = 0;
  has vy: int = 0;
  needs x: int;
  needs y: int;
  whenever (updating) {
    x += vx * dt;
    y += vy * dt;
  }
}

entity Sprite {
  has frame: int = 0;
  needs image: Image? = null;
  needs x: int;
  needs y: int;
  whenever (rendering) {
    graphics_draw_image(self.image, self.x, self.y);
  }
}

entity ArrowKeyVelocityControls {
  needs velocity: Velocity;
  whenever (key_down <> null) {
    if (key_down = LEFT_ARROW) { self.velocity.vx = -1; }
    // …
  }
}

entity Player {
  has image: Image;
  has sprite: Sprite;
  has position: Position;
  has controls: ArrowKeyVelocityControls;
}

var player = new Player {
  image: player_sprite,
  position.x: screen_width / 2,
  position.y: screen_height / 2
};
evincarofautumn commented 4 years ago

An approach to game prototyping software that I liked very much was ProjectFUN, developed at DigiPen. It offered a visual state machine editor, which let you attach code to each state (running continuously) and upon exiting or entering via a transition (executed each time the transition was taken). You had to write the code itself in C++, which I feel was a uniquely poor choice for a programming tool targeted at beginners, but the general design—concurrent state machines with code attached, hooked up to a game API—was excellent in concept. I’d like to consider borrowing that structure, although not necessarily the visual editor part.

That might look like adding language constructs for states and transitions to entities, which desugar to as long as loops for ongoing state code and when events for transition code (which also change the active state, explicitly or implicitly).

// Parameterised entities
entity switch (
  turned on,
  turned off,
  remaining on,
  remaining off
) {

  // Instance variables, as above
  has is on : flag;

  // States, equivalent to e.g.:
  // as long as (this: state = on) { … }
  state on {
    remaining on ();

    // Phrase transitions as ‘when’?
    when (not (this: is on)) {
      turned off ();
      // this: state <- off;
      go to (off);
    }
    // Or, desugaring to the above:
    go to (off) when (not (this: is on)) {
      turned off ();
    }

  }
  state off {
    remaining off ();
    when (this: is on) {
      turned on ();
      go to (on);
    }
  }

  // Read/write commands (as in “command/query separation”)
  comand toggle () {
    this: is on <- not (this: is on);
  }
  command turn on () {
    this: is on <- true;
  }
  command turn off () {
    this: is on <- false;
  }

  // Read-only queries
  query is on () {
    return (this: is on);
  }
  query is off () {
    return (not (this: is on));
  }

}

I think machines and entities can be merged, but it may be useful to differentiate them. Is it desirable to let an entity contain multiple machines? The equivalent here would be an entity that has or needs other entities, each with their own state. That may be cumbersome, but also encourages separation of concerns and detangling of states, in a language that needs as much of that as possible. How about no machines? Not all entities need to be stateful, and many shouldn’t be. Do we differentiate entities that constitute models (data), views (read-only rendering), and controllers (updating)?

evincarofautumn commented 4 years ago

Since Hap has a lot of concurrency and shared mutable state going on, it might be helpful to borrow some of the type system ideas from LLL to at least advise on how to avoid races and deadlocks, even if it doesn’t necessarily enforce that.

LLL prevents races by avoiding sharing of linear resources, which isn’t really suitable for Hap because the point is sharing, but maybe it can be controlled at a finer granularity. To help track down logic errors with events, it could keep track of which global variables a function can access, to deduce which code might modify an object or induce an event to happen. Deadlocks are ruled out by LLL’s interpretation of ⅋/fork: the programmer can fork a pair of tasks that execute concurrently, where one must join the other by sending a message on their shared one-shot channel, meaning that tasks form a tree without any cycles. That seems like a fine primitive to add, and potentially very useful for wrangling down the complexity of writing & modifying nontrivial Hap programs.

New task sends, old task receives:

var channel = async {
  var sent value = …;
  send (channel, sent value)
};
…
var received value = receive (channel);

Vice versa:

var channel = async {
  var sent value = …;
  var received value = receive (channel);
};
…
send (channel, sent value)