1 Punkte von GN⁺ 2024-11-14 | 1 Kommentare | Auf WhatsApp teilen
  • Raku ähnelt eher einem Sprachfeature-Labor, das verschiedene Ideen direkt ausprobiert, als einem ausgereiften Einzelentwurf; dadurch lassen sich Experimente, die andere Sprachen übernehmen könnten, an einem Ort betrachten
  • Junctions verteilen Operationen über Mengen möglicher Werte und falten sie dann mit booleschen Quantoren wie all, any oder one, sodass sich komplexe Bedingungen kurz ausdrücken lassen
  • Das * der Whatevers wird für unendliche Bereiche, anonyme Funktionen und längenbasierte Indexierung von Listen verwendet und erzeugt einen knappen, aber ungewohnten Codestil
  • Raku-Reguläre-Ausdrücke verzichten auf PCRE-Kompatibilität und bieten stattdessen komponierbare Grammatiken sowie Komfortfunktionen wie Werttrenner, gut lesbare Lookaheads und standardmäßig ignorierte Leerzeichen
  • Mit Hyperoperators, Pair-Syntax, Slangs, RakuAST und Rakudo Star zeigt sich Raku als Sprache, die von Syntaxexperimenten bis zur Distributionsform breit angelegte Experimente wagt

Warum Raku wie ein Labor für Sprachfeatures wirkt

  • Nach mehr als einem Jahr tatsächlicher Nutzung wirkt Raku eher wie ein „Labor für Sprachfeatures
  • Fünf verschiedene Nebenläufigkeitsmodelle und mehrere Wege, dieselbe Aufgabe zu erledigen, zeigen den experimentellen Charakter: Es geht darum zu sehen, „was passiert“
  • Dass viele Features seltsam miteinander interagieren und zahlreiche Edge Cases haben, liegt auch daran, dass es viel schwieriger ist, 100 Experimente so zu verfeinern, dass sie zusammen funktionieren, als sie einfach nur auszuführen
  • Die hier behandelten Features sind Raku-Experimente, die man irgendwann auch in anderen Programmiersprachen sehen könnte

Junctions: Verschachtelte mögliche Werte und boolesche Quantoren

  • Junctions stehen für Verschachtelungen möglicher Werte; wendet man eine Operation auf eine Junction an, wird dieselbe Operation auf alle enthaltenen Werte angewandt
    • 2|10 wird zu any(2, 10)
    • 2&10 + 3 wird zu all(5, 13)
    • (1&2) + (10^20) wird zu einer Kombination wie all(one(11, 21), one(12, 22))
  • Der zentrale Einsatzzweck von Junctions ist das Ausdrücken boolescher Formeln wie all und any
    • Es gibt keine Möglichkeit, eine Junction zu destrukturieren
    • Um sie praktisch zu verwenden, muss man sie zuerst auf einen booleschen Wert falten
  • so erzwingt die Umwandlung einer Junction in einen booleschen Wert
    • so (1&2) + (10^20) < 15 ist True
    • so (1&2) + (10^20) > 0 ist False
  • Rakus list.all ist keine Methode mit Argumenten, sondern eine parameterlose Methode, die eine Liste in eine Junction umwandelt
    • <1 2 3>.all wird zu all(1, 2, 3)
    • Wendet man is-prime($x) an, wird es auf jeden Wert angewandt, etwa als all(False, True, True)
  • Durch das Kombinieren von Junctions lassen sich auch Verhaltensweisen wie unique bauen
    • „Ist jedes Element gleich genau einem Element?“ lässt sich als {.all == .one} ausdrücken
    • <1 2 3 7> ist wahr, <1 2 3 7 2> ist falsch

Whatevers: Mit * zu unendlichen Bereichen und anonymen Funktionen

  • Das * von Whatevers ist in Raku ein Whatever-Symbol mit mehreren Rollen
  • In Bereichen oder Sequenzen bedeutet *, dass es keine Grenze gibt
    • 1..* ist 1..Inf
    • (2,4,8...*)[17] ist 262144
  • Ein Ausdruck, der * enthält, wird zu einer anonymen Funktion hochgestuft
    • Dieses Verhalten heißt whatever-priming
    • Das Ergebnis ist WhateverCode und bis auf den Typ nicht von anderen Funktionen zu unterscheiden
    • (* + 10)(2) ist 12
    • (^10).map(* % 2) erzeugt das Muster 0 1 0 1 ...
  • Wenn in einem Ausdruck zwei * vorkommen, wird jedes zu einem eigenen Positionsargument
    • (2, 30, 4, 50).map(* + *) gibt (32, 54) zurück
    • Das ist nützlich für trickreiche Fibonacci-Definitionen, aber nicht unbedingt überzeugender als ein Verhalten, bei dem jedes * denselben Wert hätte
  • Viele Raku-Methoden können Funktionen entgegennehmen, daher wird Priming breit eingesetzt
    • l[*-1] sieht wie ein negativer Index aus, tatsächlich übergibt [] aber die Listenlänge an eine Funktion und fragt dann den berechneten Index ab
    • Bei einer Listenlänge von 10 wird l[*-1] zu l[10-1], also zum letzten Element
    • l.head(2) bedeutet die ersten zwei Elemente, l.head(*-2) dagegen alles außer den letzten zwei
  • Gibt man [] andere Funktionen, lassen sich auch Strukturen wie Ringpuffer einfach umsetzen
    • @x[95 % *]-- berechnet die Position anhand der Listenlänge

Reguläre Ausdrücke: Komponierbare Grammatiken statt PCRE-Kompatibilität

  • Raku verwendet eine neue Syntax für reguläre Ausdrücke, die sich von POSIX-RegEx oder PCRE unterscheidet
    • POSIX-RegEx bestehen aus vielen Backslashes und Satzzeichen
    • PCRE ist zu POSIX abwärtskompatibel und übernimmt diese noch komplexere Struktur aus Backslashes und Satzzeichen
    • Perl 6 brach die Abwärtskompatibilität und führte eine neue RegEx-Syntax ein
  • Die auffälligste Verbesserung ist die Komponierbarkeit
    • In vielen Sprachen werden RegEx durch Aneinanderhängen von Zeichenketten kombiniert
    • Raku kann mit /< foo >+/ andere RegEx einbetten, sodass der RegEx foo einmal oder öfter gematcht wird, ohne nach außen in den übergeordneten RegEx auszulaufen
  • Komplexe RegEx lassen sich so in kleinere, besser lesbare Teile zerlegen
    • Dieser Ansatz bildet die Grundlage dafür, dass Raku Parsing-Grammatiken als eingebautes Sprachfeature unterstützt
  • Mit dem Bruch der Abwärtskompatibilität kamen auch mehrere Komfortfunktionen hinzu
    • \\d+ % ',' matcht 1, 1,2 und 1,1,4, aber nicht 1, oder 12
    • Lookahead und non-capturing group sind keine kryptischen Symbolkombinationen
    • r1 && r2 matcht nur Zeichenketten, die sowohl r1 als auch r2 erfüllen
    • Mit : lässt sich Backtracking stoppen
    • Leerzeichen werden standardmäßig ignoriert und müssen im Matchmuster explizit aktiviert werden
  • Unabhängig von den eigentlichen Verarbeitungsfunktionen kann Rakus RegEx-Notation als Sammlung von Features gesehen werden, die auch in anderen Sprachen auftauchen könnten

Hyperoperators: Verallgemeinertes Mapping bis in verschachtelte Listen

  • Die Grundform von Hyperoperators, l>>.method, ähnelt map, geht aber rekursiv in Unterlisten hinab
    • [1, [2, 3], 4]>>.succ wird zu [2 [3 4] 5]
  • In Raku lässt sich ein Funktionsaufruf der Form f(list, *args) in die Methodenform list.&f(* args) umschreiben
    • Deshalb funktioniert >>. wie ein verallgemeinerter Mapping-Operator
    • Zusammen mit Whatever lässt sich etwa [1, [2, 3], 4]>>.&(*+1) schreiben
  • Der binäre Hyperoperator l1 << op >> l2 wendet op elementweise auf beide Listen an
    • Die kürzere Liste wird wiederholt, bis die längere Liste endet
    • [1, 2, 3, 4, 5] <<+>> [10, 20] wird zu [11 22 13 24 15]
    • Formen wie << op<< oder >>op>> wiederholen nur bis zum Ende der linken bzw. rechten Liste
  • Das funktioniert auch mit Einzelwerten und verschachtelten Listen
    • [1, 2, 3, 4, 5] <<+>> 10 wird zu [11 12 13 14 15]
    • [1, [2, 3], 4, 5] <<+>> [10, 20] wird zu [11 [22 23] 14 25]
  • Für zwei Hashes haben Hyperoperators außerdem ein separates Verhalten, bei dem op auf Vereinigungen, Schnitte und Hash-Differenzen angewandt wird
  • Der Name Hyperoperator kommt daher, dass alle Operationen parallel ausgewertet werden
    • Auch explizite Schleifen lassen sich mit dem Präfix hyper parallelisieren

Pair-Syntax: Grundeinheit von Hashes und knappe Übergabe von Flags

  • Pair ist die grundlegende Einheit, aus der Raku-Hashes bestehen
    • Ein Hash ist eine Sammlung von key => value-Paaren
    • Pair ist der Basistyp, Hash eine Sammlung von Pairs
  • Raku bietet viele colon syntax-Kurzformen zum Schreiben von Pairs
    • my $x = 3; :$x ist x => 3
    • :a<$x> ist a => "$x"
    • :a($x) ist a => 3
    • :3a ist a => 3
  • Die wichtigste Kurzform sind :key und :!key
    • Sie entsprechen key => True bzw. key => False
    • Das wird als knappe Art verwendet, Flags an Methoden zu übergeben
  • Die Methode match akzeptiert mehrere colon pairs als Flags
    • Verwendet werden Formen wie :continue(:$c), :pos(:$p), :global(:$g), :overlap(:$ov)
    • :f(:$foo) in der Signatur definiert den Parameter $foo und erzeugt zusätzlich das Alias :f
    • Aufrufer können dasselbe Flag also mit :f oder :foo setzen
  • Colon-pair-Argumente sind bei Reihenfolge und Position flexibel
    • "abab".match(/../, :g) führt ein globales Matching aus
    • "abab".match(/../, :g, :ov) führt überlappendes Matching aus
    • Die Reihenfolge kann geändert und kompakt geschrieben werden wie in "abab".match(:g:ov, /../)
    • Sogar nach dem Funktionsaufruf lassen sich Keyword-Argumente anhängen, etwa "abab".match(:g, /../):ov:2nd
  • Mit dieser Syntax lassen sich Methodenoptionen viel kürzer angeben als mit match(global=True, overlap=True)

Slangs, RakuAST, Rakudo Star

  • Slangs sind eine Funktion zum Verändern der Raku-Syntax
  • RakuAST ist eine Funktion, die Raku-Ausdrücke innerhalb von Raku als AST parst
    • Q/my $x; $x++/.AST zeigt Variablendeklaration und Postfix-Inkrement als RakuAST::StatementList
    • Es gibt ein Beispiel, das mit Q/my $x; put $x/.AST.DEPARSE("NL") Raku-Code in einer anderen sprachartigen Notation ausgibt
  • Für Raku gibt es die Installationsdistribution Rakudo Star
    • Diese Distribution liefert ausgewählte Drittanbieter-Module vorinstalliert mit
    • Sie ist ein Kompromiss zwischen dem Wartungsaufwand einer großen Standardbibliothek und der Last für Nutzer, im Ökosystem selbst passende Pakete finden zu müssen

1 Kommentare

 
GN⁺ 2024-11-14
Hacker-News-Kommentare
  • Ich habe früher etwas Ähnliches wie die hier erwähnte Funktion für kombinierbare reguläre Ausdrücke unabhängig für JavaScript implementiert, und selbst Jahre später ist es noch eine der Bibliotheken, die ich am häufigsten in andere Projekte mitnehme.
    Es kommt dem Gefühl ein Stück näher, First-Class-Parser in der Sprache zu haben; als Beispiel gibt es eine Implementierung, die damit das Parsen von Medientypen mit regulären Ausdrücken umsetzt: https://runkit.com/tolmasky/media-type-parsing-with-template...
    Auf npm: templated-regular-expression, GitHub: https://github.com/tolmasky/templated-regular-expression
    Natürlich sollten Programmiersprachen echte Parser haben, und man sollte Regex nicht als Parser verwenden, aber wenn man ohnehin reguläre Ausdrücke nutzt, ist es wirklich schön, sie in kleine Teile zerlegen zu können.

    • „Echte Parser“ sind nicht mächtig genug, um Raku zu parsen.
      Rakus reguläre Ausdrücke sind in Verbindung mit Grammatiken deutlich mächtiger und, wenn gut geschrieben, leichter zu verstehen als jeder „echte Parser“. Um Raku mit einem herkömmlichen Parser zu parsen, müsste man während des Parsens Einträge zum Parser hinzufügen und daraus entfernen können; Rakus Parser löst das, indem er die aktuelle Grammatik subklassifiziert, innerhalb des aktuellen lexikalischen Scopes Einträge hinzufügt bzw. entfernt und nach Ende des Scopes zur vorherigen Grammatik zurückkehrt.
      In Raku sind reguläre Ausdrücke eine weitere Grammatik zum Schreiben von Code; sie unterscheiden sich nur etwas von der Standardgrammatik und ihrem Verhalten und können Parameter sowie Variablen haben. Wenn die Regex-Grammatik für eine Aufgabe nicht passt, kann man normale Raku-Grammatik einbetten, die nötige Arbeit erledigen und anschließend wieder zur Regex-Grammatik zurückkehren.
      Zum Beispiel ist es möglich, auf ein oder mehr A dieselbe Anzahl B und dieselbe Anzahl C zu matchen. In den meisten Regex-Syntaxen bräuchte man schon für dieselbe Anzahl A und B Rekursion in der Mitte; damit auch C auf dieselbe Weise zu behandeln, ist schwierig und deutlich schwerer zu lesen. Der Raku-Ansatz benötigt kein Backtracking und keine Rekursion und kann daher ziemlich schnell laufen.
      Wenn man es in eine Grammatik packt, bekommt man einen vollständigen Parse-Tree; ohne Grammatik geht es auch, aber mit Grammatik ist es einfacher. Als Beispiel für einen echten Parser empfehle ich oft JSON::TINY::Grammar: https://github.com/moritz/json/blob/master/lib/JSON/Tiny/Gra...
      Ehrlich gesagt wirkt ein beträchtlicher Teil des Designs „echter Parser“ wie ein Nebenprodukt des begrenzten RAM früher Computer. Der separate Tokenisierungsschritt diente dazu, den RAM-Bedarf des Quellcodes zu reduzieren, damit für spätere semantische Analyse und Kompilierung Speicher übrig blieb; ich glaube nicht, dass er diese späteren Phasen besonders stark vereinfacht. Das oben genannte JSON::Tiny-Modul baut während des Parsens über eine Action-Klasse native Raku-Datenstrukturen auf, es kompiliert also gewissermaßen gleichzeitig mit dem Parsen.
  • Ich nutze Raku in Produktion. Für Textverarbeitung ist es die beste Sprache, und das Erstellen von Parsern fühlt sich großartig an.
    Es überrascht mich, dass es nicht die Top-Sprache für den Bau von LLM-Text-Pipelines ist.

    • Ich komme spät dazu, aber ich frage mich, ob jemand ein gutes Beispiel kennt, wie man mit Raku eine HTTPS-API aufruft und pollt, bis ein bestimmter Wert zurückgegeben wird.
    • Mich würde interessieren, ob du LLM-Pakete für Raku verwendest, und falls ja, welche.
  • Wow. Da möchte ich die Branche verlassen, bevor der Tag kommt, an dem ich eine Raku-Codebase warten muss.

    • Lustigerweise hatte ich nach dem Lesen des Blogposts eher Lust, meinen Job zu kündigen und zum Raku-Team zu gehen, um dort zu arbeiten. Vielleicht bin ich noch zu naiv :)
    • Wenn man noch keinen typischen Raku-Code gesehen hat, ist so eine Reaktion auf den Artikel nachvollziehbar.
      In Einsteigerbüchern zu Raku fühlt sich vieles an wie Python mit Syntax aus der C-Familie. Es ist eine klammerzentrierte Syntax, aber Bedienkomfort, eingebaute Datenstrukturen und objektorientierte Features sind alle auf hohem Niveau.
      Wenn man andere High-Level-Skriptsprachen kennt, dürfte Raku-Code für ähnliche Skripte recht leicht zu lesen sein. Ich glaube nicht, dass die meisten Leute im Alltagscode die wirklich ungewöhnlichen Features verwenden; Raku ist flexibler, aber normaler Code sieht in der Regel nicht obskur aus.
    • Ähnlich wie Perl. Niemand will es warten, aber es zu benutzen macht wirklich Spaß. Die Ausdrucksstärke ist sehr groß.
      Das sieht man in Raku auch an der Möglichkeit, benannte Argumente in Kurzform zu definieren, etwa :global(:$g), oder an der Fähigkeit, unter Annahme eines True-Werts einen globalen Regex-Match wie match(/foo/, :g) aufzurufen. In Perl gibt es viel davon; alles ist dazu da, schneller und spaßiger zu arbeiten, ist für Anfänger aber weniger gut lesbar.
    • Es ist seltsam, dass Leute Ähnliches auch über die Wartung von mit KI-Unterstützung geschriebenen Codebases sagen.
      Das wird wohl jetzt ein Generationenunterschied. Eine ganze Generation älterer Programmierer könnte das Gefühl bekommen, aus einer Arbeitsumgebung herauszufallen, die für die aktuelle Generation ganz normal ist.
    • Ich glaube nicht, dass Raku als Sprache für die „Industrie“ gedacht ist.
  • Einige Features sind mir halbwegs vertraut. Hyper klingt wie eine spontanere Variante von etwas aus dem Bereich recursion-schemes, und das hier gezeigte * ähnelt bis zu einem gewissen Grad Scalas _.
    _ ist praktisch für Lambdas; ich wünschte, jede Sprache würde etwas Ähnliches übernehmen.

  • > (2, 30, 4, 50).map(* + *) returns (32, 45) müsste doch returns (32, 54) sein, oder? Der zweite Term wäre schließlich 4+50.
    Vielleicht ist es das Ergebnis einer Übersetzung im Kopf, weil man in manchen Ländern statt des englischen fifty-four etwas wie „vierundfünfzig“, also „four and fifty“, sagt.

    • In rakudo nachgeprüft: Tatsächlich gibt es (32 54) zurück. Dem Autor ist wohl die Hand ausgerutscht.
  • Ist Perl damit jetzt die Einstiegsdroge zu APL-artigen Sprachen?

    • Genau. Außerdem behandelt der Artikel nicht einmal Metaoperatoren.
      Zum Beispiel kann man den Reduce-Metaoperator [ ] zusammen mit dem Infix-+ verwenden, um „alles zu summieren“: [+] 1, 2, 3
  • Bei Raku ist Geschwindigkeit weiterhin ein großes Problem. Logdateien mit Regex zu parsen ist eigentlich eine Stärke von Perl, aber selbst aktuelles Raku braucht, abgesehen von der Startzeit, 6,5-mal so lange wie Python 3.13

    • Dazu sollte man ein Beispiel mitliefern. Nach meiner Erfahrung ist Raku bei manchen Dingen schneller und bei anderen langsamer; wenn man nicht sieht, wobei es langsam ist, sagt „Raku braucht 6,5-mal so lange wie Python 3.13“ allein nicht viel aus
  • Dass (2,4,8...*)[17] zu 262144 wird, fand ich wirklich erstaunlich

    • Mir kam sofort das Wort „schrecklich“ in den Sinn, danach „schreckliche Konsequenzen“ und das Halteproblem, aber Geschmäcker sind verschieden
    • Die Erkennung von Inkrementen gibt es in Perl 6 schon lange, das ist also keine neue Funktion.
      Abgesehen von Whatever dürfte das Neue seit Perl 6/Raku eher bei der Lazy Evaluation liegen
    • Es ist ziemlich konsistent.
      (1,3,9...*)[4,5] wird zu (81 243), und (1,3,9...*)[(1..3)] wird zu (3 9 27).
      Verschachtelung ist ebenfalls möglich, etwa (1,2,4...*)[(1,2,4...*)[1,2,3]]
  • Der Artikel greift also einige interessante Raku-Funktionen heraus.
    Die wichtigsten Raku-Funktionen sind meiner Ansicht nach die Kommandozeilenschnittstelle und Grammatiken (grammar).
    Unterstützung für Kommandozeilenschnittstellen ist eine verbreitete Funktion; es gibt zum Beispiel Implementierungen und Anwendungsfälle von docopt. In Raku ist diese Funktion jedoch eingebaut und angenehm zu verwenden.
    Grammatiken hingegen sind eine seltene Funktion, weil die Programmiersprache Grammatiken als First-Class-Bausteine bereitstellt und es ermöglicht, sie mithilfe objektorientierter Programmierung zu erstellen und zu kombinieren

  • Ich habe dieses Projekt über mehrere Jahre verfolgt und finde es interessant, aber es ist wirklich schade, dass Perl 6 sich auf dieser Reise durchs Spiegelland so sehr verirrt zu haben scheint

    • Ich frage mich, wie und woran Patrick Michaud heutzutage arbeitet