Здравейте, интересно ми е защо насърчавате duck typing-a? В други езици се твърди че изискването на тип помага с проверки и е хубаво нещо, защо в руби е по-различно?
Duck typing
Много добър въпрос! Ще се опитам да отговоря изчерпателно, та вероятно ще се получи дълго :)
Първо, малко термини
Обикновено се прави разграничение по следните критерии:
- Кога се проверяват типовете
- Със статична проверка, без да се изпълнява кодът. Това се прави от type checker (например, по време на компилация). Обикновено трябва да пишем типове ръчно. - това е статично типизиране - C/C++, Java
- Динамично, по време на изпълнение на програмата - динамично типизиране - Ruby, Python, JavaScript
- Правят ли се автоматични преобразувания между един или друг тип
- Не се правят автоматични преобразувания (или са лимитирани до много прости случаи) - силно типизиране - Ruby, Java
- Правят се автоматични преобразувания - слабо типизиране - JavaScript
- Как точно се проверява дали обектът е съвместим (от правилен тип)
- Проверява се само дали поддържа операциите, които се използват - duck typing (патешко типизиране?) - Ruby, Python, JavaScript
- Проверява се дали очакваният тип и полученият поддържат същите операции, дори някои от тях да не се използват във функцията - структурно типизиране - Go
- Проверява се дали полученият обект е от конкретният тип (по име на клас) или негов наследник - nominal или name-based typing - C++, Java, C#, ...
Обикновено статичното и name-based типизирането вървят ръка за ръка. Същото важи и за динамичното и патешкото. Причината е ясна - ако трябва да пишем типове на всичко - лесно проверяваме дали те съвпадат само по имена. Ако не пишем типове на променливи/функции и проверките стават по време на изпълнение на програмата - тогава е по-лесно при самото извикване на метод просто да проверим дали съществува такъв.
Сравнение
Да вземем тези два най-често срещани случая и да им направим сравнение:
-
static + name-based typing
- Плюсове:
- Преди дори да пуснем програмата знаем дали подаваме обекти от правилен тип за съответните операции. Това е добра първа стъпка, но не ни гарантира, че програмата ще върши правилното нещо. Често има и дупки в самата типова система - например, можем да подадем
nullкъдето се очаква обект от конкретен клас и програмата да гръмне. - Много по-лесно се правят инструменти, които разбират кода. Например, всичките IDE-та за Java и C#, които могат дори да рефакторират.
- Преди дори да пуснем програмата знаем дали подаваме обекти от правилен тип за съответните операции. Това е добра първа стъпка, но не ни гарантира, че програмата ще върши правилното нещо. Често има и дупки в самата типова система - например, можем да подадем
- Минус: Трябва да пишем типови описания за всяка променлива/операция. Трябва да групираме класовете в интерфейси. Често се налага да пишем немалко количество код само, за да задоволим претенциозността на type checker-а.
- Плюсове:
-
dynamic + duck typing
- Плюсове:
- Не пишем повече, отколкото е необходимо. Знаем, че функция приема аргумент, който може да е
Person,AnimalилиMachine. Тези са коренно различни, но имат общ методname. Защо, тогава, да трябва да правим общ родителски клас или интерфейс? Просто подаваме аргумента и то работи. Освен това не ни се налага да пишемMap<String, Map<String, List<NameableInterface>>>всеки път, когато ни трябва lookup таблица по първо име и фамилия (например). - Позволява ни да поддържаме обекти от типове, които не са под наш контрол или не са съществували докато сме писали функцията.
- Не пишем повече, отколкото е необходимо. Знаем, че функция приема аргумент, който може да е
- Минуси:
- Понякога правим грешки - подаваме невалидни неща - и разбираме за това чак като изпълним кода. Това може да се прояви и след време, и е опасно.
- Статичните tool-ове не разбират кода толкова добре. Не е сигурно какъв е типа на обект в променлива, докато не се изпълни самият код.
- Плюсове:
Филосовията на Ruby
Философията на Ruby е, че не трябва да пишем излишни неща. Например:
def get_names(things) things.map(&:name) endСъс силно и name-based типизиране, това би изглеждало така (псевдо код):
def get_names(things : Array<Person>) Array<String> things.map(&:name) endДотук добре, но вторият вариант може да се използва само за хора. Да го направим по-общ:
interface Nameable name() : String end class Person implements Nameable ... end def get_names(things : Array<Nameable>) Array<String> things.map(&:name) endИзползвахме интерфейс, за да опишем нещо, което има метод
name. Сега трябва да променим всеки клас, който искаме да използваме тук, така че да имплементира този интерфейс. Ако не го прави - няма как да го използваме.НО! Методът още не е достатъчно общ - може да се използва само за масиви от неща с име. Какво правим, ако искаме да подадем свързан списък или дърво? Да, правим нов интерфейс.
interface Collection map(...) : Collection end def get_names(things : Collection<Nameable>) Array<String> things.map(&:name) endОтново, всички неща, които използваме тук трябва да имплементират
Collection. А какво става, ако искаме да позволимnameда връща и други обекти - не самоString?Разбира се, ако очакваме този метод да работи само за хора и масиви - type checker-а ще ни съобщи за проблем, ако сме объркали нещо.
В Ruby идеята е нещата да могат да взаимодействат безпроблемно и да работят почти "магически". Това, че се разчита на duck typing, е част от играта.
Статична проверка vs тестове
Друго валидно твърдение - тестовете проверяват типове + поведение. В идеалния случай, без значение от езика, трябва да пишем тестове.
Следователно, типовата информация е излишна, защото я проверяваме с тестовете. Едновременно с това, неща като мокване и стъбване са една идея по-трудни, ако трябва да се борим и със статична типова система.В Ruby автоматизираното тестване е страшно популярно.
Разлика в идеологиите
Няма верен отговор и най-добър. Това е въпрос на избор и предпочитания. Matz е избрал Ruby да бъде такъв - това се харесва на някои хора и не се харесва на други.
Има и някои use-case-ове, в които е по-подходящо едното пред другото, но тези примери са много по-малко отколкото ни се иска. Например, не би писал софтуер за космически кораб на JavaScript.
В повечето случаи - това какъв език ще се използва зависи единствено от хората в екипа, който ще го пише.Моят съвет - не го гледай като избор, който трябва да направиш. Разбери идеите и зад двата подхода и използвай който намериш за по-подходящ за проекта ти, и за езика, на който го пишеш.
- Кога се проверяват типовете
@Георги

Трябва да сте влезли в системата, за да може да отговаряте на теми.
