Last active
April 14, 2021 21:28
-
-
Save aldonline/4f7af9b787250d81616c41d6316cfe8b to your computer and use it in GitHub Desktop.
Typed State Machine
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { SM, Clazz } from "./typed_state_machines" | |
interface ITalker { | |
say_hello(name: string): string | |
repeat(message: string) | |
} | |
// the root state is implemented by a class that extends SM (State Machine) | |
class Talker extends SM implements ITalker { | |
say_hello(name: string): string { | |
return "nolang " + name | |
} | |
repeat(message: string) { | |
console.log(message) | |
} | |
to_spanish() { | |
this.sm_enter(Talker_Spanish) | |
} | |
to_angry_spanish() { | |
this.sm_enter(Talker_Spanish_Angry) | |
} | |
to_english() { | |
this.sm_enter(Talker_English) | |
} | |
to_angry_english() { | |
this.sm_enter(Talker_English_Angry) | |
} | |
// there are some lifecycle methods | |
sm_on_enter() { | |
console.log("enter talker") | |
} | |
chillout() {} | |
} | |
// sub-states are sub-classes | |
class Talker_English extends Talker { | |
say_hello(name: string) { | |
return "Hello " + name | |
} | |
sm_on_enter() { | |
console.log("enter english") | |
} | |
} | |
class Talker_English_Angry extends Talker_English { | |
say_hello(name: string) { | |
return super.say_hello(name) + " !" | |
} | |
chillout() { | |
this.sm_enter(Talker_English) | |
} | |
// sm_on_enter(){ console.log("enter angry english") } | |
} | |
class Talker_Spanish extends Talker { | |
say_hello(name: string) { | |
return "Hola " + name | |
} | |
sm_on_enter() { | |
console.log("enter spanish") | |
} | |
} | |
class Talker_Spanish_Angry extends Talker_Spanish { | |
say_hello(name: string) { | |
return super.say_hello(name) + "!" | |
} | |
chillout() { | |
this.sm_enter(Talker_Spanish) | |
} | |
sm_on_enter() { | |
console.log("enter angry spanish") | |
} | |
sm_on_exit() { | |
console.log("exit angry spanish") | |
} | |
} | |
describe.skip("", () => { | |
test("", () => { | |
var aldo = new Talker() | |
expect(aldo.say_hello("Bob")).toEqual("nolang Bob") | |
aldo.to_angry_spanish() // --> Talker_Spanish_Angry | |
expect(aldo.say_hello("Bob")).toEqual("Hola Bob!") | |
aldo.to_english() // --> Talker_English | |
expect(aldo.say_hello("Bob")).toEqual("Hello Bob") | |
aldo.to_angry_english() // --> Talker_English_Angry | |
expect(aldo.say_hello("Bob")).toEqual("Hello Bob!") | |
aldo.chillout() // Talker_English_Angry --> Talker_English | |
expect(aldo.say_hello("Bob")).toEqual("Hello Bob") | |
aldo.to_angry_spanish() // --> Talker_Spanish_Angry | |
expect(aldo.say_hello("Bob")).toEqual("Hola Bob!") | |
aldo.chillout() // Talker_Spanish_Angry --> Talker_Spanish | |
expect(aldo.say_hello("Bob")).toEqual("Hola Bob") | |
}) | |
test("tt", () => { | |
const talker_spanish = Clazz.for_func(Talker_Spanish) | |
expect(talker_spanish.func).toEqual(Talker_Spanish) | |
const talker_spanish_angry = Clazz.for_func(Talker_Spanish_Angry) | |
const talker_english = Clazz.for_func(Talker_English) | |
const talker_english_angry = Clazz.for_func(Talker_English_Angry) | |
const fff = talker_spanish_angry.ancestors().map((a) => a.func) | |
expect(fff).toEqual([SM, Talker, Talker_Spanish]) | |
const fff2 = talker_english_angry.ancestors().map((a) => a.func) | |
expect(fff2).toEqual([SM, Talker, Talker_English]) | |
const lcd1 = talker_spanish.lcd(talker_spanish_angry) | |
expect(lcd1.get().func).toEqual(Talker_Spanish) | |
const path = talker_spanish_angry.lcd_path(talker_english_angry) | |
console.log(path.exits) | |
console.log(path.enters) | |
}) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class Clazz { | |
constructor(public func: Function) {} | |
parent() { | |
try { | |
return Clazz.for_func(this.func.prototype.__proto__.constructor) | |
} catch (e) {} | |
return undefined | |
} | |
ancestors(): Clazz[] { | |
var parent = this.parent() | |
if (parent) return parent.ancestors().concat([this]) | |
return [] | |
} | |
is_state_machine(): boolean { | |
var a = this.ancestors() | |
return a.length > 0 && a[0].func == SM | |
} | |
lcd(other: Clazz): Option<Clazz> { | |
var acs1 = this.ancestors().reverse() | |
var acs2 = other.ancestors().reverse() | |
var res = acs1.find((c1) => acs2.includes(c1))! | |
return new Option<Clazz>(res) | |
} | |
lcd_path<T>(other: Clazz) { | |
var lcd = this.lcd(other) | |
if (lcd.empty()) return { exits: [], enters: [] } | |
var acs1 = this.ancestors().reverse() | |
var acs2 = other.ancestors().reverse() | |
var exits = acs1.slice(0, acs1.indexOf(lcd.get())) | |
var enters = acs2.slice(0, acs2.indexOf(lcd.get())) | |
return { enters: enters.reverse(), exits: exits } | |
} | |
run_enter(context: any) { | |
this._run_f(context, "sm_on_enter") | |
} | |
run_exit(context: any) { | |
this._run_f(context, "sm_on_exit") | |
} | |
private _run_f(context: any, name: string) { | |
var p = this.func.prototype | |
var f = p[name] | |
if (p.hasOwnProperty(name) && typeof f == "function") f.apply(context, null) | |
} | |
override_methods(target: any): void { | |
var prot = this.func.prototype | |
for (const p of Reflect.ownKeys(prot)) { | |
if ( | |
prot.hasOwnProperty(p) && | |
typeof prot[p] == "function" && | |
p != "constructor" | |
) { | |
target[p] = prot[p] | |
} | |
} | |
} | |
static for_func(f: Function): Clazz { | |
var k = "____clazz____" | |
return f[k] ? f[k] : (f[k] = new Clazz(f)) | |
} | |
} | |
export class SM { | |
private __k = "__current_state__" | |
sm_current_state(): Function { | |
if (this[this.__k]) return this[this.__k] | |
return this["constructor"] | |
} | |
// alias | |
sm(state: Function, silent: boolean = false) { | |
this.sm_enter(state, silent) | |
} | |
sm_enter(state: Function, silent: boolean = false) { | |
if (this.sm_current_state() == state) return | |
var from_c = Clazz.for_func(this.sm_current_state()) | |
var to_c = Clazz.for_func(state) | |
this[this.__k] = state | |
var lcs = to_c.lcd(from_c) | |
if (lcs.empty()) | |
throw new Error( | |
"Cannot transition to a class that has no common ancestor" | |
) | |
var path = from_c.lcd_path(to_c) | |
path.exits.forEach((c) => { | |
c.override_methods(this) | |
if (!silent) c.run_exit(this) | |
}) | |
path.enters.forEach((c) => { | |
c.override_methods(this) | |
if (!silent) c.run_enter(this) | |
}) | |
this.sm_changed(from_c.func, to_c.func) | |
} | |
// abstract | |
sm_changed(from: Function, to: Function) { | |
// console.log("changed", from, to ) | |
} | |
sm_on_enter() {} | |
sm_on_exit() {} | |
} | |
class Option<T> { | |
private _empty: boolean = false | |
constructor(private v: T) { | |
this._empty = v === null | |
} | |
empty() { | |
return this._empty | |
} | |
isDefined() { | |
return !this._empty | |
} | |
getOrElse(generator: { (): T }) { | |
return this.empty() ? generator() : this.v | |
} | |
toArray(): T[] { | |
return this.empty() ? <T[]>[] : [this.v] | |
} | |
forEach(f: { (value: T, index?: number, array?: T[]): void }) { | |
this.toArray().forEach(f) | |
} | |
// monadic | |
map<X>(f: { (v: T): X }): Option<X> { | |
return this.empty() ? <Option<X>>(<any>this) : new Option(f(this.v)) | |
} | |
flatMap<X>(f: { (v: T): Option<X> }) { | |
return this.empty() ? new Option<X>(null as any) : f(this.v) | |
} | |
get(): T { | |
if (this.empty()) { | |
throw new Error("Cannot get() from empty option") | |
} | |
return this.v | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment