Здравейте, интересно ми е защо насърчавате 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.
В повечето случаи - това какъв език ще се използва зависи единствено от хората в екипа, който ще го пише.Моят съвет - не го гледай като избор, който трябва да направиш. Разбери идеите и зад двата подхода и използвай който намериш за по-подходящ за проекта ти, и за езика, на който го пишеш.
- Кога се проверяват типовете
@Георги
Трябва да сте влезли в системата, за да може да отговаряте на теми.