
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
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
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 e 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:
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.