جوليا. سلاسل و Metaprogramming


نستمر في دراسة لغة جوليا للأولاد الشباب الواعدة. هذه المرة سوف نولي المزيد من الاهتمام للخطوط ، ونبدأ خطوات خجولة في عالم البرمجة الوصفية ، ونعلم المترجم الشفوي إجراء عمليات رمزية (لا يوجد سوى صورتين قيد القطع ، ولكن هناك الكثير من السكر النحوي)


خطوط


يتم إنشاء متغيرات السلسلة عن طريق تضمين أحرف في علامات اقتباس مزدوجة ، يتم تعيين أحرف مفردة على أحرف مفردة من النوع char . يتم تنفيذ تسلسل السلسلة باستخدام الضرب "*":


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

بمعنى أن الأفعال تتصرف مثل هذا:


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

من ناحية أخرى ، يتم فهرسة الصفوف:


 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) 

يمكن إنشاء سلاسل من سلاسل وأنواع أخرى:


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

هناك العديد من الوظائف المفيدة في الترسانة ، على سبيل المثال ، البحث عن عنصر:


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

وهكذا ، يمكنك بسهولة تنفيذ تشفير قيصر


 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 

هنا ، يتم التشغيل على جميع أحرف السلسلة ، تتم إضافة n إلى رمز كل منهم. اتضح مجموعة من أحرف char التي يتم لصقها معًا بواسطة دالة prod () ، والتي تنفذ عملية الضرب لجميع عناصر الصفيف المستلم.


شخصية الحوسبة


جوليا لديها بالفعل حزم من درجات متفاوتة من الاستعداد للحسابات الرمزية ، على سبيل المثال:



لكننا تعلمنا بطريقة ما الغراء ، لماذا لا ننفذ تصرفات رمزية بمصفوفات. لنفترض أننا أردنا إضافة صفيفتين متباينتين:


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

للبدء ، قم بتشغيل الأقلام المرحة في المشغلين الأساسيين:


 import Base: *, -, + 

إذا كان هذا التشوه في C ++ يسمى التحميل الزائد للمشغل ، فإننا هنا ببساطة نضيف طريقة لوظيفة موجودة.


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

أكثر من مائة ونصف طريقة ، وهنا طريقة أخرى ستعيش حتى نهاية الجلسة.


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

الآن subport الضرب ، أي أننا نحدد حالة ضرب سلسلة برقم:


 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) 

أي إذا ضُربت السلسلة بصفر ، فسوف نحصل على صفر ، ثم بعلامة واحدة ، ثم نحصل على نفس السلسلة ، وفي حالات أخرى ، أعمى الرقم بعلامة "*" والسلسلة (أضفنا الأقواس حتى لا تكون هناك شكوك في الأحرف). شخصيا ، أنا أحب طريقة عرض سطر واحد للوظيفة ، وهو أمر مفيد للمشغل الثلاثي:


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

سنكشف الحالات المتبقية لعمليات إعدام مماثلة ونحصل على شيء مثل:


 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 

ودعنا نحسب المحدد! ولكن نظرًا لأن الوظيفة المضمنة معقدة ، فإن أساليبنا ليست كافية لإطعامها بمصفوفة تحتوي على صفوف - لقد حصلنا على خطأ. وهذا يعني أننا سربنا باستخدام رمز 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 

صيغة المنتج المختلط


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


يمكن استخدامها للعثور على محدد المصفوفة 3 × 3 ، وفهم a ، b ، c كالصف الأول والثاني والثالث (العمود) ، على التوالي


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

من السهل أن نتخيل أنه بالنسبة للمصفوفات ، سيكون الأمر أكثر تعقيدًا ، سيبدو كل شيء غير بديهي للغاية - فهناك الكثير من الأقواس ومضاعفات الأرقام ، لذلك سيكون من الجيد إنشاء وظيفتين تقومان بإزالة إلغاء التجزئة. هذا سيكون واجبك.
بالإضافة إلى ذلك ، فإن مضاعفة الصفائف أكثر تعقيدًا:


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

سوف يعطي خطأ. أعمال واضحة ، بالنسبة للمصفوفات ذات البعد الذي يزيد عن ثلاثة ، يتم استخدام أساليب أكثر تعقيدًا وتعقيدًا ، لم نكن ندعو إلى حملها. حسنًا ، سنغطي الأساليب الحالية بالفعل باستخدام مُضاعِفنا (بالطبع ، هذا غير مستحسن ، خاصة في البرامج الأكثر تعقيدًا أو الأقل ، حيث من المحتمل أن تنشأ النزاعات الداخلية).


 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) 

الآن يمكنك مضاعفة بأمان:


 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 

لنضع هذه المصفوفة في الاعتبار ، ولكن الآن ، سنبدأ الخطوات الأولى


برمجة


ونقلت


جوليا تدعم metaprogramming. يشبه هذا البرمجة الرمزية ، حيث نتعامل مع التعبيرات (مثل 6 * 7) بدلاً من القيم (على سبيل المثال 42). سحب العمليات إلى المترجم الفوري ، نحصل على الفور على النتيجة التي ، كما رأينا ، يمكن التحايل عليها بمساعدة الخطوط:


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

يمكن تحويل السلاسل إلى تعبيرات باستخدام parse () ، ثم تقييمها باستخدام eval () :


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

لماذا كل هذه الصعوبات؟ الحيلة هي أنه عندما يكون لدينا تعبير ، يمكننا تعديله بطرق مختلفة مثيرة للاهتمام:


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

لتجنب التداعيات مع الخطوط ، يتم توفير عامل تشغيل :()


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

يمكنك "الاقتباس" تعبير ، وظيفة ، كتلة التعليمات البرمجية ...


 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) 

والآن سنقوم بتحليل المصفوفة المحسوبة مسبقًا (من الأفضل أن تبدأ جلسة جديدة بعد القسم السابق ، يمكن أن تتعارض أحمالي القبيحة بسهولة مع حزم المكونات الإضافية):


 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 

كما خمن البعض ، هذه مصفوفة من التحولات ، لقد قمنا بالفعل بتسوية ذلك ، الآن فقط للإحداثيات ثلاثية الأبعاد. سوف نحسب لقيم محددة:


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

تدور هذه المصفوفة حول محاور X و Z بواسطة 20 circونقل إلى \ 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) 


ثمرة شجرة التعبير


كما سبق أن رأينا ، فإن الأوتار تدعم "الاستيفاء" ، والذي يسمح لنا بسهولة بإنشاء سلاسل كبيرة من مكونات أصغر.


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

مع اقتباسات (علامات الاقتباس) - نفس القصة:


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

جذر كل إيفال


eval() لا تقوم فقط بإرجاع قيمة التعبير. دعنا نحاول اقتباس إعلان الوظيفة:


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

الآن إحياء لها:


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

باستخدام الاستيفاء ، يمكننا بناء تعريف وظيفة على الطاير. في الواقع ، يمكننا أن نفعل على الفور عددا من الوظائف.


 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! 

قد يكون ذلك مفيدًا للغاية عند التفاف واجهة برمجة التطبيقات (على سبيل المثال ، من مكتبة C أو من خلال HTTP). تحدد واجهات برمجة التطبيقات غالبًا قائمة بالوظائف المتاحة ، حتى تتمكن من التقاطها وإنشاء غلاف كامل مرة واحدة! راجع أمثلة Clang.jl أو TensorFlow.jl أو الجبر الخطي الأساسي.


الخطيئة الأصلية


هنا مثال عملي أكثر. النظر في التعريف التالي للدالة sin() على أساس سلسلة تايلور:


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 

الآن هو أبطأ بكثير مما يمكن أن يكون. والسبب هو أننا نكررها على k ، وهو مكلف نسبيًا. الكتابة الصريحة أسرع بكثير:


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

ولكن هذا أمر شاق في الكتابة ولم يعد يشبه سلسلة تايلور الأصلية. بالإضافة إلى ذلك ، هناك خطر كبير من الشتلات. هل هناك طريقة لقتل الأرنب مع اثنين من الطلقات؟ ماذا عن جوليا تكتب لنا هذا الرمز؟ أولاً ، فكر في الإصدار الرمزي للدالة +.


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

باستخدام علامة plus() يمكننا القيام بالعديد من الأشياء المثيرة للاهتمام ، على سبيل المثال ، مبلغ رمزي:


 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) 

هذا يعطينا جزءًا مهمًا من اللغز ، لكننا نحتاج أيضًا إلى معرفة ما نلخصه. فلنقم بإنشاء نسخة رمزية من سلسلة Taylor أعلاه التي تُقحم قيمة k.


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

يمكنك الآن تثبيت عناصر الصف كما في ناقل:


 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) 

ولخصهم:


 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 

ليس سيئا للتنفيذ السذاجة! ليس لدى الفوتون وقت لتشغيل مائة ونصف متر ، وقد تم بالفعل حساب الجيب!
هذا هو الوقت المناسب للانتهاء. بالنسبة لأولئك الذين يرغبون في إجراء مزيد من الفرز لهذا الموضوع ، فإنني أنصح باستخدام برنامج تعليمي تفاعلي يتم تنفيذه في Jupyter ، وقد تم استخدام ترجمته في الغالب هنا ، ودورة فيديو من الموقع الرسمي ، والجزء المقابل من الوثائق. للقادمين الجدد ، أنصحك بالتجول حول المحور ، حيث يوجد بالفعل كمية مقبولة من المواد.

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


All Articles