Skip to content

Commit

Permalink
First commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
voodooattack committed Sep 19, 2018
0 parents commit d4e3049
Show file tree
Hide file tree
Showing 21 changed files with 7,309 additions and 0 deletions.
13 changes: 13 additions & 0 deletions .editorconfig
@@ -0,0 +1,13 @@
root = true

[*]
indent_style = space
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 100
indent_size = 2

[*.md]
trim_trailing_whitespace = false
13 changes: 13 additions & 0 deletions .gitignore
@@ -0,0 +1,13 @@
*.log
npm-debug.log*
node_modules
coverage
.nyc_output
.DS_Store
.vscode
.idea
dist
compiled
.awcache
.rpt2_cache
docs
14 changes: 14 additions & 0 deletions .npmignore
@@ -0,0 +1,14 @@
src/
tests/**.ts
dist/lib/tests/**.*
node_modules
*.log
npm-debug.log*
coverage
.nyc_output
.DS_Store
.vscode
.idea
compiled
.awcache
.rpt2_cache
35 changes: 35 additions & 0 deletions .travis.yml
@@ -0,0 +1,35 @@
language: nodejs

node_js: 8.12.0

before_script:
- npm install
- npm run build

script:
- npm run test
- npm run test:coveralls

after_success:
- npm pack

deploy:
- provider: pages
skip_cleanup: true
local_dir: docs/
github_token: $GITHUB_TOKEN
on:
tags: true
- provider: releases
api_key: $GITHUB_TOKEN
file_glob: true
file: "{when-ts}-*.tgz"
skip_cleanup: true
on:
tags: true
- provider: npm
email: voodooattack@hotmail.com
api_key: $NPM_TOKEN
on:
tags: true
repo: voodooattack/when-ts
19 changes: 19 additions & 0 deletions LICENSE
@@ -0,0 +1,19 @@
Copyright (c) 2018 Abdullah A. Hassan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
281 changes: 281 additions & 0 deletions README.md
@@ -0,0 +1,281 @@
# When: TypeScript Reference Implementation
##### A software design pattern for building event-based recombinant state machines

[![npm](https://img.shields.io/npm/v/when-ts.svg)](https://www.npmjs.com/package/when-ts)
[![GitHub license](https://img.shields.io/github/license/voodooattack/when-ts.svg)](https://github.com/voodooattack/when-ts/blob/master/LICENSE)
[![GitHub issues](https://img.shields.io/github/issues/voodooattack/when-ts.svg)](https://github.com/voodooattack/when-ts/issues)
[![Build Status](https://travis-ci.org/voodooattack/when-ts.svg?branch=master)](https://travis-ci.org/voodooattack/when-ts) [![Coverage Status](https://coveralls.io/repos/github/voodooattack/when-ts/badge.svg)](https://coveralls.io/github/voodooattack/when-ts)
![npm type definitions](https://img.shields.io/npm/types/when-ts.svg)

### Introduction

This is a reference implementation for a new software design pattern that allows for composable even-based state machines with complete (including temporal) control over their state.

Please note that this spec and reference implementation are still in alpha and the specs are not yet final.

#### Features:

- Discrete: if your actions only deal with the state object, then every state transition is 100% predictable.
- Temporal: time can be rewound at any given moment (tick) by default, and the state machine will transition to a previously known state in time, along with any future information in the form of an optional state mutation to apply.
- Recombinant: the pattern is based on [gene expression](https://en.wikipedia.org/wiki/Gene_expression), and since state machines are composed of events (`condition -> behaviour` pairs) that are quite similar to how real genes are theorised to work (`activation region -> coding region`), this means that genetic recombination can be applied to `when` state machines by transferring new events from one machine to another. Mutating the machine (DNA) by transferring events (genes) from one machine to the other will introduce new behaviour.

#### Possible Proposals

Here are some possible expansions on the idea. These require further discussion before they're mature enough to include:

- Inhibitors that can suppress an action: these would inhibit a certain event and prevent it from triggering. Alternatively, the possibility for an action to disable/inhibit another action during a tick could be introduced.
- Sexual reproduction of state machines: possible use of a similar mechanic to the one used in organic cells to combine two different programs (DNA) by randomly selecting an equal half of each.
- Mutation: Possible, but difficult since we can't swap code like basepairs. The simplest possible mutation would be a random swap of conditions between two randomly selected actions.

This would all lead to more emergent behaviour in agents produced by recombination.

#### Pattern

*The following is a description of the pattern itself, and not this specific implementation.*

This pattern itself is completely generic and can be implemented in any programming language available today with varying degrees of adaptability potential, depending on the features of the target language.

##### Program state

A `MachineState` consists of user-defined variables and is passed to every condition and action independently.

An external tick counter (`history.tick`) exists and can be considered part of the state (but is not included inside the state object). It is a special variable that is automatically incremented with every new tick. Can be used to reference discrete points in time.

##### Main loop

The goal of the main loop is to move execution forward by mutating the current `state`.

To do this, `when` implements a loop that constantly evaluates a set of rules (`program`). Every iteration of this loop is called a `tick`, when a condition evaluates to `true`, the `action` associated with the condition is evaluated. `actions` can return a partial `MachineState` object containing new variable values for the next `state`, or they can return nothing.

Note that the main loop will always pass an immutable copy of the previous state to actions. Any new mutations caused by actions will only appear during the next `tick`. This is to prevent interactions between different `actions` during the same `tick`.

If multiple actions try to modify the same variable during the same `tick`, the last `action` to execute takes precedence.

The main loop will abort by default if no conditions evaluate to `true` during a single `tick`. This prevents the program from running forever.

#### State Manager

- A State Manager (`history`) is accessible from events. It is responsible ofr managing an array of previous states (`history.records`), in which states are recorded as the program advances.

- A state machine can exit by calling `exit()` from any event, the returned value is the last recorded state. A single argument can be passed to `exit()` to override the returned state.

- Events can use `history.tick` to access the current tick counter.

- Events can access the last recorded states from `history.currentState`.

- Events can access the next state being actively mutated by the current tick through the read-only property `history.nextState`.

- The state can be rewound to a previously recorded state using the `history.rewind(n)` method. `history.rewind(2)` will cause the program to rewind by two full ticks (the tick counter will be decremented as needed). If this occurs inside an event handler, further events will not be processed.

- `history.rewind` accepts a second parameter with optional variable to pass after rewinding to the past state, `history.rewind(2, { backToTheFuture: true })` will rewind and mutate the past state by setting the variable `backToTheFuture` to `true`.

- State history can be erased at any time using `history.clear();`.

- State recording can be configured or disabled at any time by manipulating `history.limit`.

- Setting a finite `limit` during startup is strongly advised. `history.limit` defaults to `Infinity`.

**Examples of `limit`:**

- `history.limit = Infinity;` Record an infinite amount of state. (This is the default, which may cause memory issues if your state objects are very big and/or your program stays running for a long time)

- `history.limit = 4;` Only record the most recent 4 states. Discards any stored older states.

- `history.limit = 0;` No further state recording allowed, and acts the same as `history.limit = 1`. Discards any older history, and `history.record` will only show the previous state.

#### Note on Recombination

This is not part of the current spec, but is currently offered by the TypeScript reference implementation. You can combine any two machines by calling `machine1.recombine(machine2)`, see the [TypeScript API documentation](https://voodooattack.github.io/when-ts/) for more details.

#### Abstract Syntax

Here are some abstract syntax examples for a full pseudo-language based on this pattern. In this theoretical language, the program itself is a state machine, variables of the `MachineState` are global variables, and all of the primitives described above are part of the language itself.

You can read about the original idea (slightly outdated) [in this proposal](https://gist.github.com/voodooattack/ccb1d18112720a8de5be660dbb80541c).

This is mostly pseudo-javascript with two extra `when` and `exit` keywords.

##### Examples

- A prime number generator:

```javascript
let counter = 2; // starting counting up from 2
let current = 3; // start looking at 3
let primes = []; // array to store saved primes

// increment the counter with every tick till we hit the potential prime
when(counter < current) {
counter++;
}

// not a prime number, reset and increment current search
when(counter < current && current % counter === 0) {
counter = 2;
current++;
}

// if this is ever triggered, then we're dealing with a prime.
when(counter >= current) {
// save the prime
primes.push(current);
// print it to the console
console.log(current);
// reset the variables and look for the next one
counter = 2;
current++;
}

// exit when we've found enough primes
when(primes.length >= 10) {
exit();
}
```

Predicted exit state after `exit`:
```json
{
counter: 2,
current: 30,
primes: [ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 ]
}
```

### Installation

You need to install `reflect-metadata` in your project.

`npm install when-ts reflect-metadata`

Additionally, you must add the following to your project's `tsconfig.json` for the TypeScript decorator to work:

```json
{
/* Experimental Options */
/* Enables experimental support for ES7 decorators. */
"experimentalDecorators": true,
/* Enables experimental support for emitting type metadata for decorators. */
"emitDecoratorMetadata": true
}
```

### API

See the [API documentation](https://voodooattack.github.io/when-ts/) for more information.

### Usage

- Simple example:

```typescript
import { EventMachine, when } from 'when-ts';

type State = { // the state of our program
value: number; // a counter that will be incremented once per tick
}

class TestMachine extends EventMachine<State> {
constructor() {
super({ value: 0 }); // pass the initial state to the event machine
}

@when(true) // define a condition for this block to execute, in this case always
reportOncePerTick(s: State, m: TestMachine) {
console.log(`beginning tick #${m.history.tick} with state`, s);
}

@when(state => state.value < 5) // this only executes when `value` is less than 5
incrementOncePerTick(s: State) { // increment `value` once per tick
return { value: s.value + 1 };
}

@when(state => state.value >= 5) // this will only execute when `value` is >= 5
exitWhenDone(s: State, m: TestMachine) {
console.log(`finished on tick #${m.history.tick}, exiting`, s);
if (m.history.tick >= 5)
m.exit(); // exit the state machine
}
}

const test = new TestMachine();

const result = test.run(); // this does will block until the machine exits, unlike `.step()`

console.log('state machine exits with:', result);
```

- The same prime machine from earlier, implemented in TypeScript:

```typescript
import { StateMachine, when, MachineState } from 'when-ts';

interface PrimeState extends MachineState {
counter: number;
current: number;
primes: number[];
}

class PrimeMachine extends StateMachine<PrimeState> {
constructor() {
super({ counter: 2, current: 3, primes: [2] });
}

@when(state => state.counter < state.current)
incrementCounterOncePerTick({ counter }: PrimeState) {
return { counter: counter + 1 };
}

@when(state => state.counter < state.current && state.current % state.counter === 0)
resetNotPrime({ counter, primes, current }: PrimeState) {
return { counter: 2, current: current + 1 };
}

@when(state => state.counter >= state.current)
capturePrime({ counter, primes, current }: PrimeState) {
return { counter: 2, current: current + 1, primes: [...primes, current] };
}

@when(state => state.primes.length >= 10)
exitMachine() {
this.exit();
}
}

const primeMachine = new PrimeMachine();

const result = primeMachine.run();

if (result)
console.log(result!.primes);

```

### Contributions

All contributions and pull requests are welcome.

If you have something to suggest or an idea you'd like to discuss, then please submit an issue or a pull request.

Please make sure that test coverage does not drop below the set limits in `package.json`.

### License (MIT)

Copyright (c) 2018 Abdullah A. Hassan

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

0 comments on commit d4e3049

Please sign in to comment.