Rewriting History

This is an essay in the Herodotus series. Please read at least Introducing Herodotus before proceeding.

The challenge

Herodotus's Io implementation is slowish, complex, and a little clumsy. It is loosely defined, and that fault has extended to Herodotus as a whole, such that it is difficult to describe the system's actions. Especially prone to this complexity are cancellations, which have extremely loose semantics. Accordingly, Herodotus will for now be defined concretely.

Furthermore, it will be necessary with the release of Metaplace to provide Herodotus as a service for worldbuilders. In pursuit of this goal, I have rewritten Herodotus in Erlang, a concurrency-oriented functional programming language. Functional programming's lack of side effects is a perfect fit for the semantics of Herodotus, where a history is defined as a series of transformations(Incidents) on a base state. As a nice bonus, the speed of Erlang means that Histories with tens of thousands of incidents are now quite feasible, even with the relatively few optimizations that have been performed so far(none of which yet include indexing or caching).

The Specification

Herodotus will continue to have only a few concepts and operations at its core. The Thucydides simulation framework will extend these concepts and handle all the logic of causality, as well as paradox detection and prevention.

Herodotus provides:

  • Fluents
    • A Fluent is an {id, selector, persistence, bindings, function, stack} tuple.
      • id uniquely identifies this Fluent.
      • Selectors and Matching
        • A selector is a {fluentType, context, start, end} tuple.
        • Selectors are used primarily for defining and querying fluents.
        • Two selectors "match" if their fluentTypes and contexts match.
          • Two single items match if they are identical, or if either is any.
          • A single item is matched by a list if it matches any element of that list.
          • A list is matched by a single item if that item matches any element.
          • A list is matched by another list if every item in the latter matches an item in the former.
      • Persistence
        • When a Fluent is asked to provide a value after its end-time, it will choose what to do based on its persistence value.
        • If the value is persist, then the value of the Fluent at its end-time will be used.
        • If the value is anything else, then the value will be its persistence value.
        • This provides a mechanism to detect the "death" of specific fluents.
      • Bindings
        • When a Fluent is evaluated, it will be provided a dictionary of bindings.
        • These bindings are configuration for the Fluent, and are guaranteed to be static after the Fluent has been created.
        • This provides a way to provide arbitrary additional data to Fluents at creation time.
      • function is a function of Fluent, Time, and Arguments which returns the value of the Fluent at that time.
        • Fluents are evaluated in forward order by their selectors' set_time.
      • stack is a function of Fluent, Time, Arguments, Stack, Net, and Value which returns the new net value after applying this Fluent's Value.
        • A Stack is a list of {Fluent, Selector, Persistence, Value} tuples in backward order by set_time.
        • After they are evaluated, Fluents are "stacked" together using their respective stack functions, in forward order by time.
  • Cancels
    • A Cancel prematurely terminates one Fluent.
    • It effectively sets an end_time for that Fluent at the Cancel's set_time.
    • Cancels also maintain a unique id.
    • Cancelling Cancels is a little tricky: if Cancel-cancels are to be supported, then so must Cancel-cancel-cancels and so on. Since the semantic is unclear, Herodotus does not define it. There are two approaches recommended for applications that wish to counter the effect of a Cancel:
      • Remove the Incident which causes the Cancel. This is suitable when you don't mind a destructive change. This is the easiest and most correct solution.
      • Add a new Incident that sets a Fluent which continues the function of the old Fluent. This must act differently depending on the old Fluent's persist value. This has a slightly different meaning, in that the period of time between the Cancel and the Cancel-cancel will act as an irregularity in the function.
  • Incidents
    • An Incident is a unit of historical change.
    • A guiding principle is that an Incident is indivisible and immutable. Nothing should ever corrupt an Incident's value, and an Incident being removed should not damage the History in any way.
    • Incidents contain a payload of Fluents and Cancels.
    • Incidents are set at a particular set_time and maintain a unique id.
    • Incidents also mirror the bindings used in their fluents.
    • To aid simulation frameworks or for self-documentation purposes, Incidents can be granted an isa which maps to some Simulation construct. For instance, a particular Instance might have an isa of "birth", while another has "death".
  • Histories
    • A History is a named series of Incidents.
    • Histories can have an Incident added to them. This is called Triggering.
    • Histories can have an existing Incident rolled back.
    • Histories can be queried for the list of Fluents that would match a given Selector, post-Cancellation.
    • Histories can be queried for their value given a Selector and optional additional arguments. These additional arguments will be passed on to the Fluents.
    • Histories can be queried for their Incidents matching a given isa.
  • Herodotus
    • A Herodotus process maintains a set of Histories.
    • Herodotus provides a complete external interface to these Histories, but prevents direct access.
    • Herodotus also provides a Transaction semantic.
      • A Transaction is opened automatically when a set of Dependencies is provided with an Incident to be triggered.
        • A Dependency is a tuple mapping from an input Selector to an output Selector.
      • Whenever such a set of Dependencies intersects the set of Dependencies of an existing Transaction, the new Incident will be considered part of the existing Transaction.
      • Whenever a value or fluent query on a selector which intersects an existing Transaction's identifier is received, the query will use the corresponding inflight transaction data.
        • An exception will be made for "external" queries, i.e. those that come from outside the system. These will ignore the intermediate data, and are the reason that the Transaction system is in place.
      • Transactions can be completed, at which point the "hypothetical" Incidents will be triggered.
      • Transactions can be aborted, at which point the hypothetical Incidents will be thrown away.
      • Transactions can involve the triggering of Incidents across several Histories.
      • Multiple Transactions can be active at once.

Although it isn't short, the above specification does completely specify the behavior of a Herodotus system. In particular, note the absence of the Requirements present in Herodotus/Io. After much deliberation, I decided that Requirements should be removed from base Herodotus and moved into Thucydides. Thucydides will also provide a superior set of tools for expressing causal relationships and the handling of paradoxes.

A complete specification for Thucydides is still pending. The above was a refinement of the existing implementations of and test cases for Herodotus/Io and Herodotus/Erlang. A similar process must be used to develop the specification for Thucydides, or the result will be either over- or under-engineered. Accordingly, the next few essays will express a series of text-based games, each showcasing an increasingly advanced simulation environment. Bartle's Designing Virtual Worlds will be used as a reference for such "interesting" simulation.

Labels: ,

Joe Osborn 2008-03-08

0 Comments:

Post a Comment

<< Home