Vectors কনসেপ্টে আমরা বলেছিলাম যে "অ্যারেগুলোই Julia ভাষার মূল ভিত্তি" এবং একটি ভেক্টর হলো একমাত্রিক অ্যারে।
এ কথা মাথায় রেখে আমরা নিশ্চিতভাবেই আশা করতে পারি যে ভাষাটি ভেক্টর নিয়ে কিছু করার জন্য বহুমুখী ও শক্তিশালী অনেক উপায় দেবে, সেটার মানে যা-ই হোক না কেন।
পরিভাষা নিয়ে একটি কথা: এই লেখায় "ভেক্টর" নিয়ে অনেক কথা বলা হলেও এর অনেকটাই যেকোনো ইটারেবল টাইপের ক্ষেত্রেও খাটে: রেঞ্জ, টাপল, সেট এবং আরও নানা কিছু।
কিছু খুব সাধারণ ফাংশন ইনপুট হিসেবে একটি ভেক্টর নেয় এবং (একমাত্রিক ইনপুটের জন্য) আউটপুট হিসেবে একটি স্কেলার রিটার্ন করে।
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() যত খুশি ভেক্টরসদৃশ ইনপুট নেয় এবং টাপলের একটি ইটারেটর রিটার্ন করে।
সাধারণত সব ইনপুটের দৈর্ঘ্য একই থাকে। কোনোটির দৈর্ঘ্য কম হলে বাকিগুলোকে সবচেয়ে ছোটটির দৈর্ঘ্যে ছেঁটে ফেলা হয়: হয়তো আপনি এটাই চেয়েছিলেন, কিন্তু বেশিরভাগ ক্ষেত্রেই এটা আপনার কোডের একটি bug।
ধরুন আপনার একটি সংখ্যাসূচক ভেক্টর আছে এবং আপনি প্রতিটি মান থেকে 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
সাধারণভাবে, অসমান দৈর্ঘ্য একটি এরর, তবে তখন নয় যখন কোনোটির দৈর্ঘ্য ১ হয় (প্রকৃতপক্ষে, একটি "সিঙ্গেলটন" মাত্রা)।
[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 হলো v-এর মেমরি লোকেশনে w-এর একটি copy। 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
যদি আপনার রৈখিক বীজগণিতের পটভূমি থেকে থাকে, তাহলে (১) আপনি সাধারণ Exercism ব্যবহারকারী নন (কিন্তু এখানে আপনাকে খুব স্বাগতম!) এবং (২) আপনি হয়তো চিনতে পারবেন যে 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)