Metasprache fürs Typsystem

ComFreek

Mod | @comfreek
Moderator
TL;DR Funktionen, die auf Typen arbeiten, und induktiv definiert sind:
Javascript:
// Make the function return a Promise, but do not nest Promises
typeop AsyncifyType(T: FunctionType) = match T {
  case (U => Promise<V>) => AsyncifyType(U => V),
  case (U => V) => (U => Promise<V>)
};

function asyncify<T extends Function>(fun: T): AsyncifyType<T> { /* ... */ }

typeop UnwrapPromises(T: type) = match T {
  case Promise<U> => UnwrapPromises(U)
  case U => U  // default branch
};
Promise.resolve<T>(value: T): UnwrapPromises(T) { /* ... */ }

// AsyncifyType kann nun geschrieben werden als:
// case (U => V) => (U => Promise<UnwrapPromises(V)>)

Kennt ihr Programmiersprachen, die das erlauben? Ich vermute fast Haskell ;)
Findet ihr das sinnvoll? Hattet ihr schon mal Anwendungsfälle dafür gehabt?

Meine Anwendungsfälle:

  • Eine Funktion asyncify<U, T>(fun: U => T): U => Promise<T>. In TypeScript leider nicht ganz möglich:
    Javascript:
    // 1. Die Typinfos für die Argumentliste gehen verloren
    // 2. Falls die übergebene Funktion bereits eine Promise zurückgibt, wird sie
    // nun verschachtelt
    function asyncify<T>(fun: (...args: any[]) => T): (...args: any[]) => Promise<T> {}

    Ausführlicher Anwendungsfall: Ich habe ich neulich eine CriticalSection-Klasse in JS implementiert (Repo), die Folgendes kann:
    Javascript:
    class MyClass {
      public async foo(): Promise<void> {
        // Only lock on this instance
        await CriticalSection.for(this).do(async () => {
          // Do something exclusively
        });
      }
    }
    Randbemerkungen: JS ist single-threaded und nicht präemptiv, erlaubt aber das Multiplexen verschiedener Ausführungspfade ('Fiber'), wenn asynchrone Ereignisse im Spiel sind, etwa durch Timeouts (setTimeout), IO-Aufrufe (fs.readFile) oder eben Promises. Der aktuelle Ausführungspfad muss aber aktiv "aufgegeben" werden, indem yield, await oder weiter Konstrukte benutzt werden.

    Auf jeden Fall gefallen mir hier 3 Dinge nicht:
    1. foo muss als 'async' deklariert werden, damit das await-Statement benutzt werden kann. Es ginge zwar auch ohne await, indem die Promise der CriticalSection einfach zurückgegeben wird (return CriticalSection.for...), das sieht aber auch unschön aus. Außerdem könnte foo vorher komplett synchron gewesen sein. Mit der geänderten Signatur wäre es nun erlaubt, Promises zurückzugeben und so schleichend Fehler einzubauen.
    2. Die CriticalSection muss explizit erstellt/als Singleton für die aktuelle Instanz angefordert werden.
    3. Es ist zu viel Boilerplate.

    => Warum nicht Decorators (aktuell noch ein TC39 Stage 2 Proposal) nutzen?

    Es geht und es funktioniert! Nur leider unterstützt TypeScript (noch) nicht, dass der Decorator auch die Methodensignatur ändern kann.

  • Unwrapping von Typen: Promise.resolve überführt einen Wert in eine Promise oder behält die Promise bei, falls es bereits eine war. Wie formuliert man das im Typsystem?
    Promise.resolve<T>(value: T): Promise<T> geht nicht, da gelten sollte: Promise.resolve(Promise.resolve(42)): Promise<number>.
    Promise.resolve<T>(value: T | Promise<T>): Promise<T> geht erstaunlicherweise und ist auch die Version, die TypeScript offiziell für Promise.resolve benutzt.
    Javascript:
    const x = Promise.resolve(Promise.resolve(42));
    Ich vermute, dass der "beste" Match des generischen Typarguments herangezogen wird. Denn folgender Code kompiliert ohne Probleme:
    Javascript:
    const x: Promise<Promise<number>> = Promise.resolve<Promise<number>>(Promise.resolve(42));
    Sollte eigentlich ein Bug sein? :rolleyes:

    Ideen:
    => Promise.resolve<T not extends Promise<arbitrary>>(value: T)
    =>
    Javascript:
    typeop UnwrapPromises(T: type) = match T {
      case Promise<U> => UnwrapPromises(U)
      case U => U  // default branch
    };
    Promise.resolve<T>(value: T): UnwrapPromises(T) { /* ... */ }