في مفهوم Vectors، قلنا إن «المصفوفات هي قلب لغة Julia»، وإن المتجه مصفوفة أحادية البعد.
وبناءً على ذلك، يمكننا أن نأمل بحق أن توفر اللغة طرقًا كثيرة ومتنوعة وقوية لـ_إنجاز الأمور_ بالمتجهات، أيًّا كان المقصود بذلك.
ملاحظة حول المصطلحات: رغم أن هذا المستند يتحدث كثيرًا عن «المتجهات»، فإن كثيرًا مما فيه ينطبق أيضًا على أي نوع قابل للتكرار: النطاقات، وtuple، والمجموعات، وأنواع أخرى متنوعة.
هناك دوال بسيطة جدًا تأخذ متجهًا كمدخل، وتُرجع (للمدخل أحادي البعد) قيمة قياسية.
v = [2, 3, 4]
length(v) # => 3
sum(v) # => 9
وعندما نصل إلى مفهوم المصفوفات متعددة الأبعاد، سيصبح من الأوضح أن هذا تقليل للأبعاد لا إرجاع لقيمة قياسية بالضرورة. وإن لم يكن ذلك مفهومًا لك، فلا تقلق بشأنه الآن.
وهناك دوال كثيرة أخرى من هذا النوع.
انظر مفهوم Statistics لبعض الأمثلة.
وهناك أيضًا دوال تعمل على عدة متجهات، مثل zip (المفيد جدًا).
julia> z = zip( 1:3, ['a', 'b', 'c'], ["I", "make", "tuples"] )
zip([1, 2, 3], ['a', 'b', 'c'], ["I", "make", "tuples"])
# convert iterator to vector
julia> collect(z)
3-element Vector{Tuple{Int64, Char, String}}:
(1, 'a', "I")
(2, 'b', "make")
(3, 'c', "tuples")
تأخذ zip() أي عدد من المدخلات الشبيهة بالمتجهات، وتُرجع مُكرِّرًا من الـ tuple.
وعادةً ما تكون المدخلات جميعها بالطول نفسه. وإذا كان أحدها أقصر، قُطعت البقية إلى الطول الأقصر: ربما كان هذا ما قصدته، لكنه في الغالب خطأ في الكود.
لنفترض أن لديك متجهًا عدديًا وتريد طرح 0.5 من كل قيمة.
julia> v = [1.2, 1.5, 1.7]
3-element Vector{Float64}:
1.2
1.5
1.7
julia> v - 0.5
ERROR: MethodError: no method matching -(::Vector{Float64}, ::Float64)
فشل ذلك، فما رأيك بطرح متجه آخر؟
julia> v - [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
نجح الأمر، لكنه مرهق جدًا ويستهلك ذاكرة كبيرة كلما طالت المتجهات.
وبحسب ما وصلت إليه في المنهج، يمكنك على الأرجح التفكير في طرق أخرى:
[x - 0.5 for x in v] يعطي النتيجة المطلوبة (على طريقة Python).map(x -> x - 0.5, v) يعمل أيضًا (على طريقة Haskell، وإن كان شائعًا في لغات كثيرة).لحسن الحظ، تمتلك Julia نقطة «سحرية» تحل هذه المشكلة ببساطة شديدة: كل ما تحتاجه هو v .- 0.5.
وسيوضح القسم التالي السبب.
إذن، يفشل v - 0.5 بينما ينجح v .- 0.5، وعلينا أن نفهم ما تفعله هذه النقطة.
إنها تفعل أمرين يجتمعان ليعطيا النتيجة المطلوبة.
أولًا، إضافة نقطة قبل أي عامل يُكتب بين طرفَي العملية تعني «طبّق هذه العملية على كل عنصر على حدة».
وبالمثل، إضافة نقطة بعد اسم الدالة تجعلها تُطبَّق على العناصر واحدًا واحدًا، حتى لو كانت الدالة مكتوبة لمدخلات قياسية.
julia> sqrt.([1, 4, 9])
3-element Vector{Float64}:
1.0
2.0
3.0
وعلى سبيل الاستطراد، ليست العوامل البينية إلا صياغة نحوية مختصرة للدالة الكامنة وراءها.
وهذا يعني، على سبيل المثال، أن المفسّر يترجم [1, 5, 10] .% 3 إلى mod.([1, 5, 10], 3)، ثم تُنفَّذ صيغة mod. (وكلا الصيغتين تُرجع [1, 2, 1]).
رأينا في مثال سابق أنه يمكننا طرح متجهات متساوية الطول، لكن اعلم أن .- عامل أكثر أمانًا من - لأنه يوضّح النية في التطبيق عنصرًا عنصرًا.
julia> v .- [0.5, 0.5, 0.5]
3-element Vector{Float64}:
0.7
1.0
1.2
وماذا عن المتجهات غير المتساوية في الطول؟
julia> v .- [0.5, 0.5]
ERROR: DimensionMismatch: arrays could not be broadcast to a common size
julia> v .- [0.5,]
3-element Vector{Float64}:
0.7
1.0
1.2
بشكل عام، الأطوال غير المتساوية تُعدّ خطأً، إلا إذا كان طول أحدهما 1 (تقنيًا، بُعد «مفرد»).
وتُوسَّع القيم المفردة مثل [0.5,] أو 0.5 وحدها تلقائيًا إلى الطول اللازم عن طريق التكرار.
وهذا هو جوهر broadcasting.
ومن يقلق من استهلاك الذاكرة بسبب هذا «التكرار» يمكنه أن يطمئن: فهو منفَّذ بطريقة فعّالة جدًا لا تنسخ القيم فعليًا في الذاكرة.
وعلى المبرمجين المطلعين على البث في لغات أخرى أن يلاحظوا أن نهج Julia (في معظمه) مشابه لـ NumPy، لكنه أقل تسامحًا بكثير مع عدم تطابق الأحجام مقارنةً بلغة R.
إذا كان استهلاك الذاكرة يقلقك، فإن العمليات في المكان طريقة شائعة للبحث عن تقليل التخصيصات.
غير أن عمليات البث في الأمثلة أعلاه تُنشئ Vector جديدًا بدل تعديل المتجه الأصلي.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
1
2
3
لتعديل v في المكان، يجب بثّ التعديل.
julia> v .= v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v .+= 1 # equivalent operation to above
3-element Vector{Int64}:
3
4
5
julia> v
3-element Vector{Int64}:
3
4
5
لكن انتبه! النقطة قبل عامل الإسناد .= مهمة.
julia> v = [1, 2, 3];
julia> v = v .+ 1
3-element Vector{Int64}:
2
3
4
julia> v
3-element Vector{Int64}:
2
3
4
يبدو أن هذا نجح بالطريقة نفسها، لكن هنا أنشأ v .+ 1 متجهًا جديدًا [2, 3, 4] ثم أسنده إلى المتغير v، تاركًا المتجه الأولي [1, 2, 3] في الذاكرة لكي تتولّى عملية جمع القمامة التخلص منه.
وهذا ينتهي باستهلاك ضعف الذاكرة التي استهلكها المثال السابق الذي أعاد استخدام الذاكرة المخصَّصة للمتجه الأولي.
وبالمثل، هناك تفاصيل دقيقة مرتبطة بذلك عند البث مع متجهات بالحجم نفسه.
على سبيل المثال، إذا كان لدينا متجهان v و w بالحجم نفسه:
v .= w ينتج copy من w في موقع الذاكرة الخاص بـ v. والتغييرات اللاحقة في v لا تؤثر على w والعكس صحيح.v = w ينتج مؤشرًا آخر إلى موقع الذاكرة الخاص بـ w باسم v. والتغييرات اللاحقة في v تنعكس على w والعكس صحيح.هذا القسم الفرعي مليء بالرياضيات نوعًا ما، لذا لا يُتوقع من معظم الطلاب فهمه حقًا. لكنه تحذير مفيد قد يساعدك في تصحيح الأخطاء عندما ترى رسائل خطأ غير متوقعة.
julia> v = [1, 2, 3]
3-element Vector{Int64}:
1
2
3
julia> v * v
ERROR: MethodError: no method matching *(::Vector{Int64}, ::Vector{Int64})
# look, no commas
julia> u = [1 2 3]
1×3 Matrix{Int64}:
1 2 3
julia> u * v
1-element Vector{Int64}:
14
julia> v * u
3×3 Matrix{Int64}:
1 2 3
2 4 6
3 6 9
إذا كانت لديك خلفية في الجبر الخطي، فأنت (1) لست مستخدمًا نموذجيًا لـ Exercism (لكنك مرحّب بك هنا!) و(2) قد تدرك أن v متجه عمودي، وأن u متجه صف، وأن u * v هو الجداء الداخلي، وأن v * u هو الجداء الخارجي.
تتبع Julia قواعد الرياضيات، في هذا كما في كل شيء.
ولكل من سواهم: أرجو فقط أن تفهم لماذا نوصيك بأن تستخدم دائمًا العوامل المنقوطة في الحسابات التي تجريها عنصرًا عنصرًا: v .* v يعمل تمامًا كما تتوقع، ليعطي [1, 4, 9].
سبق أن ناقشنا في مفاهيم سابقة اختيار عناصر المتجه برقم الفهرس.
a = collect('A':'Z') # => 26-element Vector{Char}
# index with an integer
a[2] # => 'B'
# index with a range
a[12:2:18] # => ['L', 'N', 'P, 'R']
# index with another vector
a[ [1, 3, 5] ] # => ['A', 'C', 'E']
من الممكن أيضًا اختيار العناصر التي تحقق تعبيرًا منطقيًا ما (تقنيًا، «محمول»). وهذا يتطلب عادةً استخدام البث.
julia> a[a .< 'D']
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
وفي التعبيرات الأكثر تعقيدًا تكثر النقاط (لكنها صغيرة وسهلة الكتابة).
julia> a[a .< 'D' .|| a .> 'W']
6-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)
'X': ASCII/Unicode U+0058 (category Lu: Letter, uppercase)
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'Z': ASCII/Unicode U+005A (category Lu: Letter, uppercase)
والتذكير بأن «المتجه» يمكن في الواقع أن يكون أي نوع مرتب قابل للتكرار ومناسب، مثل النطاق:
julia> n = 3:10
3:10
julia> n[isodd.(n)]
4-element Vector{Int64}:
3
5
7
9
وداخليًا، يُحوَّل المحمول إلى BitVector يُستخدم بعد ذلك كفهرس.
julia> condition = a .< 'D'
26-element BitVector:
1
1
1
0
# display truncated
julia> a[condition]
3-element Vector{Char}:
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'B': ASCII/Unicode U+0042 (category Lu: Letter, uppercase)
'C': ASCII/Unicode U+0043 (category Lu: Letter, uppercase)