مسیرها
/
Julia
Julia
/
تمرین‌ها
/
مواجهه‌ها
مواجهه‌ها

مواجهه‌ها

تمرین یادگیری

مقدمه

تا اینجای برنامه‌ی درسی، تعریف‌های زیادی از تابع دیده‌اید. معمولاً این تعریف‌ها بدون توضیح چندانی درباره‌ی خروجی در REPL ارائه می‌شدند:

julia> f(x) = x^2
f (generic function with 1 method)

چرا "generic function with 1 method"؟

فرض کنید می‌خواهیم درباره‌ی نوع آرگومان‌ها دقیق‌تر باشیم.

julia> f(x::Integer) = x^2
f (generic function with 2 methods)

julia> f(x::Real) = x^2
f (generic function with 3 methods)

یا می‌توانیم تعداد آرگومان‌ها را تغییر دهیم:

julia> f(x, y) = x * y
f (generic function with 4 methods)

به نظر می‌رسد که تعریف‌های جدید از یک تابع، «روش» نامیده می‌شوند. روش‌ها به یک فهرست اضافه می‌شوند، نه این‌که تعریف قدیمی را بازنویسی کنند.

می‌توانیم به‌راحتی روش‌ها را فهرست کنیم:

julia> methods(f)  # output edited for brevity
# 4 methods for generic function "f" from Main:
 [1] f(x, y)
 [2] f(x::Integer)
 [3] f(x::Real)
 [4] f(x)

اگر تابع f را فراخوانی کنیم، جولیا به شکلی می‌داند که کدام روش را باید استفاده کند:

julia> f(2.5)  # Float64
6.25

julia> f(2)  # Int64
4

julia> f(2, 3)  # Int64, Int64
6

تابع چیست؟

بر اساس مستندات، تابع شیئی است که یک تاپل از آرگومان‌ها را به یک مقدار بازگشتی نگاشت می‌کند.

ما معمولاً آرگومان‌های تابع در f(2, 3) را دو عنصر جداگانه در نظر می‌گیریم، اما کامپایلر آن‌ها را یک تاپل می‌بیند. این موضوع حتی برای یک آرگومان هم درست است (هرچند برای روشن‌تر شدن ماجرا باید یک ویرگول اضافه کنیم):

julia> typeof( (2, 3) )
Tuple{Int64, Int64}

julia> typeof( (2,) )
Tuple{Int64}

اگر تابع را مثل یک جعبه‌ی سیاه در نظر بگیریم، می‌توان گفت که یک تاپل وارد می‌شود و یک مقدار بازگشتی بیرون می‌آید. به شکل رسمی‌تر، تاپل به یک مقدار بازگشتی نگاشت می‌شود.

توابع چگونه تعداد و نوع‌های مختلف آرگومان‌ها را مدیریت می‌کنند؟

معمولاً به این «چندریختی» می‌گویند.

این موضوع چندین دهه است که یکی از مباحث مهم علوم کامپیوتر بوده و رویکردهای مختلفی برای آن اتخاذ شده است.

رویکرد جولیا غیرمعمول است، هرچند بی‌نظیر نیست.

  • توابع «عمومی» هستند، یعنی می‌توان چندین روش به آن‌ها متصل کرد.
  • توابع تا زمانی که روشی به آن‌ها متصل نشده، فقط یک اسم‌اند.
  • روش‌ها را می‌توان به هر تعداد که لازم باشد اضافه کرد، حتی در تعداد زیاد (عملگر + در حال حاضر تابعی است با ۱۶۱ روش).
  • وقتی تابعی فراخوانی می‌شود، جولیا «امضای فراخوانی» را بررسی می‌کند: تعداد آرگومان‌ها و نوع فعلی هرکدام.
  • روشی که بیشترین تطابق را با امضای فراخوانی داشته باشد، انتخاب می‌شود.
    • به‌طور کلی، تعداد آرگومان‌ها باید دقیقاً یکسان باشد (هرچند عملگر splat/slurp یعنی ... می‌تواند تا حدی از این محدودیت عبور کند).
    • روش‌هایی که نوع‌هایشان دقیقاً با امضای فراخوانی مطابقت دارد، ترجیح داده می‌شوند.
    • در صورت لزوم، جولیا ابرنوع‌های هر آرگومان در امضای فراخوانی را بررسی می‌کند تا نزدیک‌ترین و خاص‌ترین روش را پیدا کند.
  • روش انتخاب‌شده با آرگومان‌های داده‌شده dispatched می‌شود و نتیجه‌ای برمی‌گرداند.
  • اگر روش مناسبی پیدا نشود، خطای MethodError صادر می‌شود.

توجه کنید که جولیا نوع‌بندی پویا دارد، بنابراین اعزام روش در زمان اجرا رخ می‌دهد.

اعزام چندگانه: یک مثال

بخش قبلی پر از حرف بود، پس بیایید این ایده‌ها را به کد تبدیل کنیم. برای سادگی، توابع زیر فقط یک آرگومان دارند، اما پیدا کردن توابعی با ده‌ها آرگومان غیرمعمول نیست (مثلاً در بسته‌های رسم نمودار یا یادگیری ماشین).

برای شروع، چند نوع سفارشی تعریف کنید که شکل‌های هندسی را نمایش می‌دهند.

julia> abstract type Shape end

julia> struct Circle <: Shape
           radius::Float64
       end

julia> struct Square <: Shape
           side::Float64
       end

julia> struct Rectangle <: Shape
           length::Float64
           width::Float64
       end

در مرحله‌ی بعد می‌توانیم روش‌هایی برای محاسبه‌ی مساحت هر شکل تعریف کنیم. هرکدام یک آرگومان می‌گیرد که در هر مورد از یک نوع مشخص و کاربرساخته‌ی متفاوت است.

julia> area(c::Circle) = π * c.radius^2
area (generic function with 1 method)

julia> area(s::Square) = s.side^2
area (generic function with 2 methods)

julia> area(r::Rectangle) = r.length * r.width
area (generic function with 3 methods)

حالا می‌توانیم چند شکل بسازیم و مساحت‌ها را محاسبه کنیم و برای استفاده از فرمول درست هرکدام، به سازوکار اعزام جولیا تکیه کنیم.

julia> circle = Circle(2.3)
Circle(2.3)

julia> area(circle)  # call signature Tuple{Circle}
16.619025137490002

julia> square = Square(1.6)
Square(1.6)

julia> area(square)  # call signature Tuple{Square}
2.5600000000000005

julia> rectangle = Rectangle(1.4, 2.1)
Rectangle(1.4, 2.1)

julia> area(rectangle)  # call signature Tuple{Rectangle}
2.94

روش‌های پارامتری

امضاهای روش می‌توانند مقادیر نوعی ساختگی بگیرند، تقریباً به همان شیوه‌ای که نوع‌های پارامتری دارند.

نحوه‌ی نگارش آن متمایز است و به یک عبارت where نیاز دارد:

julia> same_type(v::Vector{T}, x::T) where {T} = true
same_type (generic function with 1 method)

julia> same_type([1, 2], 3)
true

julia> same_type([1, 2], "three")
ERROR: MethodError: no method matching same_type(::Vector{Int64}, ::String)

مثال بالا نیاز دارد که هر دو آرگومان از یک نوع T باشند، هر نوعی که باشد.

می‌توانیم با گذاشتن یک محدودیت روی T دقیق‌تر عمل کنیم:

julia> same_type(v::Vector{T}, x::T) where {T<:Number} = true
same_type (generic function with 2 methods)

با این روش، فقط نوع‌های عددی یکسان true برمی‌گردانند.

چند متغیر ساختگی هم امکان‌پذیر است که با ویرگول در عبارت where از هم جدا می‌شوند.

julia> myfunc(a::S, b::T) where {S, T} = true
myfunc (generic function with 1 method)

نتیجه‌گیری

اعزام چندگانه در جولیا بسیار انعطاف‌پذیر و بسیار کارآمد است، به همین دلیل بسیار گسترده استفاده می‌شود.

همان‌طور که مثال ۱۶۱ روش برای + نشان می‌دهد، برنامه‌نویسان جولیا تلاش می‌کنند روش‌هایی با نوع‌های کاملاً مشخص تعریف کنند. جمع کردن دو عدد صحیح در سطح بایت‌کد با جمع کردن دو عدد اعشاری بسیار متفاوت است و جمع کردن یک عدد صحیح با یک عدد اعشاری، پیچیدگی دیگری به نام ارتقای نوع را اضافه می‌کند.

هرچه نوع مشخص‌تر باشد، کامپایلر می‌تواند بهینه‌سازی بیشتری انجام دهد: در یک تمرین کوچک در Exercism اهمیت چندانی ندارد، اما برای کارایی در شبیه‌سازی‌های عددی بزرگی که جولیا برای آن‌ها طراحی شده حیاتی است.

اگر می‌دانید یک روش چه ورودی‌هایی دریافت می‌کند، لطفاً به کامپایلر بگویید!

دستورالعمل‌ها

در این تمرین، شبیه‌سازی‌ای از برخوردها را پیاده‌سازی می‌کنید. این کار شما را با مبانی multiple dispatch، پارادایم اصلی Julia، آشنا می‌کند.

For this exercise, all function definitions should be a single-line declaration with no if/else logic.

به‌طور کلی، یک برخورد یعنی یک موجودیت a با موجودیت دیگری b روبه‌رو می‌شود و به آن واکنش نشان می‌دهد.

encounter(a, b) = "$(name(a)) meets $(name(b)) and $(meets(a, b))."

در ابتدا، شبیه‌سازی می‌کنیم که وقتی گربه‌ها و سگ‌ها به هم می‌رسند چه اتفاقی می‌افتد.

1. تعریف نوع‌های سفارشی

ابتدا یک نوع انتزاعی Pet اضافه کنید.

سپس نوع‌های Cat و Dog را به‌عنوان زیرنوع‌های Pet تعریف کنید. هر کدام تنها یک فیلد name دارند.

2. گرفتن اسم حیوان خانگی

تابعی به اسم name() پیاده‌سازی کنید که اسم یک حیوان خانگی را برمی‌گرداند.

julia> fido = Dog("Fido")
Dog("Fido")

julia> name(fido)
"Fido"

3. تعریف اینکه وقتی گربه‌ها و سگ‌ها به هم می‌رسند چه اتفاقی می‌افتد

متدهای meets() را برای برخوردهای زیر پیاده‌سازی کنید:

  • وقتی دو سگ به هم می‌رسند، واکنش‌شان sniff است.
  • وقتی گربه‌ای با سگی روبه‌رو می‌شود، واکنش‌اش به سگ hisses است.
  • وقتی سگی با گربه‌ای روبه‌رو می‌شود، واکنش‌اش به گربه chases است.
  • وقتی دو گربه به هم می‌رسند، واکنش‌شان slink است.
julia> ginger = Cat("Ginger")
Cat("Ginger")

julia> meets(ginger, fido)
"hisses"

4. تعریف یک برخورد بین دو موجودیت

تابع encounter() را با یک متد پیاده‌سازی کنید تا گزارش دهد وقتی دو موجودیت که اسم دارند به هم می‌رسند چه اتفاقی می‌افتد.

julia> encounter(ginger, fido)
"Ginger meets Fido and hisses."

5. تعریف یک واکنش پیش‌فرض برای برخوردهای میان حیوان‌های خانگی

چه اتفاقی می‌افتد اگر در راه رفتن‌شان با حیوان خانگی دیگری روبه‌رو شوند، مثلاً یک اسب؟

  • اگر حیوان خانگی با حیوان خانگی دیگری روبه‌رو شود که آن را نمی‌شناسد، واکنش‌اش is cautious است.
julia> bella = Horse("Bella")
Horse("Bella")

julia> encounter(fido, bella)
"Fido meets Bella and is cautious."

6. تعریف یک پیش‌فرض برای وقتی که حیوان خانگی با چیزی روبه‌رو می‌شود که نمی‌شناسد

موارد بسیار دیگری هم هست که حیوان‌های خانگی ممکن است با آن‌ها روبه‌رو شوند و خودشان حیوان خانگی نیستند: ماشین‌ها، انسان‌ها، گیاهان، بلایای طبیعی، سیارک‌ها… آن وقت چه اتفاقی می‌افتد؟

  • اگر حیوان خانگی با چیزی روبه‌رو شود که هرگز ندیده است، واکنش‌اش runs away است.
julia> colnago = Bicycle("Colnago")
Bicycle("Colnago")

julia> encounter(ginger, colnago)
"Ginger meets Colnago and runs away."

7. تعریف یک پیش‌فرض کلی

برخوردهای بسیار دیگری هم هست که ممکن است در شبیه‌سازی ما رخ دهد. یک ماشین با یک سگ روبه‌رو می‌شود، دو الکترون با هم برخورد می‌کنند و بر هم اثر می‌گذارند…

پوشش دادن همه‌ی برخوردها از پیش ممکن نیست، بنابراین یک پیش‌فرض کلی پیاده‌سازی می‌کنیم.

  • اگر دو مورد یا موجود ناشناخته با هم روبه‌رو شوند، nothing happens.
julia> γ1 = Photon("γ1")
Photon("γ1")

julia> γ2 = Photon("γ2")
Photon("γ2")

julia> encounter(γ1, γ2)
"γ1 meets γ2 and nothing happens." 
# This is true, photons just pass through each other.
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Julia Exercism

آماده‌اید مواجهه‌ها را شروع کنید؟

در Exercism ثبت‌نام کنید تا Julia را همراه با 35 مفهوم128 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.