Julia ermutigt Programmierer dazu, so viel Code wie möglich in Funktionen unterzubringen, die JIT-kompiliert werden können, und viele kleine Funktionen zu erstellen ist von vornherein performant.
So entstehen oft viele kleine, einfache Funktionen, die kombiniert werden müssen, um nicht triviale Aufgaben zu erledigen.
Ein naheliegender Ansatz ist es, Funktionsaufrufe zu verschachteln. Das folgende Beispiel ist sehr konstruiert, veranschaulicht aber den Punkt.
julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
Der Nachteil dieses Ansatzes ist, dass die Lesbarkeit schnell sinkt, je tiefer die Verschachtelung wird.
Wir brauchen einen einfacheren und flexibleren Ansatz.
Diese Technik ist bei Mathematikern beliebt, und Julia übernimmt die mathematische Syntax.
Eine beliebige Anzahl von Funktionen kann mit ∘-Operatoren composed werden (eingegeben als \circ und dann Tab).
Das Ergebnis kann wie eine einzelne Funktion verwendet werden.
julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse
julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])
Ein paar Punkte, die du beachten solltest:
Eine Alternative lässt sich eher als der Ansatz der Programmierer verstehen, nicht als der der Mathematiker.
Pipelines werden seit Langem in Unix-Shell-Skripten verwendet und wurden in jüngerer Zeit in gängigen Programmiersprachen populär (F# wird manchmal zugeschrieben, ihre Verbreitung vorangetrieben zu haben).
Das Grundkonzept: Du beginnst mit irgendwelchen Daten und leitest sie dann durch eine Abfolge von Funktionen, um das Ergebnis zu erhalten.
Der Pipe-Operator ist |> (wie in F# und neueren Versionen von R), allerdings hat Julia auch eine Broadcast-Version .|>.
julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
Die Ausführung erfolgt jetzt strikt von links nach rechts, wobei die Ausgabe jeder Funktion in Richtung des Pfeils fließt und zum Eingabewert der nächsten Funktion wird.
Es ist kein Zufall, dass die Funktionen, mit denen die Komposition und das Pipelining veranschaulicht wurden, alle ein einziges Argument entgegennehmen.
Manche rein funktionalen Sprachen leiten das erste Argument in eine Funktion und erlauben, weitere Argumente anzugeben.
Im Gegensatz dazu erwartet Julia in einer Pipeline nur Funktions_namen_ (oder etwas Gleichwertiges), ohne zusätzliche Argumente.
Dafür gibt es wichtige technische Gründe (sie hängen damit zusammen, dass currying kein fester Bestandteil des Sprachdesigns ist).
Die vielen Menschen, die Currying nicht kennen, sollten einfach akzeptieren, dass diese Einschränkung kein sorgloser Fehler ist und sich in künftigen Julia-Versionen wahrscheinlich nicht ändern wird.
Wir brauchen Funktionen mit einem einzigen Argument, die jeweils das Nötige tun. Zum Glück ist das Definieren neuer Funktionen in Julia einfach.
Am einfachsten könnten wir eine anonymous function verwenden.
Zum Beispiel, wenn wir einen einzelnen String als Eingabe haben und ihn an Unterstrichen aufteilen wollen:
julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
Dieser Vektor könnte dann wie zuvor durch weitere Funktionen geleitet werden.
In diesem Fall ist es optional, die anonyme Funktion in runde Klammern zu setzen, aber allgemein ist das eine nützliche Möglichkeit, Mehrdeutigkeiten zu vermeiden.
Ebenso könnten wir weiter oben im Programm eine benannte Funktion erstellen und sie nach Bedarf wiederverwenden.
Closures gehen über den Rahmen dieses Konzepts hinaus, aber wer sie aus anderen Sprachen kennt, erkennt, dass sie eine flexiblere Möglichkeit bieten, Funktionen mit einem Argument zu erstellen.
julia> function makesplit(sep)
fs(str) = split(str, sep)
fs
end
makesplit (generic function with 1 method)
julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)
julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
Sobald makesplit() definiert ist, kann es mit jedem beliebigen Trennzeichen verwendet werden.
Beachte, dass makesplit('_') ein Funktionsaufruf ist, der zu einer anderen Funktion ausgewertet wird, die wiederum die Eingabe aus der Pipe erhält.
Wenn dir das verwirrend vorkommt, ist das am Anfang normal (mit Übung wird es klarer).
Es gab eine lange Diskussion darüber, Pipes in Base Julia vielseitiger zu machen, aber die verschiedenen Vorschläge sind miteinander unvereinbar, und es wurde keine Einigung erzielt.
In der Zwischenzeit haben Nutzer den üblichen Weg gewählt und verschiedene installierbare Pakete erstellt, die bestimmte Bedürfnisse abdecken. Keines davon funktioniert innerhalb von Exercism, aber schau sie dir an, wenn du interessiert bist: