برنامه نویسی

ژنریک در زنگ: آب تیره اجرای صفات خارجی بر انواع خارجی

این پست در مورد چیزی است که من را برای مدتی در Rust عمومی آزار می داد قبل از اینکه بتوانم آنچه را که در حال وقوع است (نوعی) توضیح دهم، یعنی پیاده سازی ویژگی خارجی در انواع خارجیخصوصاً در زمینه روش Rust برای “اپراتور اضافه بار”.

ما نمی توانیم آن را انجام دهیم، یا می توانیم؟

اول اینکه هیچ رازی وجود ندارد، درست است؟ Rust Book در این مورد کاملاً واضح است.

اما ما نمی توانیم صفات خارجی را روی انواع خارجی پیاده سازی کنیم. به عنوان مثال، ما نمی توانیم صفت Display را بر روی آن پیاده سازی کنیم Vec در جعبه جمع آوری ما، زیرا نمایش و Vec هر دو در کتابخانه استاندارد تعریف شده اند و محلی برای ما نیستند aggregator جعبه این محدودیت بخشی از خاصیتی است به نام coherence و به طور خاص قاعده یتیم که به این دلیل نامگذاری شده است که نوع والد وجود ندارد.

بنابراین اگر بخواهیم چیزی شبیه به این را در زمین بازی بنویسیم:

impl From for f64 {
    // -- snippet --
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

کامپایلر بلافاصله این را به ما یادآوری می کند حکومت یتیم

error[E0117]: only traits defined in the current crate can be implemented for primitive types
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خوب و واضح! حال، اگر این خطوط را با ژنریک جایگزین کنیم، خطای کامپایلر متفاوت است (و در یک تناقض منطقی “خیلی” با پیام خطای اول)، که نشان می دهد چیزی به این سادگی نیست که تبلیغ می شود.

impl From for U {
    // -- snippet --
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

error[E0210]: type parameter `U` must be used as the type parameter for some local type (e.g., `MyStruct`)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

هنگامی که ما به توضیح مفصل نگاه می کنیم error[E0210]، متوجه شدیم که شهود ما درست بوده است:

هنگام اجرای یک صفت خارجی برای یک نوع خارجی، این صفت باید یک یا چند پارامتر نوع داشته باشد. قبل از هر گونه استفاده از پارامترهای نوع باید یک نوع محلی در جعبه شما ظاهر شود.

بنابراین ما می توانیم آن را در Rust انجام دهیم، نمی توانیم؟ اما درباره کتاب چطور؟

چگونه می توان nalgebra انجام دهید؟

نگاهی به جعبه های کتابخانه های معتبر از این قبیل nalgebra سوالاتی را نیز مطرح می کند. بیایید برای مثال امتحان کنیم:

use nalgebra::Vector3;

fn main() {
   let v = Vector3::new(1.0, 2.0, 3.0);
   println!("{:?}", v * 3.0);
   println!("{:?}", 3.0 * v);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آن چیزی که انتظار می رود را جمع آوری و تولید می کند و همه چیز روشن به نظر می رسد. اما چگونه ممکن است؟

اولین عبارت، البته، بسیار استاندارد است: v * 3.0 نیاز به اجرا دارد std::ops::Mul صفت با Output = Vector3 بر Vector3. با این حال، 3.0 * v نیاز دارد std::ops::Mul در نوع داخلی f64، که چیزی نیست جز اجرای یک صفت خارجی بر روی یک نوع خارجی نقض مستقیم کتاب

نگاه کردن به nalgebra کد منبع، متوجه می‌شویم که اولین عبارت با استفاده از ژنریک پیاده‌سازی شده است

macro_rules! componentwise_scalarop_impl(
    ($Trait: ident, $method: ident, $bound: ident;
     $TraitAssign: ident, $method_assign: ident) => {
        impl $Trait for Matrix
            where T: Scalar + $bound,
                  S: Storage,
                  DefaultAllocator: Allocator {
                //
                // -- snippet --
                //
            }
        }  
    }
);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اعلان کلان در این مورد چندان مهم نیست. مهمتر از آن این است که ضرب راست توسط یک اسکالر عمومی است و همه متا متغیرها در الگوی کلان به سادگی به شناسایی متصل می شوند.

ضرب چپ در یک اسکالر کاملاً متفاوت است. این کلی نیست، تطبیق الگوی کلان به انواع با الگوهای تکرار متصل می شود

macro_rules! left_scalar_mul_impl(
    ($($T: ty),* $(,)*) => {$(
        impl> Mul> for $T
            // -- snippet --
    )*}
);

left_scalar_mul_impl!(u8, u16, u32, u64, usize, i8, i16, i32, i64, isize, f32, f64);
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خط آخر به صراحت پیاده سازی ها را برای انواع داخلی نمونه سازی می کند.

پس چرا فرق می کند؟

ما می توانیم کاری را که نمی توانیم انجام دهیم

بالاخره جواب را در کتاب RFC یافتم (RFC مخفف Request For Comments) است.
RFC 2451 از 30-05-2018 که با خطوط زیر شروع می شود:

خوب یا بد، ما اجازه می دهیم که ویژگی های خارجی را برای انواع خارجی پیاده سازی کنیم.

خودشه! این پاسخ است.

سپس جالب تر می شود:

این تغییر چیزی نیست که در نهایت به یک راهنما ختم شود و بیشتر از طریق پیام های خطا اطلاع رسانی می شود. رایج ترین مورد مشاهده شده E0210 است. متن آن خطا به صورت تقریبی به صورت زیر تغییر خواهد کرد:

سپس جزئیات E0210 را که قبلاً در بالا ذکر کردم دنبال می شود. همراه با RFC 2451 کمی روشن می‌کند که چه زمانی می‌توانیم ویژگی‌های خارجی را برای انواع خارجی پیاده‌سازی کنیم و چه زمانی توپ می‌زنیم. یک جزئیات بیشتر از این اسناد:

هنگام اجرای یک صفت خارجی برای یک نوع خارجی، این صفت باید یک یا چند پارامتر نوع داشته باشد. قبل از هر گونه استفاده از پارامترهای نوع باید یک نوع محلی در جعبه شما ظاهر شود. این بدان معناست که impl ForeignTrait، T> برای ForeignType معتبر است، اما impl ForeignTrait> برای ForeignType معتبر نیست.

این در مثال زیر برای ضرب اسکالر چپ از کتابخانه کوچک من از منحنی های عمومی Bezier که برای تصویر در پست های قبلی استفاده کردم کار می کند.

impl Mul> for f64 where
            T: Copy + Mul,
            [(); N]:
{
    // -- snippet --
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در این مثال یک ویژگی خارجی std::ops::Mul متخصص در یک نوع عمومی محلی Bernstein برای نوع خارجی اجرا می شود f64 مشابه مثال بالا با left_scalar_mul_impl از جانب nalgebra جعبه نوع کاملاً عمومی این پیاده سازی

impl Mul> for U where
            T: Copy + Mul,
            U: Copy,
            [(); N]:
{
    type Output = Bernstein;

    fn mul(self, rhs: Bernstein) -> Self::Output {
        // -- snippet --
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خطای کامپایلر آشنا E0210 را می دهد.

خلاصه

با رعایت نکاتی می توانیم صفات خارجی را روی انواع خارجی در Rust پیاده کنیم. با این حال، این رفتار هنوز در The Rust Book وجود ندارد و بیشتر از طریق E0210 و RFC ها منتقل می شود. ژنریک های خالص کار نمی کنند، که طبق RFC 2451، به نظر یک مشکل فنی است که ممکن است در آینده بازنگری شود.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا