Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I type parameterize a tuple?

Tags:

typescript

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.

like image 366
Martin Algesten Avatar asked Sep 12 '17 20:09

Martin Algesten


People also ask

Is tuple value type c#?

Tuple types are value types; tuple elements are public fields. That makes tuples mutable value types. The tuples feature requires the System. ValueTuple type and related generic types (for example, System.

When to use tuple c#?

Tuples can be used in the following scenarios: When you want to return multiple values from a method without using ref or out parameters. When you want to pass multiple values to a method through a single parameter. When you want to hold a database record or some values temporarily without creating a separate class.


1 Answers

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

like image 139
HTNW Avatar answered Oct 03 '22 18:10

HTNW