جوليا: أنواع ، multimethods والحساب على كثيرات الحدود

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

بناء الجملة الأساسية


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

العمليات الحسابية


الحساب هو نفسه تقريبا في كل مكان: + ، - ، * ، / ، ^ ، للتسفي ، الخ
المقارنة:> ، <،> = ، <= ، == ،! = إلخ.
الواجب: =.
الميزات: القسمة من خلال / دائما إرجاع عدد كسور. إذا كنت بحاجة إلى الجزء الصحيح من قسم عدد صحيحين ، فأنت بحاجة إلى استخدام div(m, n) أو مكافئ infix m ÷ n .

أنواع


أنواع رقمية:
  • أعداد صحيحة ( Int ) - 2 ، 3 ، -42
  • أعداد صحيحة غير موقعة ( UInt ) - 0x12345
  • النقطة العائمة ( Float32 ، Float64 ) - 1.0 ، 3.1415 ، -Inf ، NaN
  • عقلاني ( Rational ) - 3//3 ، 7//2
  • حقيقي ( Real ) - كل ما سبق
  • Complex ( Complex ) - 3+4*im ، 2//3+2//3*im ، 3.0+0.0*im ( im هي وحدة وهمية ، فقط رقم به جزء تخيلي مكتوب بشكل صريح يعتبر معقدًا)
  • Number - كل ما سبق


السلاسل والشخصيات:
  • 'a' - شخصية ( Char )
  • "a" عبارة عن سلسلة ( String )


ملحوظة: السلاسل ، كما هو الحال الآن في العديد من اللغات ، غير قابلة للتغيير.
ملحوظة: تدعم السلاسل (وكذلك أسماء المتغيرات) Unicode ، بما في ذلك الرموز التعبيرية.

المصفوفات:
  • x = [1, 2, 3] - تحديد مجموعة من التعداد المباشر للعناصر
  • zeros(length) الخاصة: zeros(length) لمجموعة من الأصفار ، الأصفار ones(length) لمجموعة من الأعمدة ، rand(length) لمجموعة من الأرقام العشوائية ، إلخ.
  • دعم مجموعة متعددة الأبعاد
  • دعم عمليات الجبر الخطي (إضافة المصفوفات ، الضرب القياسي ، ضرب مصفوفة المتجهات ، وأكثر من ذلك بكثير) في المكتبة القياسية


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

Tuples (مجموعة من العناصر مرتبة ، غير قابلة للتغيير):
  • (2, 5.3, "k") هو tuple منتظم
  • (a = 3, b = 4) - الاسم المسمى tuple


ملحوظة: يمكن الوصول إلى حقول tuple المسماة بالاسم خلال فترة وحسب الفهرس عبر []
 julia> x = (a = 5, b = 12) (a = 5, b = 12) julia> x[1] 5 julia> sqrt(xa^2 + x[2]^2) 13.0 


القواميس:
 julia> x = Dict('a' => 5, 'b' => 12) Dict{Char,Int64} with 2 entries: 'a' => 5 'b' => 12 julia> x['c'] = 13 13 julia> x Dict{Char,Int64} with 3 entries: 'a' => 5 'c' => 13 'b' => 12 


بنيات لغة التحكم الأساسية


1. يتم إنشاء المتغيرات تلقائيا عند التعيين. النوع اختياري.
 julia> x = 7; x + 2 9 julia> x = 42.0; x * 4 168.0 

2. كتلة الانتقال الشرطية تبدأ بالتعبير if <condition> وتنتهي end الكلمة. يمكنك أيضًا الحصول على أضواء elseif أو أضواء أخرى:
 if x > y println("X is more than Y") elseif x == y println("X and Y are equal") else println("X is less than Y") end 

3. هناك اثنين من يبني حلقة: في while for . والثاني يعمل كما في بيثون ، أي يتكرر على المجموعة. الاستخدام الشائع هو التكرار على نطاق من القيم التي start[:increment]:end بناء الجملة فيها start[:increment]:end . على عكس بيثون ، يشتمل النطاق على قيمتي البداية والنهاية ، أي لن يكون النطاق الفارغ 1:1 (هذا هو نطاق 1) ، ولكن 1:0 . يتم وضع علامة end جسم الحلقة end الكلمة.
 julia> for i in 1:3; print(i, " "); end #   1  3   1 ( ) 1 2 3 julia> for i in 1:2:3; print(i, " "); end #   1  3   2 1 3 

4. يتم تحديد وظائف من خلال function الكلمة الأساسية ، كما ينتهي تعريف الوظيفة end الكلمة. يتم دعم الوسائط ذات القيم الافتراضية والوسائط المسماة.
 function square(x) return x * x end function cube(x) x * square(x) #       ; return   end function root(x, degree = 2) #  degree     return x^(1.0/degree) end function greeting(name; times = 42, greet = "hello") #       println(times, " times ", greet, " to ", name) end julia> greeting("John") 42 times hello to John julia> greeting("Mike", greet = "wassup", times = 100500) #           100500 times wassup to Mike 


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

اكتب التعليقات التوضيحية والإرسال المتعدد


دعونا نرى ما هي وظيفة المدمج في:
 julia> sqrt sqrt (generic function with 19 methods) 

كما يوضح لنا REPL ، sqrt هي وظيفة عامة مع 19 طريقة. أي نوع من الوظائف المعممة وأي نوع من الأساليب؟

وهذا يعني أن هناك العديد من وظائف sqrt التي تنطبق على أنواع مختلفة من الوسائط ، وبالتالي ، قم بحساب الجذر التربيعي باستخدام خوارزميات مختلفة. يمكنك معرفة الخيارات المتاحة عن طريق الكتابة
 julia> methods(sqrt) 

يمكن ملاحظة أن الوظيفة محددة لأنواع مختلفة من الأرقام ، وكذلك للمصفوفات.

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

السمة المميزة هي أنه يتم تطبيق نهج يسمى "التجميع في وقت مبكر" من قبل مؤلفي اللغة. أي يتم تجميع الوظائف لأنواع البيانات المحددة في المكالمة الأولى ، وبعد ذلك يتم إجراء المكالمات التالية بشكل أسرع بكثير. يمكن أن يكون الفرق بين المكالمات الأولى والمكالمات اللاحقة كبيرًا جدًا:
 julia> @time sqrt(8) #  @time -      0.006811 seconds (3.15 k allocations: 168.516 KiB) #   ,        2.8284271247461903 julia> @time sqrt(15) 0.000002 seconds (5 allocations: 176 bytes) # 5   -     @time 3.872983346207417 

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

على سبيل المثال ، ضع في اعتبارك حساب المبلغ

 sumk=1N sqrt(1)k


 function mysqrt(num) #    -     #   -           if num >= 0 return sqrt(num) else return sqrt(complex(num)) end end function S(n) #    sum = 0 sgn = -1 for k = 1:n sum += mysqrt(sgn) sgn = -sgn end return sum end function S_typed(n::Integer) # ..     ,      #     sum::Complex = 0.0 sgn::Int = -1 for k = 1:n sum += mysqrt(sgn) sgn = -sgn end return sum end 

يوضح المعيار أن وظيفة S_typed() لا تعمل فقط بشكل أسرع ، ولكن لا تتطلب أيضًا تخصيص ذاكرة لكل مكالمة ، على عكس S() . المشكلة هنا هي أن نوع mysqrt() إرجاعها من mysqrt() غير معرف ، تمامًا مثل نوع الجانب الأيمن من التعبير
 sum = sum + mysqrt(sgn) 

نتيجة لذلك ، لا يمكن للمترجم معرفة نوع sum سيكون في كل تكرار. لذلك ، فإن الملاكمة (كتابة تسمية النوع) هي متغير ويتم تخصيص الذاكرة.
بالنسبة إلى S_typed() ، يعرف المترجم مقدمًا أن sum هو قيمة معقدة ، وبالتالي فإن الكود محسّن أكثر (على وجه الخصوص ، استدعاء mysqrt() يمكن أن يكون مضمنًا بشكل فعال ، mysqrt() دائمًا قيمة الإرجاع إلى Complex ).

الأهم من ذلك ، بالنسبة إلى S_typed() يعرف المترجم أن قيمة الإرجاع هي من النوع Complex ، ولكن بالنسبة إلى S() نوع قيمة الإخراج مرة أخرى ، مما سيؤدي إلى إبطاء جميع الوظائف التي سيتم استدعاء S() .
يمكنك التحقق من أن المحول البرمجي يفكر في الأنواع التي تم إرجاعها من التعبير باستخدام الماكرو @code_warntype :
 julia> @code_warntype S(3) Body::Any #     ,      ... julia> @code_warntype S_typed(3) Body::Complex{Float64} #      ... 

إذا تم استدعاء وظيفة في مكان ما في الحلقة التي لا تستطيع @code_warntype طباعة نوع الإرجاع ، أو التي تظهر في مكان ما في الجسم استلام قيمة النوع " Any ، فمن المحتمل أن يوفر تحسين هذه المكالمات تعزيزًا ملموسًا للأداء.

أنواع المركبات


يمكن للمبرمج تحديد أنواع البيانات المركبة لاحتياجاته باستخدام struct الهيكلية:
 julia> struct GenericStruct #   struct    name b::Int c::Char v::Vector end #       #       ,        julia> s = GenericStruct("Name", 1, 'z', [3., 0]) GenericStruct("Name", 1, 'z', [3.0, 0.0]) julia> s.name, sb, sc, sv ("Name", 1, 'z', [3.0, 0.0]) 

الهياكل في جوليا غير قابلة للتغيير ، أي عن طريق إنشاء مثيل للبنية ، لم يعد من الممكن تغيير قيم الحقول (بتعبير أدق ، لا يمكنك تغيير عنوان الحقول في الذاكرة - يمكن تغيير عناصر الحقول القابلة للتغيير ، مثل sv في المثال أعلاه). يتم إنشاء بنيات mutable struct ، يكون بناء جملةها مماثلًا للهياكل العادية.

لا يتم دعم وراثة الهياكل بالمعنى "الكلاسيكي" ، ولكن هناك احتمال "وراثة" السلوك من خلال الجمع بين الأنواع المركبة في أنواع فائقة أو ، كما يطلق عليها في جوليا ، أنواع مجردة. يتم التعبير عن علاقات الكتابة كـ A<:B (A هو نوع فرعي من B) و A>:B (A هو نوع فرعي من B). يبدو شيء مثل هذا:
 abstract type NDimPoint end #   -     # ,    -     N  struct PointScalar<:NDimPoint x1::Real end struct Point2D<:NDimPoint x1::Real x2::Real end struct Point3D<:NDimPoint x1::Real x2::Real x3::Real end #     ;   Markdown """ mag(p::NDimPoint) Calculate the magnitude of the radius vector of an N-dimensional point `p` """ function mag(p::NDimPoint) sqrmag = 0.0 # ..   ,       #     T   fieldnames(T) for name in fieldnames(typeof(p)) sqrmag += getfield(p, name)^2 end return sqrt(sqrmag) end """ add(p1::T, p2::T) where T<:NDimPoint Calculate the sum of the radius vectors of two N-dimensional points `p1` and `p2` """ function add(p1::T, p2::T) where T<:NDimPoint #  -  , ..       #     list comprehension sumvector = [Float64(getfield(p1, name) + getfield(p1, name)) for name in fieldnames(T)] #     ,    #  ...      , .. # f([1, 2, 3]...) -   ,  f(1, 2, 3) return T(sumvector...) end 

دراسة حالة: كثيرات الحدود


نظام الكتابة إلى جانب إرسال متعددة مناسب للتعبير عن المفاهيم الرياضية. دعونا نلقي نظرة على مثال لمكتبة بسيطة للعمل مع كثير الحدود.
نقدم نوعين من كثيرات الحدود: "متعارف عليه" ، يتم تعريفه من خلال المعاملات عند القوى ، و "الاستيفاء" ، المعرف بمجموعة من الأزواج (x ، f (x)). للبساطة ، سننظر في الحجج الصحيحة فقط.

لتخزين كثير الحدود في تدوين معتاد ، يكون الهيكل الذي يحتوي على صفيف أو مجموعة من المعاملات كحقل مناسبًا. لتكون غير قابل للتغيير بالكامل ، فليكن هناك موكب. وهكذا ، فإن الكود الخاص بتعريف النوع التجريدي وبنية كثير الحدود وحساب قيمة كثير الحدود في نقطة معينة بسيط للغاية:
 abstract type AbstractPolynomial end """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N # NTuple{N, Type} -    N    end """ evpoly(p::Polynomial, z::Real) Evaluate polynomial `p` at `z` using the Horner's rule """ function evpoly(p::Polynomial, z::Real) ans = p.coeff[end] for idx = p.degree:-1:1 ans = p.coeff[idx] + z * ans end return ans end 


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

P(x)= sumk=0Ncknk(x)،


حيث n k ( x ) متعددات الحدود الأساسية ، n 0 ( x ) و k > 0

nk(x)= prodi=0k1(xxi)،


حيث x i هي عقد الاستيفاء.

من الصيغ أعلاه ، يمكن ملاحظة أن التخزين منظم بشكل ملائم في شكل مجموعة من العقد الاستيفائية x i ومعاملات c i ، ويمكن إجراء الحساب بطريقة مشابهة لمخطط Horner.
 """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N end """ evpoly(p::Polynomial, z::Real) Evaluate polynomial `p` at `z` using the Horner's rule """ function evpoly(p::InterpPolynomial, z::Real) ans = p.coeff[p.degree+1] for idx = p.degree:-1:1 ans = ans * (z - p.xval[idx]) + p.coeff[idx] end return ans end 

تسمى وظيفة حساب قيمة evpoly() الحدود في كلتا الحالتين نفس الشيء - evpoly() - لكنها تقبل أنواعًا مختلفة من الوسائط.

بالإضافة إلى وظيفة الحساب ، سيكون من الجيد أن تكتب دالة تنشئ كثير الحدود من البيانات المعروفة.

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

تضمن كتابة مُنشئ داخلي تضمن اتباع هذه القواعد أن جميع المتغيرات التي تم إنشاؤها من نوع InterpPolynomial ، على الأقل ، يمكن معالجتها بشكل صحيح من خلال وظيفة evpoly() .

نكتب منشئ متعدد الحدود العادية التي تأخذ مجموعة أحادية البعد أو مجموعة من المعاملات كمدخل. يستقبل مُنشئ متعدّد الحدود الاستيفاء عقد الاستيفاء والقيم المطلوبة فيها ويستخدم طريقة الفروق المقسّمة لحساب المعاملات.
 """ Polynomial <: AbstractPolynomial Polynomials written in the canonical form --- Polynomial(v::T) where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) Construct a `Polynomial` from the list of the coefficients. The coefficients are assumed to go from power 0 in the ascending order. If an empty collection is provided, the constructor returns a zero polynomial. """ struct Polynomial<:AbstractPolynomial degree::Int coeff::NTuple{N, Float64} where N function Polynomial(v::T where T<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}) #     /     P(x) ≡ 0 coeff = isempty(v) ? (0.0,) : tuple([Float64(x) for x in v]...) #   -   new #  -    return new(length(coeff)-1, coeff) end end """ InterpPolynomial <: AbstractPolynomial Interpolation polynomials in Newton's form --- InterpPolynomial(xsample::Vector{<:Real}, fsample::Vector{<:Real}) Construct an `InterpPolynomial` from a vector of points `xsample` and corresponding function values `fsample`. All values in `xsample` must be distinct. """ struct InterpPolynomial<:AbstractPolynomial degree::Int xval::NTuple{N, Float64} where N coeff::NTuple{N, Float64} where N function InterpPolynomial(xsample::X, fsample::F) where {X<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}, F<:Union{Vector{<:Real}, NTuple{<:Any, <:Real}}} #   ,    ,   f  ,   if !allunique(xsample) throw(DomainError("Cannot interpolate with duplicate X points")) end N = length(xsample) if length(fsample) != N throw(DomainError("Lengths of X and F are not the same")) end coeff = [Float64(f) for f in fsample] #     (Stoer, Bulirsch, Introduction to Numerical Analysis, . 2.1.3) for i = 2:N for j = 1:(i-1) coeff[i] = (coeff[j] - coeff[i]) / (xsample[j] - xsample[i]) end end new(N-1, tuple([Float64(x) for x in xsample]...), tuple(coeff...)) end end 

إلى جانب الجيل الفعلي من كثير الحدود ، سيكون من الجميل أن تكون قادرًا على إجراء العمليات الحسابية معهم.

نظرًا لأن العوامل الحسابية في جوليا هي وظائف عادية ، يضاف إليها ترميز اللانهاية على شكل سكر نحوي (التعبيرات a + b و +(a, b) صالحة ومتماثلة تمامًا) ، يتم التحميل الزائد بنفس الطريقة طرق إضافية لوظائفهم.

النقطة الدقيقة الوحيدة هي أن كود المستخدم يتم تشغيله من الوحدة Main (مساحة الاسم) ، وأن وظائف المكتبة القياسية موجودة في الوحدة النمطية Base ، لذلك عند التحميل الزائد ، يجب عليك إما استيراد الوحدة النمطية Base أو كتابة الاسم الكامل للوظيفة.

لذلك ، نضيف إضافة كثير الحدود مع عدد:
 # -   Base.+  , #    Base.:+,   " :+   Base" function Base.:+(p::Polynomial, x::Real) Polynomial(tuple(p.coeff[1] + x, p.coeff[2:end]...)) end function Base.:+(p::InterpPolynomial, x::Real) # ..           - #          . #       - #        fval::Vector{Float64} = [evpoly(p, xval) + x for xval in p.xval] InterpPolynomial(p.xval, fval) end #       function Base.:+(x::Real, p::AbstractPolynomial) return p + x end 

لإضافة اثنين من كثيرات الحدود العادية ، يكفي إضافة المعاملات ، وعند إضافة متعدد الحدود إلى الآخر ، يمكنك العثور على قيم المجموع في عدة نقاط وإنشاء استيفاء جديد منها.

 function Base.:+(p1::Polynomial, p2::Polynomial) #    ,      deg = max(p1.degree, p2.degree) coeff = zeros(deg+1) coeff[1:p1.degree+1] .+= p1.coeff coeff[1:p2.degree+1] .+= p2.coeff Polynomial(coeff) end function Base.:+(p1::InterpPolynomial, p2::InterpPolynomial) xmax = max(p1.xval..., p2.xval...) xmin = min(p1.xval..., p2.xval...) deg = max(p1.degree, p2.degree) #         #       xmid = 0.5 * xmax + 0.5 * xmin dx = 0.5 * (xmax - xmin) / cos(0.5 * π / (deg + 1)) chebgrid = [xmid + dx * cos((k - 0.5) * π / (deg + 1)) for k = 1:deg+1] fsample = [evpoly(p1, x) + evpoly(p2, x) for x in chebgrid] InterpPolynomial(chebgrid, fsample) end function Base.:+(p1::InterpPolynomial, p2::Polynomial) xmax = max(p1.xval...) xmin = min(p1.xval...) deg = max(p1.degree, p2.degree) xmid = 0.5 * xmax + 0.5 * xmin dx = 0.5 * (xmax - xmin) / cos(0.5 * π / (deg + 1)) chebgrid = [xmid + dx * cos((k - 0.5) * π / (deg + 1)) for k = 1:deg+1] fsample = [evpoly(p1, x) + evpoly(p2, x) for x in chebgrid] InterpPolynomial(chebgrid, fsample) end function Base.:+(p1::Polynomial, p2::InterpPolynomial) p2 + p1 end 

بنفس الطريقة ، يمكنك إضافة عمليات حسابية أخرى على كثير الحدود ، مما يؤدي إلى تمثيلها في الكود في تدوين رياضي طبيعي.

هذا كل شيء الآن. سأحاول الكتابة أكثر عن تنفيذ الطرق العددية الأخرى.

في الإعداد ، تم استخدام المواد التالية:
  1. وثائق لغة جوليا: docs.julialang.org
  2. منصة مناقشة لغة جوليا: discourse.julialang.org
  3. جيه ستوير ، و. مقدمة في التحليل العددي
  4. جوليا هب: habr.com/en/hub/ جوليا
  5. اعتقد جوليا: benlauwens.imtqy.com/ThinkJulia.jl/latest/book.html

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


All Articles