Skip to content

Commit

Permalink
feat: Version 1.0.0-alpha.4
Browse files Browse the repository at this point in the history
A complete rewrite of how inputs work.

BREAKING CHANGE: The StateMachine class now recognises the distinction between State and Input. A
second generic argument is need (`StateMachine<State, InputSource>`) that defined the layout of the
input source. The `@input` decorator must now decorate fields on the input source itself. The state
object passed to condition and actions is now a union of State & Input, but the return value from
actions can only be a partial State.
  • Loading branch information
voodooattack committed Sep 26, 2018
1 parent 37e82e3 commit 452ec70
Show file tree
Hide file tree
Showing 11 changed files with 503 additions and 367 deletions.
56 changes: 35 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,19 +131,19 @@ The decorators are completely optional, and the currently proposed ones are:

Action decorators may only precede a `when` block, and will only apply to that block.

- `@name('action_name')` Associate a name with an action to be make it possible for inhibitors to reference it elsewhere. Can only be used once per action.
- `@name(action_name)` Associate a name with an action to be make it possible for inhibitors to reference it elsewhere. Can only be used once per action.

- `@unless(expression)` Prevents this action from triggering if `expression` evaluates to true. Can be used multiple times with the same action.

- `@inhibitedBy('action_name')` Prevents this action from triggering if another by `action_name` will execute during this tick. Can be used multiple times with the same action and different inhibitors.
- `@inhibitedBy(action_name)` Prevents this action from triggering if another by `action_name` will execute during this tick. Can be used multiple times with the same action and different inhibitors.

- `@priority(number)` Sets a numeric priority for the action. This will influence the order of evaluation inside the main loop. Actions with higher priority values are evaluated last, meaning that they will take precedence if there's a conflict from multiple actions trying to update the same variable during the same tick.

##### Control decorators:

- `@forever()` Must be defined at the very beginning of the program, and tells the state machine not to halt due to inactivity. In this case, the machine must explicitly end its execution via a call to `exit()`. Accepts no arguments.

- `@input('name', policy?: 'once'|'always'|function)` Implementation dependent. Defaults to `once`. Must precede a constant/readonly variable declaration. Tells `when` to poll an external value and record its value as part of the state. The interpretation of what an input is depends on the implementation. It can be a command-line argument, a memory address, or an hardware interrupt. The `policy` argument specifies how frequently the polling is done: `once` is exactly once at startup, `always` is once per `tick`. `function` is user-defined function that implements custom logic and returns a boolean.
- `@input(policy?: 'once'|'always'|function, input_name?)` Implementation dependent. Defaults to `once`. Must precede a constant/readonly variable declaration. Tells `when` to poll an external value and record its value as part of the state. The interpretation of what an input is depends on the implementation. It can be a command-line argument, a memory address, or an hardware interrupt. The `policy` argument specifies how frequently the polling is done: `once` is exactly once at startup, `always` is once per `tick`. `function` is user-defined function that implements custom logic and returns a boolean.

##### Examples

Expand Down Expand Up @@ -236,16 +236,16 @@ See the [API documentation](https://voodooattack.github.io/when-ts/) for more in

Some examples are located in in [examples/](examples).

- Simple example:
#### Simple example:

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

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

class TestMachine extends EventMachine<State> {
class TestMachine extends StateMachine<State> {
constructor() {
super({ value: 0 }); // pass the initial state to the event machine
}
Expand Down Expand Up @@ -274,47 +274,61 @@ const result = test.run(); // this will block until the machine exits, unlike `.
console.log('state machine exits with:', result);
```

- The same prime machine from earlier, implemented in TypeScript:
#### Brute-forcing primes

The same prime machine from earlier, implemented in TypeScript. This one uses the `input` feature.

A better implementation exists in [examples/prime.ts](examples/prime.ts)!

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

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

class PrimeMachine extends StateMachine<PrimeState> {
constructor(maxPrimes: number) {
super({ counter: 2, current: 3, primes: [2], maxPrimes });
interface IPrimeInputSource extends MachineInputSource {
readonly maxPrimes: number;
}

class PrimeInputSource implements IPrimeInputSource {
@input('once') // mark as an input that's only read during startup.
public readonly primes: number;
constructor(primes = 10) {
this.primes = primes;
}
}

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

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

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

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

@when<PrimeState>(state => state.primes.length >= state.maxPrimes)
@when<PrimeState, IPrimeInputSource>(state => state.primes.length >= state.maxPrimes)
exitMachine(_, m: StateMachine<PrimeState>) {
m.exit();
}
}

const primeMachine = new PrimeMachine(10);
const inputSource = new PrimeInputSource(10);
const primeMachine = new PrimeMachine(inputSource);

const result = primeMachine.run();

Expand Down
39 changes: 26 additions & 13 deletions examples/prime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,27 @@
* and time spent finding every individual prime.
*/

import { MachineState, StateMachine, when } from '../src';
import { input, MachineState, StateMachine, StateObject, when } from '../src';

interface IPrimeInputSource {
// total number of primes to find in a given run (readonly)
readonly numberOfPrimes: number;
}

class PrimeInputSource implements IPrimeInputSource {
@input<PrimeState, PrimeState>('once')
public readonly numberOfPrimes: number = 10;

constructor(numberOfPrimes: number = 10)
{
this.numberOfPrimes = numberOfPrimes;
}
}

/**
* This state object defines the variables this machine will use for its state.
*/
interface PrimeState extends MachineState {
// total number of primes to find in a given run (readonly)
readonly numberOfPrimes: number;
// the current number being checked in any given `tick`
counter: number;
// number to start counting from
Expand All @@ -31,34 +44,34 @@ interface PrimeState extends MachineState {
* A simple state machine for brute-forcing primes.
*/
class PrimeMachine extends StateMachine<PrimeState> {
constructor(public readonly numberOfPrimes: number) {
constructor(inputSource: PrimeInputSource) {
// pass the initial state to the StateMachine
super({ counter: 2, current: 3, primes: [2], numberOfPrimes, times: [0] });
super({ counter: 2, current: 3, primes: [2], times: [0] }, inputSource);
}

// increment the counter with every tick
@when<PrimeState>(state => state.counter < state.current)
@when<PrimeState, IPrimeInputSource>(state => state.counter < state.current)
// this inhibit cause execution to end when we've found the required number of primes
.unless(state => state.primes.length >= state.numberOfPrimes)
incrementCounterOncePerTick({ counter }: PrimeState) {
incrementCounterOncePerTick({ counter }: StateObject<PrimeState, IPrimeInputSource>) {
return { counter: counter + 1 };
}

// this will only be triggered if the current number fails the prime check
@when<PrimeState>(
@when<PrimeState, IPrimeInputSource>(
state => state.counter < state.current && state.current % state.counter === 0)
.unless(state => state.primes.length >= state.numberOfPrimes)
resetNotPrime({ current }: PrimeState) {
resetNotPrime({ current }: StateObject<PrimeState, IPrimeInputSource>) {
return {
counter: 2, // reset the counter
current: current + 1 // skip this number
};
}

// this will only be triggered when all checks have passed (the number is a confirmed prime)
@when<PrimeState>(state => state.counter === state.current)
@when<PrimeState, IPrimeInputSource>(state => state.counter === state.current)
.unless(state => state.primes.length >= state.numberOfPrimes)
capturePrime({ primes, current, times }: PrimeState, { history }: PrimeMachine) {
capturePrime({ primes, current, times }: StateObject<PrimeState, IPrimeInputSource>, { history }: PrimeMachine) {
return {
counter: 2, // reset the counter
current: current + 1, // increment the target
Expand All @@ -71,13 +84,13 @@ class PrimeMachine extends StateMachine<PrimeState> {
// obtain the supplied count or default to 10
const count = process.argv[2] ? parseInt(process.argv[2], 10) : 10;
// crate an instance of the prime machine
const primeMachine = new PrimeMachine(count);
const primeMachine = new PrimeMachine(new PrimeInputSource(count));
// let it execute to a conclusion
const result = primeMachine.run();

if (result) {
// number of primes
console.log(`N = ${primeMachine.numberOfPrimes}`);
console.log(`N = ${count}`);
// total execution time
console.log(
`O(N) = ${primeMachine.history.tick} ticks`
Expand Down
114 changes: 0 additions & 114 deletions examples/recombination.ts

This file was deleted.

11 changes: 5 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "when-ts",
"version": "1.0.0-alpha.3",
"version": "1.0.0-alpha.4",
"description": "When: A software design pattern for building event-based recombinant state machines.",
"main": "dist/lib/src/index.js",
"types": "dist/types/src/index.d.ts",
Expand Down Expand Up @@ -71,15 +71,14 @@
"keywords": [
"state",
"abstract",
"design-pattern",
"state-machine",
"design pattern",
"state machine",
"typescript",
"decorator",
"event-based",
"gene",
"dna",
"recombination",
"immutable"
"immutable",
"temporal-model"
],
"config": {
"commitizen": {
Expand Down

0 comments on commit 452ec70

Please sign in to comment.