Partake (working title) is a domain-specific programming language for interactive systems such as GUIs and turn-based games. It’s an experiment to see if software can be composed using principles similar to those behind complex board games like Magic: the Gathering, Spirit Island, Dominion, …
Board games have a few properties that are also appealing properties for software:
Briefly, Partake is a logic programming language augmented by a relational I/O model and a simple reified notion of time. A primary goal is to facilitate natural expression of complex choice spaces. Our approach is to rely on relational queries to both specify game situations and the choices available to an actor. An actor is any external entity capable of making choices; this includes players, sources of randomness, and algorithms (Input). Actors observe the game state, which is represented as a set of relational facts (Output).
Partake is intended as a first step towards a comprehensive set of tools for authoring, playing, and modifying interactive software. These tools can be mapped using the model–view–controller terminology:
Model: Partake programs are essentially models in the MVC terminology. They formally define the schema of the application state and how the state can evolve.
View: There is a pattern for relational schemas that lends
itself to simple view definitions: a set of unary entity
predicates are used to distinguish different classes of entity that
should be depicted; unary property predicates are used to apply
temporary styling to entities; and binary relations such as
located
and adjacent
specify basic spatial
relationships. This sort of view might be automatically generated from
an annotated program.
Controller: There are two aspects to a controller: the logical dynamics of choice, and the mundane binding code that links system I/O to those logical choices. Partake programs specify the former. There is also a generic formula for the latter implied by our I/O model: when choices arise, present a menu to the appropriate actor. In practice, most programmers will want to customize this to make choice selection more naturally aligned with the visualization of the game state.
React is a highly popular “immediate mode”-style library for developing interactive GUIs in Javascript. It intermingles display and input, relying on JS as the host language for both. In comparison, Partake is a more opinionated form for interaction design. It benefits from a separate library (such as React) to implement the visualization layer of the application.
Incremental is a library for general incremental computation in OCaml. It extends the incremental DOM evaluation strategy of React to the entire computation. This suggests a certain conceptually simple model of application development: fold updates into a single value that provides the source of truth, then apply various functions to this source to compute the view; rely on incrementalization to make this efficient.
Partake avoids a certain “inversion of control” that may arise when defining GUIs this way. See Semantics Sketch below.
todo
todo
Our prototype implements the basic language components: relational observation, choice, and temporal episodes with concurrency.
Nearly all major languages descend from algorithmic ones and therefore use procedure/function composition as a dominant feature. However, interactions are processes consisting of concurrent actions taken by one or more agents across time, and they call for different composition methods.
A Partake program is a set of rules that communicate via manipulations of relational state. Each rule has three parts: an episode identifier called the trigger, a query, and an episode expression. Here is an example program fragment:
trigger: query Var, !do next-episode.
next-episode: ...
!do
expression then spawns a new episode for each
query result.An episode is an interval of time with local state attached. Episodes can be composed sequentially or concurrently, and can contain sub-episodes. Episodes are meant to reflect a user’s mental model of “what is happening right now”. They provide a scope for local state and a basis for composing rules.
Actor choice points are embedded within episodes, so they arise “in the middle of the action”. This is meant to encourage certain UI practices:
todo explain syntax
# This program implements a turn of a game where
# one or more players draft from a deck and then choose
# a card to play.
# create the players, play areas, and three cards
game: +(
player _, player _,
play-area _,
discard _,
deck D,
card X1, located X1 D,
card X2, located X2 D,
card X3, located X3 D,
),
# each player has a hand and a drafting area
(player P, +(hand P _, choose-area P _)),
!do turn.
# to move `it`, change its location to `to`
move: -(located .it _), +(located .it .to).
# players first grow, then activate cards
turn: !do (grow-stage -> play-stage).
# This query matches every `player`, then spawns concurrent
# episodes for their `grow -> play` turn sequence.
# Actions within this sequence can be arbitrarily interleaved:
# for instance, we might deal three cards to each player
# and only then make the choice of which to take.
grow-stage: player P, !do [(grow -> play) | the-player P].
# to grow, dealer deals cards, then player chooses one of three
grow: !do (deal-cards -> choose-card).
# nb: the `deal-cards` episodes ought to execute serially
# to avoid invalid states.
deal-cards:
'rand chooses ~3 (located C .deck),
!do [move | it C, to .the-player.choose-area].
# choose 1, move rest to discard
choose-card: the-player P,
('player chooses 1 (located C P.choose-area),
!do [move | it C, to P.hand]),
(located C' P.choose-area,
!do [move | it C', to .discard]).
# player moves one card from hand to the play area
play:
'player chooses ~1 (located C .the-player.hand),
!do [move | it C, to .play-area].
play-stage: located C .play-area, !do [activate | the-card C].
# todo: activate
Here’s a partial screenshot of its evaluation in action: .