Conversion and Promotion
تتمتع جوليا بنظام لترقية معاملات العمليات الرياضية إلى نوع مشترك، والذي تم ذكره في أقسام مختلفة، بما في ذلك Integers and Floating-Point Numbers، Mathematical Operations and Elementary Functions، Types، و Methods. في هذا القسم، نشرح كيف يعمل نظام الترقية هذا، بالإضافة إلى كيفية توسيعه ليشمل أنواع جديدة وتطبيقه على وظائف بخلاف العمليات الرياضية المدمجة. تقليديًا، تقع لغات البرمجة في معسكرين فيما يتعلق بترقية معاملات الحساب:
- الترقية التلقائية لأنواع الرياضيات المدمجة والمشغلين. في معظم اللغات، يتم ترقية الأنواع الرقمية المدمجة، عند استخدامها كعوامل لمشغلين رياضيين بصيغة infix، مثل
+
،-
،*
، و/
، تلقائيًا إلى نوع مشترك لإنتاج النتائج المتوقعة. C و Java و Perl و Python، على سبيل المثال، جميعها تحسب بشكل صحيح مجموع1 + 1.5
كقيمة عائمة2.5
، على الرغم من أن أحد العوامل لـ+
هو عدد صحيح. هذه الأنظمة مريحة ومصممة بعناية كافية بحيث تكون عمومًا غير مرئية تقريبًا للمبرمج: نادرًا ما يفكر أي شخص بوعي في حدوث هذه الترقية عند كتابة مثل هذا التعبير، ولكن يجب على المترجمات والمفسرات إجراء تحويل قبل الجمع حيث لا يمكن إضافة الأعداد الصحيحة والقيم العائمة كما هي. وبالتالي، فإن القواعد المعقدة لمثل هذه التحويلات التلقائية هي جزء لا مفر منه من المواصفات والتنفيذات لمثل هذه اللغات. - لا ترقية تلقائية. يتضمن هذا المعسكر Ada و ML - وهما لغتان "صارمتان" ذات نوع ثابت. في هذه اللغات، يجب على المبرمج تحديد كل تحويل بشكل صريح. وبالتالي، فإن التعبير المثال
1 + 1.5
سيكون خطأ في الترجمة في كل من Ada و ML. بدلاً من ذلك، يجب على المرء كتابةreal(1) + 1.5
، محولًا بشكل صريح العدد الصحيح1
إلى قيمة عائمة قبل إجراء الجمع. ومع ذلك، فإن التحويل الصريح في كل مكان غير مريح للغاية، حتى أن Ada لديها بعض الدرجة من التحويل التلقائي: يتم ترقية الثوابت الصحيحة إلى نوع العدد الصحيح المتوقع تلقائيًا، ويتم ترقية الثوابت العائمة بشكل مشابه إلى الأنواع العائمة المناسبة.
من ناحية ما، تقع جوليا في فئة "عدم الترقية التلقائية": المشغلون الرياضيون هم مجرد دوال بصياغة خاصة، ولا يتم تحويل معاملات الدوال تلقائيًا. ومع ذلك، يمكن ملاحظة أن تطبيق العمليات الرياضية على مجموعة واسعة من أنواع المعاملات المختلطة هو مجرد حالة متطرفة من التوزيع المتعدد المتعدد الأشكال - وهو شيء يتناسب بشكل خاص مع أنظمة التوزيع والنوع في جوليا. تظهر "الترقية" التلقائية للمعاملات الرياضية ببساطة كتطبيق خاص: تأتي جوليا مع قواعد توزيع شاملة محددة مسبقًا للمشغلين الرياضيين، يتم استدعاؤها عندما لا توجد تنفيذات محددة لبعض تركيبات أنواع المعاملات. تقوم هذه القواعد الشاملة أولاً بترقية جميع المعاملات إلى نوع مشترك باستخدام قواعد ترقية قابلة للتعريف من قبل المستخدم، ثم تستدعي تنفيذًا متخصصًا للمشغل المعني للقيم الناتجة، التي أصبحت الآن من نفس النوع. يمكن أن تشارك الأنواع المعرفة من قبل المستخدم بسهولة في هذا النظام الترويجي من خلال تعريف طرق للتحويل إلى ومن أنواع أخرى، وتوفير عدد قليل من قواعد الترقية التي تحدد الأنواع التي يجب أن تترقى إليها عند الاختلاط مع أنواع أخرى.
Conversion
الطريقة القياسية للحصول على قيمة من نوع معين T
هي استدعاء مُنشئ النوع، T(x)
. ومع ذلك، هناك حالات يكون من الملائم فيها تحويل قيمة من نوع إلى آخر دون أن يطلب المبرمج ذلك صراحة. مثال على ذلك هو تعيين قيمة في مصفوفة: إذا كانت A
هي Vector{Float64}
، يجب أن تعمل العبارة A[1] = 2
عن طريق تحويل 2
تلقائيًا من Int
إلى Float64
، وتخزين النتيجة في المصفوفة. يتم ذلك عبر دالة convert
.
تأخذ دالة convert
عمومًا وسيطين: الأول هو كائن نوع والثاني هو قيمة لتحويلها إلى ذلك النوع. القيمة المعادة هي القيمة المحولة إلى مثيل من النوع المعطى. أبسط طريقة لفهم هذه الدالة هي رؤيتها في العمل:
julia> x = 12
12
julia> typeof(x)
Int64
julia> xu = convert(UInt8, x)
0x0c
julia> typeof(xu)
UInt8
julia> xf = convert(AbstractFloat, x)
12.0
julia> typeof(xf)
Float64
julia> a = Any[1 2 3; 4 5 6]
2×3 Matrix{Any}:
1 2 3
4 5 6
julia> convert(Array{Float64}, a)
2×3 Matrix{Float64}:
1.0 2.0 3.0
4.0 5.0 6.0
لا يمكن دائمًا إجراء التحويل، وفي هذه الحالة يتم طرح MethodError
مما يشير إلى أن convert
لا يعرف كيفية إجراء التحويل المطلوب:
julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
[...]
تعتبر بعض اللغات تحليل السلاسل كأرقام أو تنسيق الأرقام كسلاسل تحويلات (حتى أن العديد من اللغات الديناميكية ستقوم بإجراء التحويل تلقائيًا). هذه ليست الحالة في جوليا. على الرغم من أن بعض السلاسل يمكن تحليلها كأرقام، إلا أن معظم السلاسل ليست تمثيلات صالحة للأرقام، وفقط مجموعة محدودة جدًا منها هي كذلك. لذلك في جوليا يجب استخدام الدالة المخصصة parse
لأداء هذه العملية، مما يجعلها أكثر وضوحًا.
When is convert
called?
تستدعي بنى اللغة التالية convert
:
- تعيين مصفوفة يتحول إلى نوع عنصر المصفوفة.
- تعيين قيمة لحقل من كائن يتحول إلى النوع المعلن للحقل.
- إنشاء كائن باستخدام
new
يحول إلى أنواع الحقول المعلنة للكائن. - تعيين قيمة لمتغير مع نوع محدد (مثل
local x::T
) يتحول إلى ذلك النوع. - تقوم الدالة ذات نوع الإرجاع المعلن بتحويل قيمة الإرجاع الخاصة بها إلى ذلك النوع.
- تمرير قيمة إلى
ccall
يحولها إلى نوع الوسيطة المقابل.
Conversion vs. Construction
لاحظ أن سلوك convert(T, x)
يبدو متطابقًا تقريبًا مع T(x)
. في الواقع، هو عادةً كذلك. ومع ذلك، هناك فرق دلالي رئيسي: نظرًا لأن convert
يمكن استدعاؤه بشكل ضمني، فإن طرقه مقيدة بالحالات التي تعتبر "آمنة" أو "غير مفاجئة". سيقوم convert
فقط بالتحويل بين الأنواع التي تمثل نفس النوع الأساسي من الأشياء (مثل: تمثيلات مختلفة للأرقام، أو ترميزات مختلفة للسلاسل النصية). كما أنه عادةً ما يكون بلا فقد؛ يجب أن يؤدي تحويل قيمة إلى نوع مختلف ثم العودة مرة أخرى إلى نفس القيمة بالضبط.
هناك أربعة أنواع عامة من الحالات حيث تختلف المُنشئات عن convert
:
Constructors for types unrelated to their arguments
بعض المنشئين لا ينفذون مفهوم "التحويل". على سبيل المثال، Timer(2)
ينشئ مؤقتًا لمدة ثانيتين، وهو ليس حقًا "تحويلًا" من عدد صحيح إلى مؤقت.
Mutable collections
convert(T, x)
من المتوقع أن تعيد x
الأصلية إذا كانت x
بالفعل من النوع T
. على النقيض من ذلك، إذا كان T
نوع مجموعة قابلة للتغيير، فإن T(x)
يجب أن تنشئ دائمًا مجموعة جديدة (تقوم بنسخ العناصر من x
).
Wrapper types
بالنسبة لبعض الأنواع التي "تغلف" قيمًا أخرى، قد يقوم المُنشئ بتغليف حجته داخل كائن جديد حتى لو كانت بالفعل من النوع المطلوب. على سبيل المثال، Some(x)
تغلف x
للإشارة إلى أن قيمة موجودة (في سياق قد تكون النتيجة Some
أو nothing
). ومع ذلك، قد تكون x
نفسها الكائن Some(y)
، وفي هذه الحالة تكون النتيجة Some(Some(y))
، مع مستويين من التغليف. من ناحية أخرى، ستقوم convert(Some, x)
ببساطة بإرجاع x
لأنها بالفعل Some
.
Constructors that don't return instances of their own type
في حالات نادرة جدًا قد يكون من المنطقي أن يُرجع المُنشئ T(x)
كائنًا ليس من نوع T
. قد يحدث هذا إذا كان نوع التغليف هو عكس نفسه (على سبيل المثال Flip(Flip(x)) === x
)، أو لدعم صيغة استدعاء قديمة من أجل التوافق مع الإصدارات السابقة عندما يتم إعادة هيكلة مكتبة. لكن يجب أن تُرجع convert(T, x)
دائمًا قيمة من نوع T
.
Defining New Conversions
عند تعريف نوع جديد، يجب في البداية تعريف جميع طرق إنشائه كمنشئات. إذا أصبح من الواضح أن التحويل الضمني سيكون مفيدًا، وأن بعض المنشئات تلبي معايير "السلامة" المذكورة أعلاه، فيمكن إضافة طرق convert
. عادةً ما تكون هذه الطرق بسيطة جدًا، حيث تحتاج فقط إلى استدعاء المنشئ المناسب. قد يبدو هذا التعريف كما يلي:
import Base: convert
convert(::Type{MyType}, x) = MyType(x)
نوع الوسيط الأول لهذه الطريقة هو Type{MyType}
، والوحيد الذي يوجد منه هو MyType
. وبالتالي، يتم استدعاء هذه الطريقة فقط عندما يكون الوسيط الأول هو قيمة النوع MyType
. لاحظ الصيغة المستخدمة للوسيط الأول: اسم الوسيط محذوف قبل رمز ::
، ويتم إعطاء النوع فقط. هذه هي الصيغة في جوليا لوسيط دالة يتم تحديد نوعه ولكن لا يحتاج قيمته إلى الإشارة إليه بالاسم.
تعتبر جميع حالات بعض الأنواع المجردة بشكل افتراضي "متشابهة بما فيه الكفاية" بحيث يتم توفير تعريف عالمي لـ convert
في قاعدة جوليا. على سبيل المثال، ينص هذا التعريف على أنه من الصحيح convert
أي نوع من Number
إلى أي نوع آخر عن طريق استدعاء مُنشئ ذو وسيط واحد:
convert(::Type{T}, x::Number) where {T<:Number} = T(x)::T
هذا يعني أن أنواع Number
الجديدة تحتاج فقط إلى تعريف البانيات، حيث سيتولى هذا التعريف التعامل مع convert
نيابةً عنها. كما يتم توفير تحويل الهوية للتعامل مع الحالة التي يكون فيها الوسيط بالفعل من النوع المطلوب:
convert(::Type{T}, x::T) where {T<:Number} = x
توجد تعريفات مشابهة لـ AbstractString
، AbstractArray
، و AbstractDict
.
Promotion
تشير الترقية إلى تحويل قيم من أنواع مختلطة إلى نوع مشترك واحد. على الرغم من أنه ليس ضروريًا بشكل صارم، إلا أنه يُفترض عمومًا أن النوع المشترك الذي يتم تحويل القيم إليه يمكنه تمثيل جميع القيم الأصلية بدقة. من هذه الناحية، فإن مصطلح "الترقية" مناسب حيث يتم تحويل القيم إلى نوع "أكبر" - أي نوع يمكنه تمثيل جميع قيم الإدخال في نوع مشترك واحد. من المهم، مع ذلك، عدم الخلط بين هذا وبين التوريث (الهيكلي) في البرمجة الكائنية، أو مفهوم جوليّا عن الأنواع الفائقة المجردة: الترقية لا علاقة لها بهرم الأنواع، وكل شيء يتعلق بالتحويل بين تمثيلات بديلة. على سبيل المثال، على الرغم من أنه يمكن تمثيل كل قيمة Int32
أيضًا كقيمة Float64
، إلا أن Int32
ليست نوعًا فرعيًا من Float64
.
يتم تنفيذ الترقية إلى نوع "أكبر" مشترك في جوليا بواسطة دالة promote
، التي تأخذ أي عدد من الوسائط، وتعيد مجموعة من نفس عدد القيم، المحولة إلى نوع مشترك، أو ترمي استثناء إذا لم يكن من الممكن الترقية. أكثر حالات الاستخدام شيوعًا للترقية هي تحويل الوسائط الرقمية إلى نوع مشترك:
julia> promote(1, 2.5)
(1.0, 2.5)
julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)
julia> promote(2, 3//4)
(2//1, 3//4)
julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)
julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)
julia> promote(1 + 2im, 3//4)
(1//1 + 2//1*im, 3//4 + 0//1*im)
تتم ترقية القيم العشرية إلى أكبر أنواع المعاملات العشرية. يتم ترقية القيم الصحيحة إلى أكبر أنواع المعاملات الصحيحة. إذا كانت الأنواع بنفس الحجم ولكن تختلف في التوقيع، يتم اختيار النوع غير الموقع. يتم ترقية مزيج من القيم الصحيحة والقيم العشرية إلى نوع عشري كبير بما يكفي لاستيعاب جميع القيم. يتم ترقية الأعداد الصحيحة المختلطة مع الكسور إلى كسور. يتم ترقية الكسور المختلطة مع القيم العشرية إلى قيم عشرية. يتم ترقية القيم المركبة المختلطة مع القيم الحقيقية إلى النوع المناسب من القيم المركبة.
هذا هو كل ما يتعلق باستخدام العروض الترويجية. الباقي هو مجرد مسألة تطبيق ذكي، حيث أن التطبيق "الذكي" الأكثر شيوعًا هو تعريف طرق شاملة للعمليات العددية مثل عوامل التشغيل الحسابية +
، -
، *
و /
. إليك بعض تعريفات الطرق الشاملة المعطاة في promotion.jl
:
+(x::Number, y::Number) = +(promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)
تقول تعريفات هذه الطرق أنه في غياب قواعد أكثر تحديدًا لإضافة وطرح وضرب وقسمة أزواج من القيم الرقمية، يتم ترقية القيم إلى نوع مشترك ثم المحاولة مرة أخرى. هذا كل ما في الأمر: لا يحتاج المرء في أي مكان آخر للقلق بشأن الترقية إلى نوع رقمي مشترك للعمليات الحسابية - فهي تحدث تلقائيًا. هناك تعريفات لطرق الترقية الشاملة لعدد من الوظائف الرياضية والحسابية الأخرى في promotion.jl
، ولكن بخلاف ذلك، هناك القليل من الاستدعاءات لـ promote
المطلوبة في قاعدة جوليا. تحدث الاستخدامات الأكثر شيوعًا لـ promote
في طرق البناة الخارجية، المقدمة للراحة، للسماح باستدعاءات البناة مع أنواع مختلطة للتفويض إلى نوع داخلي مع حقول تمت ترقيتها إلى نوع مشترك مناسب. على سبيل المثال، تذكر أن rational.jl
يوفر طريقة الباني الخارجي التالية:
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
هذا يسمح بالاتصالات مثل ما يلي للعمل:
julia> x = Rational(Int8(15),Int32(-5))
-3//1
julia> typeof(x)
Rational{Int32}
بالنسبة لمعظم الأنواع المعرفة من قبل المستخدم، من الأفضل أن يتطلب الأمر من المبرمجين تقديم الأنواع المتوقعة لوظائف البناء بشكل صريح، ولكن في بعض الأحيان، خاصةً في المشكلات العددية، قد يكون من الملائم القيام بالترقية تلقائيًا.
Defining Promotion Rules
على الرغم من أنه يمكن، من حيث المبدأ، تعريف طرق لدالة promote
مباشرة، إلا أن ذلك سيتطلب العديد من التعريفات الزائدة لجميع التباديل الممكنة لأنواع المعاملات. بدلاً من ذلك، يتم تعريف سلوك promote
من حيث دالة مساعدة تسمى promote_rule
، التي يمكن للمرء توفير طرق لها. تأخذ دالة promote_rule
زوجًا من كائنات النوع وتعيد كائن نوع آخر، بحيث سيتم ترقية مثيلات أنواع المعاملات إلى النوع المعاد. وبالتالي، من خلال تعريف القاعدة:
import Base: promote_rule
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64
يعلن المرء أنه عندما يتم ترقية قيم النقطة العائمة 64 بت و32 بت معًا، يجب ترقيتها إلى نقطة عائمة 64 بت. لا تحتاج نوع الترقية إلى أن تكون واحدة من أنواع المعاملات. على سبيل المثال، تحدث قواعد الترقية التالية في قاعدة جوليا:
promote_rule(::Type{BigInt}, ::Type{Float64}) = BigFloat
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt
في الحالة الأخيرة، نوع النتيجة هو BigInt
حيث أن BigInt
هو النوع الوحيد الكبير بما يكفي لاستيعاب الأعداد الصحيحة لعمليات الحساب الصحيحة ذات الدقة التعسفية. كما يجب ملاحظة أنه لا حاجة لتعريف كل من promote_rule(::Type{A}, ::Type{B})
و promote_rule(::Type{B}, ::Type{A})
- فالتناظر مفترض من الطريقة التي يتم بها استخدام promote_rule
في عملية الترويج.
تُستخدم دالة promote_rule
ككتلة بناء لتعريف دالة ثانية تُسمى promote_type
، والتي، بالنظر إلى أي عدد من كائنات النوع، تُرجع النوع المشترك الذي يجب أن تُرفع إليه تلك القيم، كوسائط لدالة promote
. وبالتالي، إذا أراد المرء أن يعرف، في غياب القيم الفعلية، ما هو النوع الذي ستُرفع إليه مجموعة من القيم من أنواع معينة، يمكن استخدام promote_type
:
julia> promote_type(Int8, Int64)
Int64
لاحظ أننا لا نقوم بتحميل promote_type
مباشرة: بل نقوم بتحميل promote_rule
بدلاً من ذلك. يستخدم promote_type
promote_rule
، ويضيف التماثل. يمكن أن يؤدي تحميله مباشرة إلى أخطاء غموض. نقوم بتحميل promote_rule
لتعريف كيفية ترقية الأشياء، ونستخدم promote_type
للاستعلام عن ذلك.
داخليًا، يتم استخدام promote_type
داخل promote
لتحديد نوع قيم الوسائط التي يجب تحويلها للترويج. يمكن للقارئ الفضولي قراءة الكود في promotion.jl
، الذي يحدد آلية الترويج الكاملة في حوالي 35 سطرًا.
Case Study: Rational Promotions
أخيرًا، نختتم دراستنا المستمرة لحالة نوع الأعداد الكسرية في جوليا، الذي يستخدم آلية الترقية بشكل نسبي مع القواعد التالية للترقية:
import Base: promote_rule
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)
القاعدة الأولى تقول إن ترقية عدد كسري مع أي نوع صحيح آخر ترفع إلى نوع كسري يكون بسطه/مقامه هو نتيجة ترقية نوع بسطه/مقامه مع نوع الصحيح الآخر. القاعدة الثانية تطبق نفس المنطق على نوعين مختلفين من الأعداد الكسرية، مما يؤدي إلى عدد كسري من ترقية أنواع بسطهما/مقاماتهما المعنية. القاعدة الثالثة والأخيرة تنص على أن ترقية عدد كسري مع عدد عشري تؤدي إلى نفس النوع مثل ترقية نوع البسط/المقام مع العدد العشري.
هذه المجموعة الصغيرة من قواعد الترويج، جنبًا إلى جنب مع منشئي النوع وطريقة convert
الافتراضية للأرقام، كافية لجعل الأعداد الكسرية تتفاعل بشكل طبيعي تمامًا مع جميع الأنواع الرقمية الأخرى في جوليا - الأعداد الصحيحة، والأعداد العائمة، والأعداد المركبة. من خلال توفير طرق تحويل مناسبة وقواعد ترويج بنفس الطريقة، يمكن لأي نوع رقمي معرف من قبل المستخدم أن يتفاعل بشكل طبيعي تمامًا مع الأرقام المعرفة مسبقًا في جوليا.