Julia. Cordes et métaprogrammation


Nous continuons d'étudier la jeune et prometteuse langue polyvalente Julia . Cette fois, nous porterons plus d'attention aux lignes, commencerons des pas timides dans le monde de la métaprogrammation et apprendrons à l'interprète à effectuer des opérations symboliques (Il n'y a que deux images sous la coupe, mais beaucoup de sucre syntaxique)


Lignes


Les variables de chaîne sont créées en mettant des caractères entre guillemets, les caractères simples sont définis sur des caractères simples de type char . La concaténation de chaînes est effectuée en utilisant la multiplication "*":


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

Autrement dit, l'exponentiation agit comme ceci:


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

En revanche, les lignes sont indexées:


 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) 

Les chaînes peuvent être créées à partir de chaînes et d'autres types:


 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." 

Il existe de nombreuses fonctions utiles dans l'arsenal, par exemple, la recherche d'un élément:


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

Et donc, vous pouvez facilement implémenter le chiffre de César


 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 

Ici, une course se produit sur tous les caractères de la chaîne, n est ajouté au code de chacun d'eux. Il s'avère qu'un tableau de caractères char est collé ensemble par la fonction prod () , qui implémente la multiplication de tous les éléments du tableau reçu.


Informatique de caractère


Julia dispose déjà de packages de différents degrés de préparation pour les calculs symboliques, par exemple:



mais nous avons en quelque sorte appris à coller des chaînes, pourquoi ne pas implémenter des actions symboliques avec des matrices. Disons que nous voulions ajouter deux tableaux disparates:


 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) ... 

Pour commencer, lancez vos stylos ludiques dans les opérateurs de base:


 import Base: *, -, + 

Si en C ++ une telle mutilation était appelée surcharge d'opérateur, alors nous ajoutons simplement ici une méthode pour une fonction existante.


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

Plus d'une centaine et demi de méthodes, et en voici une autre qui vivra jusqu'à la fin de la session.


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

Maintenant, sous-portez la multiplication, à savoir, nous déterminons le cas de la multiplication d'une chaîne par un nombre:


 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) 

Autrement dit, si la chaîne est multipliée par zéro, nous obtenons zéro, si par un, alors nous obtenons la même chaîne, et dans d'autres cas, nous aveuglons le nombre avec le signe "*" et la chaîne (nous avons ajouté des crochets afin qu'il n'y ait aucune incertitude avec les caractères). Personnellement, j'aime la vue sur une ligne de la fonction, qui est utile pour l'opérateur ternaire:


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

Nous exposerons les cas restants à des exécutions similaires et obtiendrons quelque chose comme:


 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 

Et calculons le déterminant! Mais comme la fonction intégrée est compliquée, nos méthodes ne suffisent pas à lui fournir une matrice avec des lignes - nous obtenons une erreur. Cela signifie que nous préparons le nôtre en utilisant le symbole Levi-Civita


 ε = 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 

Formule de produit mixte


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


peut être utilisé pour trouver le déterminant d'une matrice 3x3, comprenant a , b , c comme la première, la deuxième et la troisième rangée (colonne), respectivement


 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)" 

Il est facile de deviner que pour les matrices, c'est plus compliqué, tout aura l'air très simple - il y a beaucoup de parenthèses et de multiplications de nombres, donc ce serait bien de faire quelques fonctions qui conduisent à la défactorisation. Ce sera votre devoir.
De plus, la multiplication des tableaux est plus compliquée:


 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) ... 

donnera une erreur. Des affaires claires, pour des matrices de plus de trois, des méthodes plus avancées et complexes sont utilisées, dont nous n'avons pas «surchargé» les appels. Eh bien, nous couvrirons les méthodes déjà existantes avec notre propre multiplicateur (cela, bien sûr, n'est pas recommandé, en particulier dans les programmes plus ou moins complexes, où des conflits internes se produiront très probablement).


 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) 

Vous pouvez maintenant multiplier en toute sécurité:


 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 

Gardons cette matrice à l'esprit, mais pour l'instant, nous allons commencer les premières étapes de


Métaprogrammation


Citations


Julia prend en charge la métaprogrammation. Ceci est similaire à la programmation symbolique, où nous traitons des expressions (par exemple 6 * 7) par opposition aux valeurs (par exemple 42). En tirant les opérations dans l'interpréteur, nous obtenons immédiatement le résultat qui, comme nous l'avons vu, peut être contourné à l'aide des lignes:


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

Les chaînes peuvent être transformées en expressions en utilisant parse () , puis évaluées en utilisant eval () :


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

Pourquoi toutes ces difficultés? L'astuce est que lorsque nous avons une expression , nous pouvons la modifier de différentes manières intéressantes:


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

Pour éviter de tracasser avec les lignes, un opérateur de visage fronçant est fourni :()


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

Vous pouvez "citer" une expression, une fonction, un bloc de code ...


 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) 

Et maintenant, nous allons analyser la matrice précédemment calculée (il est préférable de démarrer une nouvelle session après la section précédente, mes surcharges laides peuvent facilement entrer en conflit avec les packages de plug-ins):


 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 

Comme certains l'ont deviné, il s'agit d'une matrice de transformations, nous l'avons déjà trié , seulement maintenant pour les coordonnées tridimensionnelles. Nous calculerons pour des valeurs spécifiques:


 ϕ = 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 

Cette matrice tourne autour des axes X et Z de 20 $ ^ {\ circ} $ et transfert à \ 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) 


Fruit de l'arbre d'expression


Comme nous l'avons déjà vu, les chaînes prennent en charge «l'interpolation», ce qui nous permet de créer facilement de grandes chaînes à partir de composants plus petits.


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

Avec des guillemets (citations) - la même histoire:


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

La racine de tout Eval


eval() renvoie non seulement la valeur d'une expression. Essayons de citer la déclaration de fonction:


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

Maintenant, ressuscitez-la:


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

En utilisant l'interpolation, nous pouvons construire la définition d'une fonction à la volée; en fait, nous pouvons immédiatement faire un certain nombre de fonctions.


 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! 

Cela peut être extrêmement utile lors de l'encapsulation d'une API (par exemple, à partir d'une bibliothèque C ou via HTTP). Les API définissent souvent une liste de fonctions disponibles, vous pouvez donc les capturer et créer un shell entier à la fois! Voir Exemples de Clang.jl, TensorFlow.jl ou Algèbre linéaire de base.


Péché originel


Voici un exemple plus pratique. Considérons la définition suivante de la fonction sin() basée sur la série de Taylor:


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 

Maintenant, c'est beaucoup plus lent qu'il ne pourrait l'être. La raison en est que nous itérons sur k, ce qui est relativement cher. L'écriture explicite est beaucoup plus rapide:


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

Mais c'est fastidieux à écrire et ne ressemble plus à la série Taylor originale. De plus, le risque de gaules est élevé. Existe-t-il un moyen de tuer un lièvre avec deux coups de feu? Et si Julia nous écrivait ce code? Considérons d'abord la version symbolique de la fonction +.


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

Avec plus() nous pouvons faire beaucoup de choses intéressantes, par exemple, une somme symbolique:


 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) 

Cela nous donne une partie importante du puzzle, mais nous devons également comprendre ce que nous résumons. Créons une version symbolique de la série Taylor ci-dessus qui interpole la valeur de k.


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

Vous pouvez maintenant riveter les éléments de la rangée comme sur un convoyeur:


 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) 

Et résumez-les:


 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 

Pas mal pour une implémentation naïve! Le photon n'a pas le temps de parcourir un mètre et demi et le sinus a déjà été calculé!
C'est le moment de finir. Pour ceux qui veulent approfondir ce sujet, je conseillerai un tutoriel interactif exécuté dans Jupyter , dont la traduction a été principalement utilisée ici, un cours vidéo sur le site officiel et la section correspondante de la documentation. Pour les nouveaux arrivants, je vous conseille de vous promener dans le hub , car il y a déjà une quantité acceptable de matériaux.

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


All Articles