تایپ‌دهی اردکی

تایپ دهی اردکی (Duck Typing)
در برنامه‌نویسی شی گرا، تایپ دهی اردکی (duck typing) یک شیوه تایپ دهی پویاست که در آن خصوصیات و توابع فعلی شی معنای آن را مشخص می‌کند و نه ارث بری آن از یک کلاس خاص و interface. نام این نوع تایپ دهی از آزمون اردکی (duck test) که به James Whitcomb Riley منسوب است گرفته شده‌است که آن را می‌توان به این صورت بیان کرد:
«اگر پرنده‌ای ببینم که مانند اردک راه می‌رود، مانند اردک شنا می‌کند و مانند اردک صدا درمی‌آورد، من به آن پرنده اردک می‌گویم»
در تایپ دهی اردکی تمرکز ما فقط بر روی آن دسته از خصوصیات شی است که استفاده می‌شوند و نه نوع شی. برای مثال در یک زبان بدون تایپ دهی اردکی، می‌توان تابعی نوشت که یک شی از نوع «اردک» می‌گیرد و متدهای «راه رفتن» و «صدا درآوردن» آن شی را فراخوانی می‌کند. اما در زبانی با تایپ دهی اردکی، همان تابع به این صورت است که یک شی از «هر نوع» می‌گیرد و متدهای «راه رفتن» و «صدا درآوردن» آن شی را فراخوانی می‌کند. اگر آن شی متدهای فراخوانی شده را نداشته باشد تابع در زمان اجرا خطا خواهد گرفت. پذیرش «هر نوع» شیی توسط تابع، که متدهای «راه رفتن» و «صدا درآوردن» درست را داشته باشد در واقع نشان دهنده نام این تایپ دهی و جمله بالاست که «اگر پرنده‌ای ببینم که مانند اردک راه می‌رود، مانند اردک شنا می‌کند و مانند اردک صدا درمی‌آورد، من به آن پرنده اردک می‌گویم». در اینجا هم شیی که توابع درست را دارد پس شی درست است.
تست نکردن نوع آرگومان‌ها در توابع از روی عادت، مستندسازی، کدهای خوانا و واضح و تست کردن برای اطمینان از استفاده صحیح، به برنامه‌نویسی با تایپ دهی اردکی کمک می‌کند. برنامه نویسانی که به تایپ دهی ایستا عادت کرده‌اند و می‌خواهند از برنامه‌های با تایپ دهی پویا استفاده کنند، معمولا تمایل دارند که قبل از اجرا چک کردن تایپ را به صورت استاتیک انجام دهند که با این کار از مزایا و انعطاف‌پذیری تایپ دهی اردکی بهره‌مند نمی‌شوند و پویایی زبان را با محدودیت مواجه می‌سازند.
مثال
شبه کد زیر را برای یک زبان با تایپ دهی اردکی در نظر بگیرید:
function calculate(a, b, c) => return (a+b)*c
example1 = calculate (1, 2, 3)
example2 = calculate (1, 2, 3, 4, 5, 6, 2)
example3 = calculate (apples , and oranges, , 3)
print to_string example1
print to_string example2
print to_string example3
در شبه کد بالا هر وقت تابع calculate صدا زده می‌شود، اشیایی با قالب‌های مختلف و بدون در نظر گرفتن وراثت به آن فرستاده می‌شود (عدد، بردار، رشته). از آنجایی که این اشیا «+» و «*» را پشتیبانی می‌کنند، اجرا در همه این حالات ممکن است و خروجی در صورتی که این کد در زبان‌های پایتون یا روبی نوشته شود، به صورت زیر خواهد بود:
9
1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6
apples and oranges, apples and oranges, apples and oranges,
در واقع اجرای تابع calculate با هر شیءای ممکن است، مشروط بر اینکه عملگرهای + و * برای آن شیء تعریف شده باشند. این همان محک اردکی است: تعریف شده بودن + (مانند اردک راه می‌رود) و تعریف شده بودن * (مانند اردک شنا می‌کند) کافی است تا به آن شیء محاسباتی (اردک) بگوییم. تنها چیزی که تابع calculate نیاز دارد این است که متغیرهای آن، متدهای «+» و «*» را داشته باشند.
بنابراین تایپ دهی اردکی اجازه داشتن پلی مورفیسم بدون وراثت را می‌دهد. در اینجا این نوع تایپ دهی با گرفتن یک رشته به عنوان ورودی تشخیص می‌دهد که باید عمل concatenation را انجام دهد و در صورتی که ورودی int باشد، جمع عادی را انجام دهد. در حالی که در تایپ دهی‌های استاتیک، برای این کار مجبور بودیم این تابع را برای نوع رشته یا int به‌طور خاص فراخوانی کنیم. (در واقع کلاس‌هایی داشتیم که هر کدام از یک کلاس پدر ارث می‌بردند و هر کدام از این کلاس‌ها یک شکل تابع calculate را پیاده‌سازی می‌کردند)
تایپ دهی اردکی در کد زیر که به زبان Python نوشته شده استفاده شده‌است. در تابع in_the_forest نوعی که به آن رد می‌شود هم می‌تواند Duck و هم Person باشد و چون هر دو این اشیا توابع quack() و feathers() دارند، هر کدام از آن‌ها یک duck است!
class Duck:
def quack(self):
print "Quaaaaaack!»
def feathers(self):
print "The duck has white and gray feathers."
class Person:
def quack(self):
print "The person imitates a duck."
def feathers(self):
print "The person takes a feather from the ground and shows it."
def in_the_forest(duck):
duck.quack()
duck.feathers()
def game():
donald = Duck()
john = Person()
in_the_forest(donald)
in_the_forest(john)
game()
تایپ دهی اردکی در زبان‌های با تایپ دهی استاتیک
زبانهایی مثل Boo و نسخه 4 C# که معمولا از تایپ دهی استاتیک استفاده می‌کنند، توضیحاتی دارند که به کامپایلر می‌گوید آماده type checking کلاس‌های در زمان اجرا باشد و نه کامپایل، و همچنین کد مربوط به type checking در زمان اجرا را، در خروجی کامپایل بگنجاند. این خصوصیت اجازه می‌دهد که این زبان‌ها هم بیشتر مزایای تایپ دهی اردکی را داشته باشند. تنها کاری که باید انجام شود این است که کلاس‌های پویا در زمان کامپایل مشخص شوند.
مقایسه با سایر سیستم‌های تایپ دهی
سیستم‌های تایپ دهی ساخت یافته
تایپ دهی اردکی مشابه ولی متمایز از تایپ دهی ساخت یافته‌است. تایپ دهی ساخت یافته یک سیستم تایپ دهی استاتیک است که مطابقت نوع و تساوی را توسط ساختار نوع تشخیص می‌دهد در حالی که تایپ دهی اردکی پویاست و مطابقت نوع را فقط با قسمتی از ساختار نوع که در موقع اجرا از آن استفاده می‌شود تعیین می‌کند.
زبان برنامه‌نویسی Objective Caml از تایپ دهی ساخت یافته‌استفاده می‌کند.
واسط‌ها (Interfaces)
واسط‌ها می‌توانند بعضی از مزایای تایپ دهی اردکی را ایجاد کنند هرچند تایپ دهی اردکی از این جهت که هیچ گونه واسطی را به‌طور صریح تعریف نمی‌کند با آن فرق دارد. برای مثال اگر یک کتابخانه جاوا یک کلاس را که اجازه تغییر آن را ندارد implement کند، نمی‌توان یک نمونه از آن کلاس را در واسطی که خودمان تعریف کرده‌ایم استفاده کنیم، در حالی که تایپ دهی اردکی این اجازه را می‌دهد.
Templateها یا تایپ‌های Generic
توابع template در واقع «تست اردکی» را در یک محیط با تایپ دهی استاتیک انجام می‌دهند. این کار تمام مزایا و مشکلات تایپ دهی استاتیک را در مقابل تایپ دهی پویا خواهد داشت. تایپ دهی اردکی همچنین از این نظر انعطاف پذیرتر است که تنها متدهایی که در زمان اجرا فراخوانی می‌شوند نیاز به پیاده‌سازی دارند، در حالی که templateها چون در زمان کامپایل این تایپ دهی را انجام می‌دهند، به پیاده‌سازی همه متدهایی که ممکن است در زمان اجرا فراخوانی شوند نیاز دارد.
Templateها در C++ و Genericها در زبان جاوا وجود دارند.
انتقاد
یکی از نقدهای معمول به این نوع تایپ دهی: یکی از مشکلات تایپ دهی اردکی این است که برنامه‌نویس مجبور است فهم وسیع‌تری از کدی که در هر لحظه می‌نویسد داشته باشد. در یک زبان با تایپ دهی استاتیک که از سلسله مراتب‌های نوع و چک کردن نوع پارامترها استفاده می‌کند، نسبت دادن شیی با نوع غلط به یک کلاس بسیار سخت‌تر خواهد بود. برای مثال در زبان پایتون، می‌توان به راحتی یک کلاس «نوشیدنی» ایجاد کرد که یک کلاس دیگر به عنوان یکی از ترکیبات این نوشیدنی، متد press() آن را implement می‌کند. هرچند می‌توان یک کلاس به نام «پیراهن» هم داشت که متد press() را implement کند! با تایپ دهی اردکی برای جلوگیری از خطاهای ناخواسته، برنامه‌نویس باید از هر نوع استفاده متد press() مطلع باشد.
ماهیتا مشکل این است که «اگر چیزی مانند اردک راه می‌رود، مانند اردک شنا می‌کند و مانند اردک صدا درمی‌آورد» می‌تواند یک اژدها باشد که ادای اردک‌ها را درمی‌آورد! شاید همیشه نخواهید یک اژدها را به یک تالاب راه دهید حتی اگر بتواند مثل یک اردک رفتار کند!
طرفداران تایپ دهی اردکی معتقدند که این مشکل با تست کردن و داشتن اطلاعات لازم در مورد کد و رفع خطاهای آن، برطرف خواهد شد.
تاریخچه
Alex Martelli در سال ۲۰۰۰ اصطلاح تایپ دهی اردکی را در یک پیغام که که به گروه خبری comp.lang.python فرستاده بود، استفاده کرد. او همچنین فهم نادرست از تست اردکی را بیان کرد (به نظر می‌رسد لغت تست اردکی در آن زمان استفاده می‌شده‌است).
«… به بیان دیگر نگاه نکنید که آیا این یک اردک است، ببینید که آیا صدای اردک درمی‌آورد، مثل اردک راه می‌رود و…، بسته به اینکه دقیقا شما چه زیر مجموعه‌ای از رفتار شبیه اردک را نیاز دارید.»
پیاده‌سازی
در C
در C# 4.0 کامپایلر و زمان اجرا با هم همکاری می‌کنند تا تایپ دهی پویا پیاده‌سازی شود.
در ColdFusion
ColdFusion زبان اسکریپت نویسی برنامه‌های وب است که اجازه می‌دهد آرگومان‌های تابع هر نوعی را بپذیرند. برای این نوع آرگومان، یک شی دلخواه می‌تواند به تابع فرستاده شود و فراخوانی توابع به صورت پویا در زمان اجرا bind می‌شوند. اگر یک شی تابع فراخوانی شده را پیاده‌سازی نکرده باشد، یک exception در زمان اجرا throw می‌شود که می‌توان آن را catch و به خوبی handle کرد. در ColdFusion 8 این کار را می‌توان به جای exception handling با متد از پیش تعریف شده onMissingMethod() انجام داد.
در Common Lisp
Common Lisp یک extension شی گرا را فراهم می‌کند. (Common Lisp Object System یا CLOS). ترکیب CLOS و تایپ دهی پویای Lisp، تایپ دهی اردکی را یک شیوه برنامه‌نویسی معمول در Common Lisp می‌سازد.
در Common Lisp نیز نیازی به چک کردن نوع‌ها نیست زیرا در زمان اجرا وقتی که تابعی قابل اجرا نباشد خطا می‌دهد. این خطا با Condition System of Common Lisp قابل مدیریت است.
در این زبان متدها خارج کلاس‌ها تعریف می‌شوند و همچنین می‌توانند برای اشیای خاصی هم استفاده شوند.
(defclass duck () ())
(defmethod quack ((a-duck duck))
(print "Quaaaaaack!»))
(defmethod feathers ((a-duck duck))
(print "The duck has white and gray feathers."))
(defclass person () ())
(defmethod quack ((a-person person))
(print "The person imitates a duck."))
(defmethod feathers ((a-person person))
(print "The person takes a feather from the ground and shows it."))
(defmethod in-the-forest (duck)
(quack duck)
(feathers duck))
(defmethod game ()
(let ((donald (make-instance duck))
(john (make-instance person)))
(in-the-forest donald)
(in-the-forest john)))
(game)
Common Lisp همچنین اجازه اصلاح خطا به صورت interactive را می‌دهد:
? (defclass cat () ())
? (quack (make-instance cat))
> Error: There is no applicable method for the generic function:
> when called with arguments:
> If continued: Try calling it again
1> (defmethod quack ((a-cat cat))
(print "The cat imitates a duck.»))
1> (continue)
"The cat imitates a duck."
در Objective C
Objective C چیزی بین C و Smalltalk است. این زبان اجازه می‌دهد که اشیایی از نوع id تعریف کرد و هر پیغامی به آن‌ها فرستاد (مثل Smalltalk). فرستنده پیام می‌تواند چک کند که شی به پیام پاسخ می‌دهد یا نه، و شی نیز می‌تواند در زمان رسیدن پیام تصمیم بگیرد که آیا به آن پاسخ دهد یا خیر، و اگر فرستنده پیامی بفرستد که گیرنده نمی‌تواند به آن پاسخ دهد، یک exception رخ می‌دهد؛ بنابراین تایپ دهی اردکی کاملا توسط زبان Objective C حمایت می‌شود.
در Python
تایپ دهی اردکی بسیار در پایتون استفاده می‌شود. واژه نامه پایتون تایپ دهی اردکی را به صورت زیر تعریف می‌کند:
شیوه برنامه‌نویسی به روش پایتون به این صورت است که نوع یک شی توسط متدها و خصوصیات(attribute) آن تعیین می‌شود و نه توسط رابطه صریح آن با نوع خاصی از اشیا. این زبان با تکیه بر واسط‌ها و نه نوع‌های خاص، انعطاف‌پذیری بالایی را در کدهایی که به خوبی طراحی شده‌اند ایجاد می‌کند. تایپ دهی اردکی مانع از تست‌هایی همچون type() و isinstance() می‌شود. در عوض از شیوه برنامه‌نویسی EAFP(Easier to Ask Forgiveness than Permission تقاضای بخشش آسان‌تر از اجازه است!) استفاده می‌کند.
مثال تایپ دهی اردکی در پایتون، کلاس‌های فایل گونه‌است. کلاس‌ها می‌توانند بعضی یا همه متدهای یک فایل را پیاده کنند و می‌توانند جاهایی که فایل‌ها استفاده می‌شوند مورد استفاده قرار گیرند. برای مثال GzipFile یک شی فایل گونه را برای دسترسی به داده‌های فایل gzip-compressed پیاده می‌کند. cStringIO اجازه می‌دهد که با یک رشته پایتون به صورت فایل رفتار کرد. همچنین سوکت‌ها و فایل‌ها متدهای مشترک زیادی دارند. هرچند سوکت‌ها متد tell() را ندارند و نمی‌توانند هرجا که GzipFile استفاده می‌شود، مورد استفاده قرار گیرند. این موضوع انعطاف‌پذیری تایپ دهی اردکی را نشان می‌دهد: یک شی فایل گونه تنها متدهایی را که توانایی آن را دارد implement می‌کند و بنابراین می‌تواند در جاهایی که معنای درستی می‌دهد مورد استفاده قرار گیرد.
قانون EAFP فایده exception handling را نشان می‌دهد. برای مثال به جای چک کردن اینکه یک شی متد quack() را دارد یا نه (استفاده از hasattr(duck,”quack”):…) بهتر است امتحان کنیم و ببینیم این متد را دارد یا نه و اگر نداشت exception آن را handle می‌کنیم.
try:
mallard.quack()
except (AttributeError, TypeError):
print "mallard cant quack()"
مزایای این روش این است که handle کردن ساخت یافته کلاس‌های دیگر را ممکن می‌کند (برای مثال یک زیر کلاس muteDuck می‌تواند یک QuackException ایجاد کند که می‌توان این خطا را نیز به قسمت except اضافه کرد بدون اینکه نیاز به بررسی منطق کد باشد.
پیاده‌سازی‌ها
چند زبان که از تایپ‌دهی اردکی استفاده می‌کنند:
روبی
کلد فیوژن
آبجکتیو-سی
پایتون (زبان)
لوآ