1unitedpower: Projektvorstellung: MVar - low-level Bibliothek für Nebenläufigkeit

problematische Seite

Moin!

Ich habe seit längerem mal wieder eine Open-Source Bibliothek veröffentlicht. Diesmal geht es um Nebenläufigkeit in JavaScript und TypeScript.

Feedback ist herzlichst willkommen: MVar

Viele Grüße
Raoul

  1. problematische Seite

    Tach!

    Feedback ist herzlichst willkommen: MVar

    Was ich vermisse, ist eine Einleitung, die man auch ohne Vorwissen verstehen kann. Was genau ist das Ziel, was sind (typische) Anwendungsfälle? Damit hat auch eine Suchmaschine mehr Futter für potentielle Verwender, die noch nicht die Fachbegriffe kennen.

    dedlfix.

    1. problematische Seite

      Hallo dedlfix,

      ist ja eigentlich nur einen Klick weiter...

      Rolf

      --
      sumpsi - posui - clusi
      1. problematische Seite

        Tach!

        ist ja eigentlich nur einen Klick weiter...

        Die hab ich auch gelesen, und sie mir war schon zu weit im Thema drin und hat mir nicht offenbart, wofür das nützlich sein könnte.

        dedlfix.

  2. problematische Seite

    Lieber Raoul,

    ich habe nicht kapiert, wofür das Ding gut ist. Offensichtlich bin ich wohl auch nicht die beabsichtigte Zielgruppe. Hängen geblieben sind bei mir diese Blitzlichter:

    • für eine andere Programmiersprache namens Ingrid (oder so ähnlich) geschrieben
    • macht intern irgend etwas, wobei es promises verwendet
    • ist was für Leute, die Compiler in JS bauen
    • ein "Hallo Welt"-Beispielprogramm braucht ganz schön viele Zeilen Code

    Im Übrigen teile ich den ersten Eindruck, den auch @dedlfix schon dargelegt hat, nämlich dass auf der Seite selbst nur eine Bibliothek angepriesen wird, nicht aber, was sie konkret für einen Sinn erfüllt. Dass das auf einer weiteren Seite ausführlich erklärt wird (was ich wie schon erwähnt nicht verstanden habe), ist zwar schön, aber eben einen weiteren Klick entfernt, den ich beim ersten Mal erst gar nicht mehr getätigt habe. Erst durch Rolfs Hinweis habe ich mir die Erklärung überhaupt durchgelesen.

    Liebe Grüße

    Felix Riesterer

  3. problematische Seite

    Hallo 1unitedpower,

    ich habe mir das jetzt mal näher angeschaut, und, nein, ich verstehe den Sinn noch nicht so ganz. Es sei denn, der Zweck ist es, genau zwei parallel laufende Aktionen irgendwie zu syncen.

    Unter Nebenläufigkeit verstehe ich zwei Threads, die parallel laufen und bei dem der eine den Output des anderen konsumiert. Das ergibt in JavaScript oder TypeScript grundsätzlich nicht den meisten Sinn, weil JavaScript per Definition single threaded ist. Das lässt sich mit Workern lösen, die es im Browser und auch in Node.js gibt. Befasst habe ich mich damit noch nicht. Die Implementierungen scheinen ähnlich, aber nicht gleich, und sie sind Event-basiert, nicht Promise-basiert. Ob eine Lib, die Browser und Node abstrahiert, sinnvoll oder nötig ist, weiß ich nicht.

    Deine MVars scheinen aber mit Workern nichts am Hut zu haben, insbesondere kannst Du eine MVar nicht dazu bringen, auf Messages von echten Workern zu lauschen (bzw. dafür müsste man einen eigenen Adapter zimmern).

    Eine andere Form von Nebenläufigkeit sind Generatorfunktionen. Hier ist es aber so, dass der Generator immer erst dann weiterläuft, wenn der Konsument das nächste Element anfordert. Ein Generator, der das nächste Element schonmal in einem Worker ermittelt, während der Konsument das vorige Element noch verarbeitet, das wär was cooles. Kann man das mit MVars realisieren?

    Dein Einsatzbeispiel zeigt, wie man mehrere Eventquellen in einen gemeinsamen Event-Strom steckt (bzw. Actions in deinem Fall), um sie nachher per if wieder auseinanderzuklamüsern. Ja, ich verstehe, das ist eine einfache Demonstration, wie man MVars benutzen kann. Aber ein Beispiel, wo die Motivation klarer ist, wäre schon gut.

    Dein API und deine Implementierung werfen bei mir ebenfalls Fragen auf. Nimm das bitte nicht als "Oh Mann ist das ein Scheiß", sondern einfach als Liste von Unklarkeiten, die ich beim Lesen des Codes empfunden habe.

    • Warum MVar.newEmpty() und MVar.new(x)? Das ist doch ein Konstruktor. Warum machst Du den vorhandenen Konstruktor private und klemmst statische Methoden davor? Das Verpacken in ein Array und das Bereitstellen der leeren TaskQueue kann der Konstruktor auch erledigen.
    • Mir ist unklar, was MVar tut, wenn ich zwei put() mache ohne dass ein take() gelaufen ist. Kann MVar das?
    • deine Actionqueue ist merkwürdig. Warum verwendest Du keine Polymorphie? Statt dessen fragst Du ab, ob das Actionelement read, take oder swap enthält. Deine Actions sind private, also dürfen sie auch private Dinge tun. Siehe meinen Codevorschlag weiter unten.
    • swap() tut nicht was sein Name verspricht. Es tauscht nicht. Es stellt den Austauschwert ans Ende der putQueue, statt den gelesenen Wert zu ersetzen. Der Name passt nicht.
    • Die Promise-Implementierung und die Do-It-Now Implementierung von Swap unterscheiden sich in der Reihenfolge der Teilaktionen. Nach reiflicher Überlegung bin ich zwar drauf gekommen, dass das egal sein sollte, aber besserer Stil wäre, die Reihenfolge gleich zu halten (lese queue, schreibe queue, resolve Promise)
    • Warum wirft tryTake eine Exception? Try-Funktionen haben doch gerade den Sinn, Exceptions zu vermeiden. Und da JavaScript singlethreaded konzipiert ist: Warum gibt's tryTake() überhaupt? Eine Abfolge von isEmpty() und take() ist nicht unterbrechbar, weil Threads nur per Message kommunizieren und es keinen thread-übergreifenden Speicher gibt. Warum also eine künstliche und scheinbar atomare tryTake Methode?
    • Warum heißen die beiden überhaupt take und read? Unter read versteht man normalerweise eine konsumierende Funktion. Eine nichtkonsumierende Lesefunktion sollte peek() heißen. Und weil eine MVar nicht viel anderes als eine Queue ist, wären enqueue() und dequeue() wohl die besseren Namen für put() und take(), denn das sind die bekannten Operationsnamen auf dem ADT Queue.

    Hier nun der Vorschlag für eine Implementierung mit Polymorphie in der put-Methode und weitgehender Abstraktion des Queue-Handlings. Ich habe deinen Stil, Temp-Variablen zu bilden, beibehalten. Man könnte drauf verzichten und etliche Methoden zu Einzeilern machen (und sich dabei Debug-Hürden einfangen). Dafür habe ich es so gemacht, dass die drei Handler-Funktionen sowohl die Promise- als auch die Direktimplementierung bilden. Ich weiß nur nicht, wie man dafür die Typisierung in Typescript angeben muss. Die performAction Methode fiel mir zum Schluss ein, als ich bemerkte, dass read, take und swap nur noch aus Boilerplate-Code bestanden, die den Action-Handler entweder direkt oder via Promise aufriefen.

    Keine Ahnung ob's funktioniert. Einiges an TypeScript-Spezifika wirst Du nachtragen müssen - ich kann Typescript eigentlich gar nicht und habe nur blindlings imitiert, was ich bei Dir gesehen habe.

      public put (y: a): void {
        this.putQueue.push(y)
        while (this.taskQueue.length !== 0 && this.putQueue.length !== 0) {
          const nextTask = this.taskQueue.shift()!
          nextTask();
        }
      }
    
      public read (): Promise<a> {
        return enqueueOrPerformAction( resolve => this.handleReadTask(resolve) )
      }
    
      public take (): Promise<a> {
        return enqueueOrPerformAction( resolve => this.handleTakeTask(resolve) )
      }
    
      public swap (swapValue: a): Promise<a> {
        return enqueueOrPerformAction( resolve => this.handleSwapTask(resolve, swapValue) )
      }
    
      // Bekommt eine Function, die einen Ding vom Typ wie Promise<a>.resolve 
      // erhält und auch zurückgibt. 
      // Gibt selbst ebenfalls so ein Ding zurück.
      private enqueueOrPerformAction(handler: ???) : Promise<a> {
        if (this.putQueue.length === 0) {
          return new Promise<a>(resolve => this.taskQueue.push(_ => handler(resolve)))
        } else {
          return handler(Promise<a>.resolve, data)
        }
      }
    
      private handleReadTask(resolve: ???): ??? {
        const nextValue = this.putQueue[0]!
        return resolve(nextValue)
      }
    
      private handleTakeTask(resolve: ???): ??? {
        const nextValue = this.putQueue.shift()!
        return resolve(nextValue)
      }
    
      private handleSwapTask(resolve: ???, swapValue: a): ??? {
        const nextValue = this.putQueue.shift()!
        this.putQueue.push(swapValue)
        return resolve(nextValue)
      }
    

    Das Handling von Swap finde ich übrigens bemerkenswert komplex. Ich kann auf einer leeren MVar zweimal Swap aufrufen. Beide gehen erstmal in die Taskqueue. Nun schiebe ich einen Wert in die MVar und der Pingpong geht los. Der erste Swap liest den Wert und schreibt einen neuen. Der triggert den anderen Swap, der wiederum den ersten Swap triggert. Da JS je nach Implementierung keine Tail-Rekursion macht, und in deiner Put-Methode wegen der while-Schleife auch keine Tail-Rekursion greifen wird, läuft das Ratzfatz auf einen Stackoverflow.

    Rolf

    --
    sumpsi - posui - clusi
    1. problematische Seite

      Vielen Dank euch Dreien für das Feedback. Es ist auf jedenfall klar geworden, dass da noch Erklärungsbedarf herrscht. Ich überlege mir derzeit noch eine sinnvolle Metapher und Anschungsbeispiele, die die dem Verständnis dienen.

      Und ein ganz besonderer Dank an @Rolf B für den ausührlichen Code-Review.

      ich habe mir das jetzt mal näher angeschaut, und, nein, ich verstehe den Sinn noch nicht so ganz. Es sei denn, der Zweck ist es, genau zwei parallel laufende Aktionen irgendwie zu syncen.

      Vorweg: Die Library beschäftigt sich mit Concurrency, nicht mit Parallelität. Die beiden Themen werden in der Literatur leider häufig miteinander vermengt. Concurrency ist die Eigenschaft eines Programms verschiedene Code-Pfade ungeordnet auszuführen ohne das Endergebnis zu beeinträchtigen. In JavaScript konkurrieren asynchrone Tasks um die Zugriffe auf die geteilten Variablen. Diese konkurrierenden Zugriffe so zu ordnen, dass das finale Ergebnis deterministisch wird, nennt man Synchronisierung. MVar stellt dafür einen primitiven Datentypen bereit.

      Deine MVars scheinen aber mit Workern nichts am Hut zu haben, insbesondere kannst Du eine MVar nicht dazu bringen, auf Messages von echten Workern zu lauschen (bzw. dafür müsste man einen eigenen Adapter zimmern).

      Die Beobachtung ist richtig. Man kann MVars nutzen, um so einen Adapter zu bauen, aber das wäre eine höher geschichtete Aufgabe, und MVar ist gewollt eine low-level Bibliothek.

      Eine andere Form von Nebenläufigkeit sind Generatorfunktionen. Hier ist es aber so, dass der Generator immer erst dann weiterläuft, wenn der Konsument das nächste Element anfordert. Ein Generator, der das nächste Element schonmal in einem Worker ermittelt, während der Konsument das vorige Element noch verarbeitet, das wär was cooles. Kann man das mit MVars realisieren?

      Das kommt drauf an, wo siehst du hier konkurrierende Tasks?

      Dein Einsatzbeispiel zeigt, wie man mehrere Eventquellen in einen gemeinsamen Event-Strom steckt (bzw. Actions in deinem Fall), um sie nachher per if wieder auseinanderzuklamüsern. Ja, ich verstehe, das ist eine einfache Demonstration, wie man MVars benutzen kann. Aber ein Beispiel, wo die Motivation klarer ist, wäre schon gut.

      Danke, ich überlege mir was.

      Dein API und deine Implementierung werfen bei mir ebenfalls Fragen auf. Nimm das bitte nicht als "Oh Mann ist das ein Scheiß", sondern einfach als Liste von Unklarkeiten, die ich beim Lesen des Codes empfunden habe.

      Danke, und keine Sorge, ich weiß deinen Code-Review wirklich zu schätzen.

      • Warum MVar.newEmpty() und MVar.new(x)? Das ist doch ein Konstruktor. Warum machst Du den vorhandenen Konstruktor private und klemmst statische Methoden davor? Das Verpacken in ein Array und das Bereitstellen der leeren TaskQueue kann der Konstruktor auch erledigen.

      Das war eine bewusste Design-Entscheidung, die ich getroffen habe, um die Schnittstelle möglichst ähnlich zu Haskell zu halten. Dort gibt es die beiden Funktionen newMVar und newEmptyMVar. Der Konstruktor ist in Haskell ebenfalls privat, dort ist es eine technische Notwendigkeit.

      • Mir ist unklar, was MVar tut, wenn ich zwei put() mache ohne dass ein take() gelaufen ist. Kann MVar das?

      Die beiden Werte würden in die Queue aufgenommen und sonst würde nichts weiter passieren. Das ist als stellst du dich im Supermakrt vor eine unbesetzte Kasse und irgendwann stellt sich jemand hinter dich.

      • deine Actionqueue ist merkwürdig. Warum verwendest Du keine Polymorphie? Statt dessen fragst Du ab, ob das Actionelement read, take oder swap enthält. Deine Actions sind private, also dürfen sie auch private Dinge tun. Siehe meinen Codevorschlag weiter unten.

      Polymorphie wäre hier mit Kanonen auf Spatzen geschossen. Es gibt genau drei Operationen, die zu beareiten sind. TypeScript kann sicherstellen, dass eine Verzweigung auf einem Wert von Typen Task, alle Fälle erschöpfend abdeckt. Polymoprhie hingegen erlaubt eine unbegrenzte Anzahl an Fällen. Auf deinen Code komme ich später nochmal zurück.

      • swap() tut nicht was sein Name verspricht. Es tauscht nicht. Es stellt den Austauschwert ans Ende der putQueue, statt den gelesenen Wert zu ersetzen. Der Name passt nicht.

      Das ist auch wieder aus Kompatibilität zu Haskell. Die Semantik in Haskell ist, dass Operationen immer nach dem FIFO-Prinzip ausgeführt werden. Eine Swap soll sich verhalten, wie take gefolgt von einem put.

      • Die Promise-Implementierung und die Do-It-Now Implementierung von Swap unterscheiden sich in der Reihenfolge der Teilaktionen. Nach reiflicher Überlegung bin ich zwar drauf gekommen, dass das egal sein sollte, aber besserer Stil wäre, die Reihenfolge gleich zu halten (lese queue, schreibe queue, resolve Promise)

      Stimmt.

      • Warum wirft tryTake eine Exception? Try-Funktionen haben doch gerade den Sinn, Exceptions zu vermeiden.

      Auch wieder aus Haskell-Kompatibilität: Die drei Funktionen können fehlschlagen, in Haskell wird der Erfolg bzw. Misserfolg durch einen Rückgabewert vom Typ Mabye bzw. Bool modelliert. In einem ersten Entwurf hatte ich das auch so implementiert, und bin dann auf Exceptions umgestiegen, weil Maybes in JavaScript eher unbekannt sind. Besonders unschön finde ich, dass tryPut nun einen boolschen Rückgabe-Wert hat und keine Exception schmeißt. Ich habe verschiedene Designs ausprobiert, und bin dann mit diesem verblieben, weil es mir am wenigsten Bauchschmerzen bereitete, nicht weil es besonders elegant wäre.

      Und da JavaScript singlethreaded konzipiert ist: Warum gibt's tryTake() überhaupt? Eine Abfolge von isEmpty() und take() ist nicht unterbrechbar, weil Threads nur per Message kommunizieren und es keinen thread-übergreifenden Speicher gibt. Warum also eine künstliche und scheinbar atomare tryTake Methode?

      Ich sehe für die try-Methoden und isEmpty auch keinen großen Nutzen; das sind Escape-Hatches, und deshalb habe ich sie übernommen. Sie könnten nützlich sein, wenn jemand einen einen Scheduler basierend auf requestAnimiationFrame oder setInterval bauen möchte. GHCJS macht das bswp. so.

      • Warum heißen die beiden überhaupt take und read? Unter read versteht man normalerweise eine konsumierende Funktion. Eine nichtkonsumierende Lesefunktion sollte peek() heißen.

      Dem Wortsinn nach finde ich take und read auch passend. Wenn ich ein Buch aus einem Regal nehme, befindet es sich nicht mehr im Regal. Wenn ich ein Buch lese, dann ist es danach nicht weg. Aber ja, die Kovention mit peek ist mir auch bekannt, ich weiß nicht, warum man sich damals dafür entschieden hat, die Funktion read zu nennen, ob den Autoren die Konvention nicht bekannt war, oder ob sie absichtlich davon abgewichen sind. Für mich spielt das keine Rolle, weil eins meiner Design-Ziele ist möglichst nah bei der Haskell-Nomenklatur zu bleiben.

      Und weil eine MVar nicht viel anderes als eine Queue ist, wären enqueue() und dequeue() wohl die besseren Namen für put() und take(), denn das sind die bekannten Operationsnamen auf dem ADT Queue.

      dequeue verhält sich anders als take, wenn die Queue (respektive die MVar) leer ist. Außerdem ähnelt eine MVar auch anderen Datentypen, bspw. einem Channel, wobei take einem receive und put einem send entspricht. Oder einem Semaphor mit take als wait und put als signal.

      Hier nun der Vorschlag für eine Implementierung mit Polymorphie in der put-Methode und weitgehender Abstraktion des Queue-Handlings. Ich habe deinen Stil, Temp-Variablen zu bilden, beibehalten. Man könnte drauf verzichten und etliche Methoden zu Einzeilern machen (und sich dabei Debug-Hürden einfangen). Dafür habe ich es so gemacht, dass die drei Handler-Funktionen sowohl die Promise- als auch die Direktimplementierung bilden. Ich weiß nur nicht, wie man dafür die Typisierung in Typescript angeben muss. Die performAction Methode fiel mir zum Schluss ein, als ich bemerkte, dass read, take und swap nur noch aus Boilerplate-Code bestanden, die den Action-Handler entweder direkt oder via Promise aufriefen.

      Danke, ich sehe woher der Wind weht. Deine Implementierung folgt auf jedem Fall dem DRY-Prinzip, das finde ich gut. Ob ich das tatsächlich verständlicher finde, weiß ich noch nicht. Ich werde das mal so implementieren und dann nochmal Feedback geben.

      Das Handling von Swap finde ich übrigens bemerkenswert komplex. Ich kann auf einer leeren MVar zweimal Swap aufrufen. Beide gehen erstmal in die Taskqueue. Nun schiebe ich einen Wert in die MVar und der Pingpong geht los. Der erste Swap liest den Wert und schreibt einen neuen. Der triggert den anderen Swap, der wiederum den ersten Swap triggert. Da JS je nach Implementierung keine Tail-Rekursion macht, und in deiner Put-Methode wegen der while-Schleife auch keine Tail-Rekursion greifen wird, läuft das Ratzfatz auf einen Stackoverflow.

      Hmm. Würde es dir was ausmachen, dafür mal einen Testfall bei Stackblitz oder JSFiddle zu programmieren? Ich habe das auf Anhieb nicht nachstellen können, aber das könnte ein Bug in meiner Implementierung sein.

      1. problematische Seite

        Hallo 1unitedpower,

        TypeScript kann sicherstellen, dass eine Verzweigung auf einem Wert von Typen Task, alle Fälle erschöpfend abdeckt.

        Ok, der Else-Fall geht schief wenn vorher nicht N-1 Ausprägungen von Task abgefragt wurden. Polymorphie hat aber noch den weiteren Vorteil, dass Du gar nicht erst abfragen musst, sondern einfach den Task in der Queue machen lässt, was vorbereitet wurde.

        Wenn ich so weiter drüber nachdenke - bei neuen Tasks musst Du mit Polymorphie die Queueverarbeitung nicht ändern. Deine Implementierung ist also nicht sOlid. Man kann von deiner MVar nicht erben, wenn man neue Tasks braucht. Da ist schon die Task-Enumeration falsch, das müsste eigentlich ein Interface sein.

        Die Rekursion habe ich auf dem Spielplatz von typescriptlang.org ausprobiert, da wusste ich wenigstens welche TS Version er nimmt.

        Ich habe sowas gemacht:

        var x = MVar.newEmpty<string>();
        x.swap("bar").then(receiveBar);
        x.swap("hon").then(w => console.log("Got a " + w));
        
        x.put("foo");
        
        function receiveBar(w: string) {
            console.log("receiveBar/ Got a " + w)
            x.swap("bark").then(receiveBar);
        }
        
        function receiveHon(w: string) {
            console.log("receiveHon/ Got a " + w)
            x.swap("honk").then(receiveHon);
        }
        

        und jetzt bin ich froh, dass Christian den Autosave ins Forum gebaut hat, ich musste den Browser abschießen. Dabei habe ich beim zweiten Swap sogar noch vergessen, die receiveHon-Funktion einzusetzen. Drei Chrome-Prozesse liefen damit auf je 33% CPU (bei 4 Kernen eines virtuellen Prozessors).

        Man musste aber definitiv die swap-Aufrufe in die Receiver einbauen, sonst ist nichts passiert. Ich lag also falsch. Wenn die taskQueue leer ist, ist sie leer.

        Dann hab ich noch was gespielt:

        var x = MVar.newEmpty<string>();
        
        x.swap("bar").then(data => receiveData("bar", data));
        x.swap("hon").then(data => receiveData("hon", data));
        x.take().then(data => receiveData("tk1", data));
        x.take().then(data => receiveData("tk2", data));
        x.take().then(data => receiveData("tk3", data));
        
        console.log("pushing data");
        
        x.put("foo");
        x.put("moo");
        x.put("boo");
        x.put("zoo");
        
        function receiveData(at: string, w: string) {
          console.log("receive at " + at + " / got " + w)
        }
        

        Welche Ausgabe sagst Du voraus?

        Was mich ein bisschen fuchst, ist mein Unverständnis von Typescript und auch von async/await. Ich kapier's auch in C# nicht wirklich. Eigentlich sollten async und await doch im Stande sein, Promises komplett zu abstrahieren. Der Gebrauch der Promise-Klasse bedeutet nämlich, dass man keinen Downcompile mehr für ES5 machen kann. Schöner wär's also, wenn man rein mit dem async/await Mechanismus auskäme. Ich krieg nur einen Knoten in's Hirn bei dem Versuch, dann die Auflösung des Promise an die Task-Queue zu delegieren.

        Rolf

        --
        sumpsi - posui - clusi
        1. problematische Seite

          Hallo Ingrolf,

          ok, async/await sind scheinbar nur ein Zückerchen für den then-Teil des Promise. Für den Resolve-Teil muss man eine Promise-Implementierung haben.

          Wobei ich dann nicht verstehe, weshalb TS sich den Wolf generiert, um async/await über einen Generator darzustellen. Oder ich kapier's nach wie vor nicht 😏

          Rolf

          --
          sumpsi - posui - clusi
        2. problematische Seite

          Wenn ich so weiter drüber nachdenke - bei neuen Tasks musst Du mit Polymorphie die Queueverarbeitung nicht ändern. Deine Implementierung ist also nicht sOlid. Man kann von deiner MVar nicht erben, wenn man neue Tasks braucht. Da ist schon die Task-Enumeration falsch, das müsste eigentlich ein Interface sein.

          Ja, damit hast du mich überzeugt. Ich werde das in den nächsten Tagen mal so implementieren. Wenn du möchtest, kann ich dich gerne als Contributor im Repo verlinken. Dein Code-Review ist wirklich enorm hilfreich.

          Man musste aber definitiv die swap-Aufrufe in die Receiver einbauen, sonst ist nichts passiert. Ich lag also falsch. Wenn die taskQueue leer ist, ist sie leer.

          Okay, dann ist das kein Bug, sondern works as intended. Das ist ein Fehlgebrauch der Library. Die Endlos-Rekursion steckt ja nicht im Library-Code, sondern im Anwendungs-Code. Imho. sollte es dazu nicht kommen, wenn der innere swap-Aufruf asynchron stattfindet, das werde ich nochmal testen.

          Dann hab ich noch was gespielt:

          var x = MVar.newEmpty<string>();
          
          x.swap("bar").then(data => receiveData("bar", data));
          x.swap("hon").then(data => receiveData("hon", data));
          x.take().then(data => receiveData("tk1", data));
          x.take().then(data => receiveData("tk2", data));
          x.take().then(data => receiveData("tk3", data));
          
          console.log("pushing data");
          
          x.put("foo");
          x.put("moo");
          x.put("boo");
          x.put("zoo");
          
          function receiveData(at: string, w: string) {
            console.log("receive at " + at + " / got " + w)
          }
          

          Welche Ausgabe sagst Du voraus?

          Vorhergesagt, habe ich folgendes, aber auch weil ich meine Implementierung kenne:

          pushing data
          receive at bar / got foo
          receive at hon / got bar
          receive at tk1 / got hon
          receive at tk2 / got moo
          receive at tk3 / got boo
          

          Du hast vielleicht folgendes erwartet:

          pushing data
          receive at bar / got foo
          receive at hon / got moo
          receive at tk1 / got boo
          receive at tk2 / got zoo
          receive at tk3 / got bar
          

          Das wäre die Ausgabe, wenn man swap durch take und put ersetzt:

          x.take().then(data => { receiveData("bar", data); x.put("bar"); })
          x.take().then(data => { receiveData("hon", data); x.put("hon"); })
          x.take().then(data => receiveData("tk1", data));
          x.take().then(data => receiveData("tk2", data));
          x.take().then(data => receiveData("tk3", data));
          

          Beides macht irgendwie Sinn. Die erste Variante arbeitet streng nach dem FIFO-Prinzip. Die zweite Variante würde take und put in eine Beziehung zu swap setzen. Ich glaube, der zweite Fall wäre tatsächlich schöner. Ich muss mir nochmal anschauen, was Haskell da macht.

          1. problematische Seite

            Hallo 1unitedpower,

            nö, verlinken musst Du mich nicht. Hab ja eh keine präsentationsfähige Homepage :)

            Dein Code-Review ist wirklich enorm hilfreich.

            Danke schön.

            Übrigens fand ich die von Dir vorhergesagte Reihenfolge durchaus logisch und richtig, sie passt zur Semantik, die der Name "Swap" vermittelt. Hätte ich jetzt nach meiner vorigen Kritik zwar nicht gedacht, aber ist so :)

            Rolf

            --
            sumpsi - posui - clusi
        3. problematische Seite

          So, ich hab jetzt eine neue Version gepusht, die schedule-Methode entspricht in etwa deiner enqueueOrPerformAction-Methode.

          Ich habe den internen Typen der Task-Queue auf () => void fixiert. Am Beispiel von take sieht das nun so aus:

          class MVar<a> {
            // ...
          
            private taskQueue: Array<() => void>
          
            private schedule (task: () => a): Promise<a> {
              return new Promise(resolve => {
                if (this.putQueue.length > 0) {
                  resolve(task())
                } else {
                  this.taskQueue.push(() => resolve(task()))
                }
              })
            }
          
            private runTake (): a {
              return this.putQueue.shift()!
            }
          
            public take (): Promise<a> {
              return this.schedule(() => this.runTake())
            }
          
            public tryTake (): a {
              if (this.putQueue.length !== 0) {
                return this.runTake()
              } else {
                throw new MVarEmptyError()
              }
            }
          }
          

          Damit ist der Code jetzt auch DRY. Als kleiner Nebeneffekt ist das UMD-Build jetzt auch nur noch 1.08KB statt 1.12KB groß. Ein Nachteil ist, dass ich jetzt Thunks allozieren muss, das dürfte einen kleinen Runtime-Overhead bedeuten, aber das nehme ich in Kauf.