5

I have a tuple where the types relate to each other. In my case it's an extractor function that extracts a value that is in turn used as input to another function.

Conceptually what I'm looking for is something like this, but this doesn't compile:

const a: <T>[(v:any) => T, (t:T) => void] = [ ... ]

The use case is this. I have an incoming RPC message of type any, and an API with well known argument types. I want to build a "wiring plan" that takes two arguments, one extractor function and the corresponding API function.

export interface API = {
    saveModel : (model:Model)    => Promise<boolean>,
    getModel  : (modelID:string) => Promise<Model>,
}

const api: API = { ... }

// this is the tuple type where i'd like to define that
// there's a relation between the second and third member
// of the tuple.
type WirePlan = [[string, (msg:any) => T, (t:T) => Promise<any>]]

const wirePlan: WirePlan = [[
  ['saveModel', (msg:any) => <Model>msg.model   , api.saveModel],
  ['getModel' , (msg:any) => <string>msg.modelID, api.getModel],
]

const handleMessage = (msg) => {
  const handler = wirePlan.find((w) => w[0] === msg.name)
  const extractedValue = handler[1](msg)
  return handler[2](extractedValue)
}

I can work around the issue in other ways, it just struck me there may be something about tuples I haven't understood.

4
  • 1
    Posting imaginary syntax isn't very helpful - can you show how you would use this a value, i.e. examples which you'd like to be legal and examples which you'd like to be illegal? Commented Sep 12, 2017 at 20:48
  • Ok. I flesh it out with a concrete example. Commented Sep 12, 2017 at 20:49
  • Not sure I understand the question but is this might be helpfull? type getSet<T> = [()=>T,(val:T)=>T] Commented Sep 12, 2017 at 20:58
  • @ThomasDevries that could be a way. Commented Sep 12, 2017 at 21:05

1 Answer 1

6

Conceptually what I'm looking for is something like this, but this doesn't compile:

const a: <T>[(v:any) => T, (t:T) => void] = [ ... ]

That is, in fact, the opposite of what you want. Drawing on the intuition of function types, a: <T>(t: T) => T means you have a function that works for all types. This is a universal quantifier: the implementation of a doesn't know what T is; the user of a can set T to whatever they want. Doing this for your tuple would be disastrous, as the inner functions need to output values of T no matter what T is, and therefore the only thing they can do is error out/loop forever/be bottom in some way or another (they must return never).

You want existential quantification. a: ∃T. [(v:any) => T, (t:T) => void] means that a has some type T associated with it. The implementation of a knows what it is and can do whatever it likes with it, but the user of a now knows nothing about it. In effect, it reverses the roles when compared to universal quantification. TypeScript doesn't have support for existential types (not even in a super basic form like Java's wildcards), but it can be simulated:

type WirePlanEntry = <R>(user: <T>(name: string, reader: (msg: any) => T, action: (t: T) => Promise<any>)) => R
type WirePlan = WirePlanEntry[]

Yes, that is a mouthful. It can be decomposed to:

// Use universal quantification for the base type
type WirePlanEntry<T> = [string, (msg: any) => T, (t: T) => Promise<any>]
// A WirePlanEntryConsumer<R> takes WirePlanEntry<T> for any T, and outputs R
type WirePlanEntryConsumer<R> = <T>(plan: WirePlanEntry<T>) => R
// This consumer consumer consumes a consumer by giving it a `WirePlanEntry<T>`
// The type of an `EWirePlanEntry` doesn't give away what that `T` is, so now we have
// a `WirePlanEntry` of some unknown type `T` being passed to a consumer.
// This is the essence of existential quantification.
type EWirePlanEntry = <R>(consumer: WirePlanEntryConsumer<R>) => R
// this is an application of the fact that the statement
// "there exists a T for which the statement P(T) is true"
// implies that
// "not for every T is the statement P(T) false"

// Convert one way
function existentialize<T>(e: WirePlanEntry<T>): EWirePlanEntry {
  return <R>(consumer: WirePlanEntryConsumer<R>) => consumer(e)
}

// Convert the other way
function lift<R>(consumer: WirePlanEntryConsumer<R>): (e: EWirePlanEntry) => R {
  return (plan: EWirePlanEntry) => plan(consumer)
}

Consuming EWirePlanEntry looks like

plan(<T>(eT: WirePlanEntry<T>) => ...)
// without types
plan(eT => ...)

but if you just have consumers like

function consume<T>(plan: WirePlanEntry<T>): R // R is not a function of T

you'll use them like

plan(consume) // Backwards!
lift(consume)(plan) // Forwards!

Now, though, you can have producers. The simplest such producer has already been written: existentialize.

Here's the rest of your code:

type WirePlan = EWirePlanEntry[]
const wirePlan: WirePlan = [
  existentialize(['saveModel', (msg:any) => <Model>msg.model   , api.saveModel]),
  existentialize(['getModel' , (msg:any) => <string>msg.modelID, api.getModel ]),
]

const handleMessage = (msg: any) => {
  let entry = wirePlan.find(lift((w) => w[0] === msg.name))
  if(entry) {
    entry(handler => {
      const extractedValue = handler[1](msg)
      return handler[2](extractedValue)
    })
  }
}

In Action

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.