Ungewöhnliche Raku-Funktionen
(buttondown.com)- 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,anyoderone, 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|10wird zuany(2, 10)2&10 + 3wird zuall(5, 13)(1&2) + (10^20)wird zu einer Kombination wieall(one(11, 21), one(12, 22))
- Der zentrale Einsatzzweck von Junctions ist das Ausdrücken boolescher Formeln wie
allundany- Es gibt keine Möglichkeit, eine Junction zu destrukturieren
- Um sie praktisch zu verwenden, muss man sie zuerst auf einen booleschen Wert falten
soerzwingt die Umwandlung einer Junction in einen booleschen Wertso (1&2) + (10^20) < 15istTrueso (1&2) + (10^20) > 0istFalse
- Rakus
list.allist keine Methode mit Argumenten, sondern eine parameterlose Methode, die eine Liste in eine Junction umwandelt<1 2 3>.allwird zuall(1, 2, 3)- Wendet man
is-prime($x)an, wird es auf jeden Wert angewandt, etwa alsall(False, True, True)
- Durch das Kombinieren von Junctions lassen sich auch Verhaltensweisen wie
uniquebauen- „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
- „Ist jedes Element gleich genau einem Element?“ lässt sich als
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 gibt1..*ist1..Inf(2,4,8...*)[17]ist262144
- Ein Ausdruck, der
*enthält, wird zu einer anonymen Funktion hochgestuft- Dieses Verhalten heißt whatever-priming
- Das Ergebnis ist
WhateverCodeund bis auf den Typ nicht von anderen Funktionen zu unterscheiden (* + 10)(2)ist12(^10).map(* % 2)erzeugt das Muster0 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]zul[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 RegExfooeinmal 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+ % ','matcht1,1,2und1,1,4, aber nicht1,oder12- Lookahead und non-capturing group sind keine kryptischen Symbolkombinationen
r1 && r2matcht nur Zeichenketten, die sowohlr1als auchr2erfü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, ähneltmap, geht aber rekursiv in Unterlisten hinab[1, [2, 3], 4]>>.succwird zu[2 [3 4] 5]
- In Raku lässt sich ein Funktionsaufruf der Form
f(list, *args)in die Methodenformlist.&f(* args)umschreiben- Deshalb funktioniert
>>.wie ein verallgemeinerter Mapping-Operator - Zusammen mit Whatever lässt sich etwa
[1, [2, 3], 4]>>.&(*+1)schreiben
- Deshalb funktioniert
- Der binäre Hyperoperator
l1 << op >> l2wendetopelementweise 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] <<+>> 10wird 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
opauf 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
hyperparallelisieren
- Auch explizite Schleifen lassen sich mit dem Präfix
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
- Ein Hash ist eine Sammlung von
- Raku bietet viele colon syntax-Kurzformen zum Schreiben von Pairs
my $x = 3; :$xistx => 3:a<$x>ista => "$x":a($x)ista => 3:3aista => 3
- Die wichtigste Kurzform sind
:keyund:!key- Sie entsprechen
key => Truebzw.key => False - Das wird als knappe Art verwendet, Flags an Methoden zu übergeben
- Sie entsprechen
- Die Methode
matchakzeptiert 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$foound erzeugt zusätzlich das Alias:f- Aufrufer können dasselbe Flag also mit
:foder:foosetzen
- Verwendet werden Formen wie
- 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
- Sie können verwendet werden, um Schleifensyntax zu ändern, Bezeichner umzubenennen, Actors hinzuzufügen oder DNA-Sequenzen einzubauen
- Als Beispiele werden Slang::Otherwise, Slang::Piersing, OO::Actors und BioInfo genannt
- RakuAST ist eine Funktion, die Raku-Ausdrücke innerhalb von Raku als AST parst
Q/my $x; $x++/.ASTzeigt Variablendeklaration und Postfix-Inkrement alsRakuAST::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
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-expressionNatü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.
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
Adieselbe AnzahlBund dieselbe AnzahlCzu matchen. In den meisten Regex-Syntaxen bräuchte man schon für dieselbe AnzahlAundBRekursion in der Mitte; damit auchCauf 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.
Wow. Da möchte ich die Branche verlassen, bevor der Tag kommt, an dem ich eine Raku-Codebase warten muss.
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.
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 einesTrue-Werts einen globalen Regex-Match wiematch(/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.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.
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 dochreturns (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.
(32 54)zurück. Dem Autor ist wohl die Hand ausgerutscht.Ist Perl damit jetzt die Einstiegsdroge zu APL-artigen Sprachen?
Zum Beispiel kann man den Reduce-Metaoperator
[ ]zusammen mit dem Infix-+verwenden, um „alles zu summieren“:[+] 1, 2, 3Bei 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
Dass
(2,4,8...*)[17]zu262144wird, fand ich wirklich erstaunlichAbgesehen von
Whateverdürfte das Neue seit Perl 6/Raku eher bei der Lazy Evaluation liegen(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