Julia. Strings und Metaprogrammierung


Wir lernen weiterhin die junge und vielversprechende Allzwecksprache Julia . Dieses Mal werden wir den Linien mehr Aufmerksamkeit schenken, schüchterne Schritte in die Welt der Metaprogrammierung beginnen und dem Interpreten beibringen, symbolische Operationen durchzuführen (es gibt nur zwei Bilder unter dem Schnitt, aber viel syntaktischen Zucker).


Linien


Zeichenfolgenvariablen werden erstellt, indem Zeichen in doppelte Anführungszeichen gesetzt werden. Einfache Zeichen werden auf einfache Zeichen vom Typ char gesetzt . Die Verkettung von Zeichenfolgen erfolgt mit der Multiplikation "*":


cats = 4 s1 = "How many cats "; s2 = "is too many cats?"; s3 = s1*s2 Out[]: "How many cats is too many cats?" 

Das heißt, Potenzierung wirkt wie folgt:


 s1^3 Out[]: "How many cats How many cats How many cats " 

Auf der anderen Seite werden Zeilen indiziert:


 s1[3] Out[]: 'w': ASCII/Unicode U+0077 (category Ll: Letter, lowercase) s1[5:13] Out[]: "many cats" s1[13:-1:5] Out[]: "stac ynam" s2[end] Out[]: '?': ASCII/Unicode U+003f (category Po: Punctuation, other) 

Zeichenfolgen können aus Zeichenfolgen und anderen Typen erstellt werden:


 s4 = string(s3, " - I don't know, but ", cats, " is too few.") #   s4 = "$s3 - I don't know, but $cats is too few." Out[]: "How many cats is too many cats? - I don't know, but 4 is too few." 

Es gibt viele nützliche Funktionen im Arsenal, zum Beispiel die Suche nach einem Element:


 findfirst( isequal('o'), s4 ) Out[]: 4 findlast( isequal('o'), s4 ) Out[]: 26 findnext( isequal('o'), s4, 7 ) #      Out[]: 11 

Und so können Sie Caesars Chiffre einfach implementieren


 caesar(X, n) = prod( [x += n for x in X] ) str3 = "    "; caesar(str3, 3) Out[]: "####" str4 = caesar(str3, -32) Out[]: "\0\0\0\0" "Latin letters go before Cyrillic" < str4 < str3 Out[]: true 

Hier wird ein Lauf über alle Zeichen der Zeichenfolge ausgeführt, wobei dem Code jedes Zeichens n hinzugefügt wird. Es stellt sich ein Array von Zeichenzeichen heraus , die durch die Funktion prod () zusammengeklebt werden, wodurch die Multiplikation aller Elemente des empfangenen Arrays implementiert wird.


Zeichenberechnung


Julia hat bereits Pakete mit unterschiedlicher Bereitschaft für symbolische Berechnungen, zum Beispiel:



aber wir haben irgendwie gelernt, Strings zu kleben, warum nicht symbolische Aktionen mit Matrizen implementieren. Angenommen, wir wollten zwei unterschiedliche Arrays hinzufügen:


 m1 = [1 1 "a"; 1 0 1] Out[]: 2×3 Array{Any,2}: 1 1 "a" 1 0 1 m3 = [1 2 "ln(3)"; 2 1 0] Out[]: 2×3 Array{Any,2}: 1 2 "ln(3)" 2 1 0 m1+m3 Out[]: MethodError: no method matching +(::String, ::String) ... 

Führen Sie zunächst Ihre verspielten Stifte in den grundlegenden Operatoren aus:


 import Base: *, -, + 

Wenn in C ++ eine solche Verstümmelung als Operatorüberladung bezeichnet wurde, fügen wir hier einfach eine Methode für eine vorhandene Funktion hinzu.


 +(a::String, b::String) = a * "+" * b Out[]: + (generic function with 164 methods) 

Mehr als eineinhalbhundert Methoden, und hier ist eine andere, die bis zum Ende der Sitzung gültig sein wird.


 m1+m3 Out[]: 2×3 Array{Any,2}: 2 3 "a+ln(3)" 3 1 1 

Subportieren Sie nun die Multiplikation, dh wir bestimmen den Fall des Multiplizierens eines Strings mit einer Zahl:


 function *(a::String, b::Number) if b == 0 0 else if b == 1 a else "$b" * "*(" * a * ")" end end end Out[]: * (generic function with 344 methods) 

Das heißt, wenn die Zeichenfolge mit Null multipliziert wurde, erhalten wir Null, wenn mit Eins, erhalten wir dieselbe Zeichenfolge, und in anderen Fällen blenden wir die Zahl mit dem Zeichen "*" und der Zeichenfolge aus (wir haben Klammern hinzugefügt, damit keine Unsicherheiten mit den Zeichen bestehen). Persönlich mag ich die einzeilige Ansicht der Funktion, die für den ternären Operator nützlich ist:


 *(a::String, b::Number) = (b==0) ? 0 : (b==1) ? a : "$b" * "*(" * a * ")" Out[]: 

Wir werden die verbleibenden Fälle ähnlichen Hinrichtungen aussetzen und so etwas wie Folgendes erhalten:


 import Base: *, -, + +(a::String, b::String) = a * "+" * b *(a::String, b::String) = a * "*(" * b * ")" *(a::String, b::Number) = (b==0) ? 0 : (b==1) ? a : "$b" * "*(" * a * ")" *(b::Number, a::String) = (b==0) ? 0 : (b==1) ? a : "$b" * "*(" * a * ")" +(a::String, b::Number) = (b==0) ? a : a * "+" * "$b" +(b::Number, a::String) = (b==0) ? a : a * "+" * "$b" -(b::Number, a::String) = (b==0) ? "-" * a : "$b" * "-" * a -(a::String, b::Number) = (b==0) ? a : a * "-" * "$b" # m1 = [1 1 "a"; 1 0 1] m2 = [2 0 2; 2 "b" 2; 2 2 2] m1*m2 Out[]: 2×3 Array{Any,2}: "2*(a)+4" "b+2*(a)" "2*(a)+4" 4 2 4 

Und berechnen wir die Determinante! Da die eingebaute Funktion jedoch kompliziert ist, reichen unsere Methoden nicht aus, um eine Matrix mit Zeilen einzugeben - wir erhalten einen Fehler. Es bedeutet, dass wir unsere eigenen mit dem Levi-Civita-Symbol aufschlagen


 ε = zeros(Int, 3,3,3) ε[1,2,3] = ε[2,3,1] = ε[3,1,2] = 1 ε[3,2,1] = ε[1,3,2] = ε[2,1,3] = -1 ε Out[]: 3×3×3 Array{Int64,3}: [:, :, 1] = 0 0 0 0 0 1 0 -1 0 [:, :, 2] = 0 0 -1 0 0 0 1 0 0 [:, :, 3] = 0 1 0 -1 0 0 0 0 0 

Gemischte Produktformel


 left[ veca vecb vecc right]= sumi,j,k=13 varepsilonijkaibjck


kann verwendet werden, um die Determinante einer 3x3-Matrix zu finden, wobei a , b , c als erste, zweite bzw. dritte Zeile (Spalte) verstanden werden


 detrmnt(arr) = sum( ε[i,j,k]*arr[1,i]*arr[2,j]*arr[3,k] for i in 1:3, j in 1:3, k in 1:3 ) detrmnt(["a" 2 "b"; 1 0 1; "c" 2 "d"]) Out[]: "2*(c)+2*(b)+2*(-1*(a))+-2*(d)" 

Es ist leicht zu erraten, dass für Matrizen alles komplizierter aussieht und alles sehr trivial aussieht - viele Klammern und Multiplikationen von Zahlen. Es wäre also schön, ein paar Funktionen zu erstellen, die eine Defaktorisierung durchführen. Dies wird Ihre Hausaufgabe sein.
Darüber hinaus ist die Multiplikation von Arrays komplizierter:


 Rx = [1 0 0 0; 0 "cos(ϕ)" "sin(ϕ)" 0; 0 "-sin(ϕ)" "cos(ϕ)" 0; 0 0 0 1]; Rz = ["cos(ϕ)" "sin(ϕ)" 0 0; "-sin(ϕ)" "cos(ϕ)" 0 0; 0 0 1 0; 0 0 0 1]; T = [1 0 0 0; 0 1 0 0; 0 0 1 0; "x0" "y0" "z0" 1]; Rx*Rz Out[]: MethodError: no method matching zero(::String) ... 

wird einen Fehler geben. Klares Geschäft, für Matrizen mit einer Dimension von mehr als drei werden fortschrittlichere und komplexere Methoden verwendet, deren Aufrufe wir nicht „überladen“ haben. Nun, wir werden die bereits vorhandenen Methoden mit unserem eigenen Multiplikator abdecken (dies wird natürlich nicht empfohlen, insbesondere in mehr oder weniger komplexen Programmen, in denen höchstwahrscheinlich interne Konflikte auftreten werden).


 function *( a::Array{Any,2}, b::Array{Any,2} ) if size(a)[2] == size(b)[1] res = Array{Any}(undef, size(a)[1], size(b)[2] ) fill!(res, 0) for i = 1:size(a)[1], j = 1:size(b)[2], k = 1:size(a)[2] res[i,j] = res[i,j] + a[i,k]*b[k,j] end res else error("Matrices must have same size mult") end end Out[]: * (generic function with 379 methods) 

Jetzt können Sie sicher multiplizieren:


 X = Rx*Rz*T Out[]: 4?4 Array{Any,2}: "cos(ϕ)" "sin(ϕ)" 0 0 "cos(ϕ)*(-sin(ϕ))" "cos(ϕ)*(cos(ϕ))" "sin(ϕ)" 0 "-sin(ϕ)*(-sin(ϕ))" "-sin(ϕ)*(cos(ϕ))" "cos(ϕ)" 0 "x0" "y0" "z0" 1 

Denken wir an diese Matrix, aber jetzt beginnen wir mit den ersten Schritten


Metaprogrammierung


Zitate


Julia unterstützt die Metaprogrammierung. Dies ähnelt der symbolischen Programmierung, bei der Ausdrücke (z. B. 6 * 7) im Gegensatz zu Werten (z. B. 42) behandelt werden. Wenn wir Operationen in den Interpreter ziehen, erhalten wir sofort das Ergebnis, das, wie wir gesehen haben, mit Hilfe der Zeilen umgangen werden kann:


 x = "6*7" Out[]: "6*7" 

Strings können mit parse () in Ausdrücke umgewandelt und dann mit eval () ausgewertet werden:


 eval(Meta.parse(ans)) Out[]: 42 

Warum all diese Schwierigkeiten? Der Trick ist, dass wir einen Ausdruck auf verschiedene interessante Arten modifizieren können:


 x = replace(x, "*" => "+") eval(Meta.parse(x)) Out[]: 13 

Um das Fummeln mit Linien zu vermeiden, wird ein stirnrunzelnder Gesichtsoperator bereitgestellt :()


 y = :(2^8-1) Out[]: :(2^8-1) eval(y) Out[]: 255 

Sie können einen Ausdruck, eine Funktion, einen Codeblock "zitieren" ...


 quote x = 2 + 2 hypot(x, 5) end Out[]: quote #= In[13]:2 =# x = 2 + 2 #= In[13]:3 =# hypot(x, 5) end :(function mysum(xs) sum = 0 for x in xs sum += x end end) Out[]: :(function mysum(xs) #= In[14]:2 =# sum = 0 #= In[14]:3 =# for x = xs #= In[14]:4 =# sum += x end end) 

Und jetzt analysieren wir die zuvor berechnete Matrix (es ist besser, eine neue Sitzung nach dem vorherigen Abschnitt zu starten, da meine hässlichen Überladungen leicht zu Konflikten mit Plug-In-Paketen führen können):


 X = [ "cos(ϕ)" "sin(ϕ)" 0 0; "cos(ϕ)*(-sin(ϕ))" "cos(ϕ)*(cos(ϕ))" "sin(ϕ)" 0; "-sin(ϕ)*(-sin(ϕ))" "-sin(ϕ)*(cos(ϕ))" "cos(ϕ)" 0; "x0" "y0" "z0" 1; ] for i = 1:size(X,1), j = 1:size(X,2) if typeof(X[i,j]) == String X[i,j] = Meta.parse(X[i,j]) end end X Out[]: 4×4 Array{Any,2}: :(cos(ϕ)) :(sin(ϕ)) 0 0 :(cos(ϕ) * -(sin(ϕ))) :(cos(ϕ) * cos(ϕ)) :(sin(ϕ)) 0 :(-(sin(ϕ)) * -(sin(ϕ))) :(-(sin(ϕ)) * cos(ϕ)) :(cos(ϕ)) 0 :x0 :y0 :z0 1 

Wie einige vermutet haben, handelt es sich um eine Matrix von Transformationen, die wir bereits jetzt für dreidimensionale Koordinaten aussortiert haben. Wir berechnen für bestimmte Werte:


 ϕ = 20*pi/180 x0 = 4 y0 = -0.5 z0 = 1.3 Xtr = [ eval(x) for x in X] Out[]: 4×4 Array{Real,2}: 0.939693 0.34202 0 0 -0.321394 0.883022 0.34202 0 0.116978 -0.321394 0.939693 0 4 -0.5 1.3 1 

Diese Matrix dreht sich um die X- und Z- Achse um 20 circund übertragen auf \ vec {R} = \ {x0, y0, z0 \}


 x = [-1 -1 1 1 -1 -1 1 1 -1 -1]; y = [-1 -1 -1 -1 -1 1 1 1 1 -1]; z = [1 -1 -1 1 1 1 1 -1 -1 -1] R = [ x' y' z' ones( length(x) ) ] plot(x', y', z', w = 3) 


 R2 = R*Xtr plot!( R2[:,1], R2[:,2], R2[:,3], w = 3) 


Frucht des Ausdrucksbaums


Wie wir bereits gesehen haben, unterstützen Strings die „Interpolation“, mit der wir auf einfache Weise große Strings aus kleineren Komponenten erstellen können.


 x = "" print("$x $x $x  ...") Out[]:     ... 

Mit Zitaten - die gleiche Geschichte:


 x = :(6*7) y = :($x + $x) Out[]: :(6 * 7 + 6 * 7) eval(y) Out[]: 84 

Die Wurzel allen Eval


eval() nicht nur den Wert eines Ausdrucks zurück. Versuchen wir, die Funktionsdeklaration zu zitieren:


 ex = :( cats() = println("Meow!") ) cats() Out[]: UndefVarError: cats not defined 

Jetzt belebe sie wieder:


 eval(ex) Out[]: cats (generic function with 1 method) cats() Out[]: Meow! 

Mithilfe der Interpolation können wir die Definition einer Funktion im laufenden Betrieb erstellen. Tatsächlich können wir sofort eine Reihe von Funktionen ausführen.


 for name in [:dog, :bird, :mushroom] println(:($name() = println($("I'm $(name)!")))) end Out[]: dog() = begin #= In[27]:2 =# println("I'm dog!") end bird() = begin #= In[27]:2 =# println("I'm bird!") end mushroom() = begin #= In[27]:2 =# println("I'm mushroom!") end for name in [:dog, :bird, :mushroom] eval(:($name() = println($("I'm $(name)!")))) end dog() Out[]: I'm dog! mushroom() Out[]: I'm mushroom! 

Dies kann beim Umschließen einer API (z. B. aus einer C-Bibliothek oder über HTTP) äußerst nützlich sein. APIs definieren häufig eine Liste der verfügbaren Funktionen, sodass Sie sie erfassen und gleichzeitig eine gesamte Shell erstellen können! Siehe Beispiele für Clang.jl, TensorFlow.jl oder Basic Linear Algebra.


Erbsünde


Hier ist ein praktischeres Beispiel. Betrachten Sie die folgende Definition der sin() -Funktion basierend auf Taylor-Reihen:


sin(x)= sumk=1 infty frac(1)k(1+2k)!x1+2k


 mysin(x) = sum((-1)^k/factorial(1+2*k) * x^(1+2k) for k = 0:5) mysin(0.5), sin(0.5) Out[]: (0.4794255386041834, 0.479425538604203) using BenchmarkTools @benchmark mysin(0.5) Out[]: BenchmarkTools.Trial: memory estimate: 112 bytes allocs estimate: 6 -------------- minimum time: 1.105 μs (0.00% GC) median time: 1.224 μs (0.00% GC) mean time: 1.302 μs (0.00% GC) maximum time: 9.473 μs (0.00% GC) -------------- samples: 10000 evals/sample: 10 

Jetzt ist es viel langsamer als es sein könnte. Der Grund ist, dass wir über k iterieren, was relativ teuer ist. Explizites Schreiben ist viel schneller:


 mysin(x) = x - x^3/6 + x^5/120 # + ... 

Das Schreiben ist jedoch mühsam und sieht nicht mehr wie die ursprüngliche Taylor-Serie aus. Darüber hinaus besteht ein hohes Risiko für Setzlinge. Gibt es eine Möglichkeit, einen Hasen mit zwei Schüssen zu töten? Wie wäre es, wenn Julia uns diesen Code schreibt? Betrachten Sie zunächst die symbolische Version der + -Funktion.


 plus(a, b) = :($a + $b) plus(1, 2) Out[]: :(1 + 2) 

Mit plus() wir viele interessante Dinge tun, zum Beispiel eine symbolische Summe:


 reduce(+, 1:10) Out[]: 55 reduce(plus, 1:10) Out[]: :(((((((((1 + 2) + 3) + 4) + 5) + 6) + 7) + 8) + 9) + 10) eval(ans) Out[]: 55 reduce(plus, [:(x^2), :x, 1]) Out[]: :((x ^ 2 + x) + 1) 

Dies gibt uns einen wichtigen Teil des Puzzles, aber wir müssen auch herausfinden, was wir zusammenfassen. Erstellen wir eine symbolische Version der obigen Taylor-Reihe, die den Wert von k interpoliert.


 k = 2 :($((-1)^k) * x^$(1+2k) / $(factorial(1+2k))) Out[]: :((1 * x ^ 5) / 120) 

Jetzt können Sie die Elemente der Reihe wie auf einem Förderband nieten:


 terms = [:($((-1)^k) * x^$(1+2k) / $(factorial(1+2k))) for k = 0:5] Out[]: 6-element Array{Expr,1}: :((1 * x ^ 1) / 1) :((-1 * x ^ 3) / 6) :((1 * x ^ 5) / 120) :((-1 * x ^ 7) / 5040) :((1 * x ^ 9) / 362880) :((-1 * x ^ 11) / 39916800) 

Und fasse sie zusammen:


 reduce(plus, ans) Out[]: :((((((1 * x ^ 1) / 1 + (-1 * x ^ 3) / 6) + (1 * x ^ 5) / 120) + (-1 * x ^ 7) / 5040) + (1 * x ^ 9) / 362880) + (-1 * x ^ 11) / 39916800) :(mysin(x) = $ans) Out[]: :(mysin(x) = begin #= In[52]:1 =# (((((1 * x ^ 1) / 1 + (-1 * x ^ 3) / 6) + (1 * x ^ 5) / 120) + (-1 * x ^ 7) / 5040) + (1 * x ^ 9) / 362880) + (-1 * x ^ 11) / 39916800 end) eval(ans) mysin(0.5), sin(0.5) Out[]: (0.4794255386041834, 0.479425538604203) @benchmark mysin(0.5) Out[]: BenchmarkTools.Trial: memory estimate: 0 bytes allocs estimate: 0 -------------- minimum time: 414.363 ns (0.00% GC) median time: 416.328 ns (0.00% GC) mean time: 431.885 ns (0.00% GC) maximum time: 3.352 μs (0.00% GC) -------------- samples: 10000 evals/sample: 201 

Nicht schlecht für naive Umsetzung! Das Photon hat keine Zeit, eineinhalb hundert Meter zu laufen, und der Sinus wurde bereits berechnet!
Dies ist die Zeit zu beenden. Für diejenigen, die dieses Thema weiter vertiefen möchten, empfehle ich ein in Jupyter ausgeführtes interaktives Tutorial , dessen Übersetzung hier hauptsächlich verwendet wurde, einen Videokurs von der offiziellen Website und den entsprechenden Abschnitt der Dokumentation. Für Neuankömmlinge empfehle ich Ihnen, um den Hub herumzulaufen , da bereits eine akzeptable Menge an Material vorhanden ist.

Source: https://habr.com/ru/post/de431438/


All Articles