Julia. Cordas e metaprogramação


Continuamos a estudar a jovem e promissora linguagem de propósito geral Julia . Desta vez, prestaremos mais atenção às falas, iniciaremos tímidos passos no mundo da metaprogramação e ensinaremos o intérprete a realizar operações simbólicas (existem apenas duas imagens sob o corte, mas muito açúcar sintático)


Linhas


Variáveis ​​de seqüência de caracteres são criadas colocando caracteres entre aspas duplas, caracteres únicos são definidos como caracteres únicos do tipo char . A concatenação de cadeias é realizada usando a multiplicação "*":


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

Ou seja, a exponenciação age assim:


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

Por outro lado, as linhas são indexadas:


 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) 

As cadeias podem ser criadas a partir de cadeias e outros tipos:


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

Existem muitas funções úteis no arsenal, por exemplo, procurando por um elemento:


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

E assim, você pode implementar facilmente o Cipher 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 

Aqui, ocorre uma execução em todos os caracteres da cadeia, n é adicionado ao código de cada uma delas. Acontece uma matriz de caracteres char que são colados pela função prod () , que implementa a multiplicação de todos os elementos da matriz recebida.


Computação de caracteres


Julia já possui pacotes de vários graus de prontidão para cálculos simbólicos, por exemplo:



mas de alguma forma aprendemos a colar cordas, por que não implementar ações simbólicas com matrizes. Digamos que desejássemos adicionar duas matrizes diferentes:


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

Para começar, execute suas canetas divertidas nos operadores básicos:


 import Base: *, -, + 

Se em C ++ essa mutilação foi chamada de sobrecarga do operador, basta adicionar um método para uma função existente.


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

Mais de uma centena e meia de métodos, e aqui está outro que permanecerá até o final da sessão.


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

Agora subportar a multiplicação, a saber, determinamos o caso de multiplicar uma string por um número:


 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) 

Ou seja, se a string é multiplicada por zero, obtemos zero, se por uma, obtemos a mesma string e, em outros casos, cegamos o número com o sinal "*" e a string (adicionamos colchetes para que não haja incertezas nos caracteres). Pessoalmente, gosto da visualização em uma linha da função, que é útil para o operador ternário:


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

Exporemos os casos restantes a execuções semelhantes e obteremos algo como:


 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 

E vamos calcular o determinante! Mas como a função interna é complicada, nossos métodos não são suficientes para alimentar uma matriz com linhas - obtemos um erro. Isso significa que preparamos nosso próprio usando o símbolo 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 

Fórmula de produto misto


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


pode ser usado para encontrar o determinante de uma matriz 3x3, entendendo a , b , c como a primeira, segunda e terceira linha (coluna), respectivamente


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

É fácil adivinhar que, para as matrizes, é mais complicado, tudo parecerá muito trivial - existem muitos colchetes e multiplicações de números; portanto, seria bom fazer algumas funções que conduzem à desfatorização. Este será o seu dever de casa.
Além disso, a multiplicação de matrizes é mais complicada:


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

dará um erro. Negócio claro, para matrizes com uma dimensão de mais de três, métodos mais avançados e complexos são utilizados, cujas chamadas não foram “sobrecarregadas”. Bem, abordaremos os métodos já existentes com nosso próprio multiplicador (isso, é claro, não é recomendado, especialmente em programas mais ou menos complexos, onde provavelmente ocorrerão conflitos internos).


 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) 

Agora você pode multiplicar com segurança:


 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 

Vamos manter essa matriz em mente, mas, por enquanto, começaremos os primeiros passos


Metaprogramação


Citações


Julia apoia a metaprogramação. Isso é semelhante à programação simbólica, onde lidamos com expressões (por exemplo, 6 * 7) em oposição a valores (por exemplo, 42). Puxando operações para o intérprete, obtemos imediatamente o resultado, que, como vimos, pode ser contornado com a ajuda das linhas:


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

As strings podem ser transformadas em expressões usando parse () e depois avaliadas usando eval () :


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

Por que todas essas dificuldades? O truque é que, quando temos uma expressão , podemos modificá-la de várias maneiras interessantes:


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

Para evitar confusão com as linhas, é fornecido um operador de face franzida :()


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

Você pode "citar" uma expressão, função, bloco de código ...


 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) 

E agora analisaremos a matriz calculada anteriormente (é melhor iniciar uma nova sessão após a seção anterior, minhas sobrecargas feias podem facilmente entrar em conflito com os pacotes de plug-in):


 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 

Como alguns supuseram, essa é uma matriz de transformações, já resolvemos isso , apenas agora para coordenadas tridimensionais. Vamos calcular valores específicos:


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

Essa matriz gira em torno dos eixos X e Z 20 circe transferir para \ 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) 


Fruto da Árvore de Expressão


Como já vimos, as strings suportam "interpolação", o que nos permite criar facilmente strings grandes a partir de componentes menores.


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

Com aspas (aspas) - a mesma história:


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

A raiz de todos os Eval


eval() não apenas retorna o valor de uma expressão. Vamos tentar citar a declaração da função:


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

Agora revive-a:


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

Usando a interpolação, podemos construir a definição de uma função rapidamente; de fato, podemos executar imediatamente várias funções.


 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! 

Isso pode ser extremamente útil ao agrupar uma API (por exemplo, de uma biblioteca C ou através de HTTP). As APIs geralmente definem uma lista de funções disponíveis, para que você possa capturá-las e criar um shell inteiro de uma só vez! Consulte Exemplos de Clang.jl, TensorFlow.jl ou Álgebra linear básica.


Pecado original


Aqui está um exemplo mais prático. Considere a seguinte definição da função sin() baseada na 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 

Agora é muito mais lento do que poderia ser. O motivo é que iteramos sobre k, o que é relativamente caro. A gravação explícita é muito mais rápida:


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

Mas isso é entediante de escrever e não se parece mais com a série Taylor original. Além disso, há um alto risco de mudas. Existe uma maneira de matar uma lebre com dois tiros? Que tal Julia nos escrever esse código? Primeiro, considere a versão simbólica da função +.


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

Com plus() podemos fazer muitas coisas interessantes, por exemplo, uma soma simbólica:


 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) 

Isso nos dá uma parte importante do quebra-cabeça, mas também precisamos descobrir o que estamos resumindo. Vamos criar uma versão simbólica da série Taylor acima que interpola o valor de k.


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

Agora você pode rebitar os elementos da linha como em um transportador:


 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) 

E resuma-os:


 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 

Nada mal para uma implementação ingênua! O fóton não tem tempo para percorrer cem metros e meio e o seno já foi calculado!
Este é o tempo para terminar. Para aqueles que desejam esclarecer melhor este tópico, aconselharei um tutorial interativo executado no Jupyter , cuja tradução foi usada principalmente aqui, um curso em vídeo do site oficial e a seção correspondente da documentação. Para os recém-chegados, aconselho que você caminhe pelo centro , pois já existe uma quantidade aceitável de materiais.

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


All Articles