Duck typing

  1. Здравейте, интересно ми е защо насърчавате duck typing-a? В други езици се твърди че изискването на тип помага с проверки и е хубаво нещо, защо в руби е по-различно?

  2. Много добър въпрос! Ще се опитам да отговоря изчерпателно, та вероятно ще се получи дълго :)

    Първо, малко термини

    Обикновено се прави разграничение по следните критерии:

    1. Кога се проверяват типовете
      • Със статична проверка, без да се изпълнява кодът. Това се прави от type checker (например, по време на компилация). Обикновено трябва да пишем типове ръчно. - това е статично типизиране - C/C++, Java
      • Динамично, по време на изпълнение на програмата - динамично типизиране - Ruby, Python, JavaScript
    2. Правят ли се автоматични преобразувания между един или друг тип
      • Не се правят автоматични преобразувания (или са лимитирани до много прости случаи) - силно типизиране - Ruby, Java
      • Правят се автоматични преобразувания - слабо типизиране - JavaScript
    3. Как точно се проверява дали обектът е съвместим (от правилен тип)
      • Проверява се само дали поддържа операциите, които се използват - duck typing (патешко типизиране?) - Ruby, Python, JavaScript
      • Проверява се дали очакваният тип и полученият поддържат същите операции, дори някои от тях да не се използват във функцията - структурно типизиране - Go
      • Проверява се дали полученият обект е от конкретният тип (по име на клас) или негов наследник - nominal или name-based typing - C++, Java, C#, ...

    Примери има тук и тук.

    Обикновено статичното и name-based типизирането вървят ръка за ръка. Същото важи и за динамичното и патешкото. Причината е ясна - ако трябва да пишем типове на всичко - лесно проверяваме дали те съвпадат само по имена. Ако не пишем типове на променливи/функции и проверките стават по време на изпълнение на програмата - тогава е по-лесно при самото извикване на метод просто да проверим дали съществува такъв.

    Сравнение

    Да вземем тези два най-често срещани случая и да им направим сравнение:

    1. static + name-based typing
      • Плюсове:
        • Преди дори да пуснем програмата знаем дали подаваме обекти от правилен тип за съответните операции. Това е добра първа стъпка, но не ни гарантира, че програмата ще върши правилното нещо. Често има и дупки в самата типова система - например, можем да подадем null където се очаква обект от конкретен клас и програмата да гръмне.
        • Много по-лесно се правят инструменти, които разбират кода. Например, всичките IDE-та за Java и C#, които могат дори да рефакторират.
      • Минус: Трябва да пишем типови описания за всяка променлива/операция. Трябва да групираме класовете в интерфейси. Често се налага да пишем немалко количество код само, за да задоволим претенциозността на type checker-а.
    2. 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.
    В повечето случаи - това какъв език ще се използва зависи единствено от хората в екипа, който ще го пише.

    Моят съвет - не го гледай като избор, който трябва да направиш. Разбери идеите и зад двата подхода и използвай който намериш за по-подходящ за проекта ти, и за езика, на който го пишеш.

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