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

این پست در مورد چیزی است که من را برای مدتی در 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، به نظر یک مشکل فنی است که ممکن است در آینده بازنگری شود.