How to express mixed ad-hoc and parametric polymor

2019-08-18 07:45发布

I'm not sure if I'm describing the questing currently in the title. What I'm trying to ask comes from the following requirement.

I'm trying to make an abstract for states of finite state machines and comes up with the following definition (in typescript)

interface IState {
    send<T, E>(message: T, callback?:(event: E)=>void): IState;
}

I'm trying to express that a state of the finite state machine should be able to accept messages and return new state, with an optional callback to handle event s during the transition.

When implementing this interface into concrete states, There is a problem.


For example, I'm trying to make a simple state machine with only two states, LEFT, and RIGHT, with three possible messages go-on, turn-left, turn-right. The following table shows their relations.

enter image description here

The key point is that I want to constrain state LEFT only accept go-on and turn-right messages, while send turn-left to LEFT is expected to be a compile error.

I've tried to implement like the following in typescript 3.4.5.

class Left implements IState {
  send(m: 'go-on', cb?: (e: never) => void): Left;
  send(m: 'turn-right', cb?: (e: never) => void): Right;
  send(m: 'go-on' | 'turn-right', cb?: any) {
    return m === 'go-on' ? new Left() : new Right();
  }
}

class Right implements IState {
  send(m: 'go-on', cb?: (e: never) => void): Right;
  send(m: 'turn-left', cb?: (e: never) => void): Left;
  send(m: 'go-on' | 'turn-left', cb?: any) {
    return m === 'go-on' ? new Right() : new Left();
  }
}

The implementation does not have compile error and auto-complete do work as expected. But since it looks weird, I've asked a question TypeScript function generic can only work for function overload with more than one signatures.

Thanks for the kind replies under that question, I understand assign overloading functions to generic functions is wrong. But then how can I express the general interface of state while keeping specific state accept only desired types of messages?

related GitHub issue


Another abstraction I can come up with is

interface IState<T, E, R extends IState<?, ?, ?>> {
    send(message: T, callback?:(event: E)=>void): R;
}

But the return type is recursive and I don't know what to fill for those three questing marks above.

A simpler version could be

interface IState<T, E> {
    send(message: T, callback?:(event: E)=>void): IState<any, any>;
}

It seems to behave like except the annoying any in the return type.

interface IState {
    send<T, E>(message: T, callback?:(event: E)=>void): IState;
}

I've found an maybe related issue in GitHub about generic value.


Is this question well-defined?

If true, is there a correct solution in those methods list above?

If false, what's the correct solution?

1条回答
Evening l夕情丶
2楼-- · 2019-08-18 08:08

I think the best option is this one interface IState<T, E, R extends IState<?, ?, ?>>. The question marks can be replaced with any we don't really care what the state after is only that it is some state.

interface IState<T, E, R extends IState<any, any, any>> {
    send(message: T, callback?: (event: E) => void): R;
}

class Left implements IState<'go-on', never, Left>, IState<'turn-right', never, Right>{
    send(m: 'go-on', cb?: (e: never) => void): Left;
    send(m: 'turn-right', cb?: (e: never) => void): Right;
    send(m: 'go-on' | 'turn-right', cb?: any) {
        return m === 'go-on' ? new Left() : new Right();
    }
}

class Right implements IState<'go-on', never, Right>, IState<'turn-left', never, Left> {
    send(m: 'go-on', cb?: (e: never) => void): Right;
    send(m: 'turn-left', cb?: (e: never) => void): Left;
    send(m: 'go-on' | 'turn-left', cb?: any) {
        return m === 'go-on' ? new Right() : new Left();
    }
}

let left = new Left();
let left_go_on: Left = left.send("go-on")
let left_turn_right: Right = left.send("turn-right")
left.send("turn-left") // error


let right = new Right();
let right_go_on: Right = right.send("go-on")
let right_turn_right: Left = right.send("turn-left")
right.send("turn-right") // error

Or if you want to only have on interface in the implements clause this also works:

interface IState<T extends [any, any, IState<[any, any, any]>]> {
    send: T extends T  ? ((message: T[0], callback?: (event: T[1]) => void) => T[2]) : never
}

class Left implements IState<['go-on', never, Left] | ['turn-right', never, Right]>{
    send(m: 'go-on', cb?: (e: never) => void): Left;
    send(m: 'turn-right', cb?: (e: never) => void): Right;
    send(m: 'go-on' | 'turn-right', cb?: any) {
        return m === 'go-on' ? new Left() : new Right();
    }
}

class Right implements IState<['go-on', never, Right] | ['turn-left', never, Left]> {
    send(m: 'go-on', cb?: (e: never) => void): Right;
    send(m: 'turn-left', cb?: (e: never) => void): Left;
    send(m: 'go-on' | 'turn-left', cb?: any) {
        return m === 'go-on' ? new Right() : new Left();
    }
}

let left = new Left();
let left_go_on: Left = left.send("go-on")
let left_turn_right: Right = left.send("turn-right")
left.send("turn-left") // error


let right = new Right();
let right_go_on: Right = right.send("go-on")
let right_turn_right: Left = right.send("turn-left")
right.send("turn-right") // error
查看更多
登录 后发表回答