Skip to content

Instantly share code, notes, and snippets.

@aldonline
Last active April 14, 2021 21:28
Show Gist options
  • Save aldonline/4f7af9b787250d81616c41d6316cfe8b to your computer and use it in GitHub Desktop.
Save aldonline/4f7af9b787250d81616c41d6316cfe8b to your computer and use it in GitHub Desktop.
Typed State Machine
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)
})
})
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