#php #ооп #любой_язык #теория_типов
В качестве языка примера буду использовать PHP , для простоты. Поясню сразу суть вопроса. В современных языках, особенно это касается интерпритируемых, есть типизация традиционно не строгая: function join($a){ return is_array($a) ? implode('', $a) : $a; } А есть типизация строгая: function join(array $a) : string{ return implode('', $a); } Есть ещё, например в C++ шаблонная типизация: templateR* join(T * array, int count) { R *output = new R(count); for (int i = 0; i < count; i++) output += array[i] ; return output; } Есть ещё в C++ перегрузка по типам, но она весьма не гибка и громоздка, как и шаблонная типизация, последняя вдобавок ещё и не читаема. Но очень не хватает типизации множественной (как я узнал - это называется union types) - хотелось бы следующее: function join(array|Iterator|ICollectable $input) : string|null{ /*какой-то код обработки*/ } Вопрос - есть ли языки, поддерживающие подобный синтаксис ограничения по типам, если да, то какие? Если нет, то каковы причины, чем концепция плоха? Почему возникает вопрос отсутствия встроенной поддержки сего - программисты языков с variance-типами, обычно пишут в коментах метода что-то вроде: /** * Some function describe * * @param array|ISomeList $list ... * @return false|string ... */ Но в объявлении ф-ии они вынуждены не делать явной отсылки к типу(полагаясь на комментарии), оставляя variance типы: public function someFunction($list) {... return $some;}; Это встречается регулярно в более чем серьёзных фреймворках, которые не могут принебречь читаемостью кода ради строгой типизации. В итоге при исследовании кода без явного указания желаемых типов: понижается читаемость. Комменты не дают 100% гарантии ограничения типов: программист, как и IDE не может точно знать - что может придти во входном параметре. А при работе программы - если вдруг во входной параметр попало не то что нужно: не происходит ошибок/предупреждений , что усложняет ловлю ошибок, понижает стабильность. Наоборот же при использовании только строгих типов - безальтернативно усложняется вся система в целом: количество классов растёт геометрично. Что ещё хуже, ведь НЕ-строгость типов работает на простые архитектурные решения и читаемый код, и в случае скриптовых языков ей нельзя принебрегать. UPD: Да, конечно такая фича не совсем ложится на принципы ООП. Но это было бы поводом эти принципы пересматривать - т.к. они очень обходчиво не учитывают "геометрический рост количества классов" . А также union-типизация работает совместно с утиной типизацией - что я бы минусом не назвал, но это большое поле для споров. UPD2: Если возникает вопрос обоснованности мульти-типов: Более живой пример из PHP - представлено объявление важной, часто используемой функции выборщика объектов из БД-таблиц (db-table-маппера) Zend фреймворка (1.9) класс Zend_Db_Table_Abstract . /** * Fetches all rows. * * Honors the Zend_Db_Adapter fetch mode. * * @param string|array|Zend_Db_Table_Select $where OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object. * @param string|array $order OPTIONAL An SQL ORDER clause. * @param int $count OPTIONAL An SQL LIMIT count. * @param int $offset OPTIONAL An SQL LIMIT offset. * @return Zend_Db_Table_Rowset_Abstract The row results per the Zend_Db_Adapter fetch mode. */ public function fetchAll($where = null, $order = null, $count = null, $offset = null) { if (!($where instanceof Zend_Db_Table_Select)) { $select = $this->select(); if ($where !== null) { $this->_where($select, $where); } if ($order !== null) { $this->_order($select, $order); } if ($count !== null || $offset !== null) { $select->limit($count, $offset); } } else { $select = $where; } $rows = $this->_fetch($select); $data = array( 'table' => $this, 'data' => $rows, 'readOnly' => $select->isReadOnly(), 'rowClass' => $this->getRowClass(), 'stored' => true ); $rowsetClass = $this->getRowsetClass(); if (!class_exists($rowsetClass)) { Zend_Loader::loadClass($rowsetClass); } return new $rowsetClass($data); } Первый аргумент по замыслу разработчика может иметь один из нескольких типов. Это нужно, чтобы потенциальному пользователю метода(программисту) было очень просто уместить в голове маппер. Ведь он может вызвать из кода этот метод по-разному, но метод при этом остаётся тот-же: $table->fetchAll('user_id in (1,2,5)'); $table->fetchAll(['user_id < 100', 'gender = "male"']); $table->fetchAll( $table->select()->where('user_id IN (?)', $userIdsArray) ); Один метод легко запомнить, это хорошо ложится в голову. В то время, как программисту метода fetchAll не хочется размазывать логику одной задачи (выбора объектов из базы данных) на несколько перегруженных методов внутри себя - он хочет чтобы всё было в одном месте, чтобы открывший код понял сразу что и в каком порядке работает. Как минимум это соответствует SRP - обязанность у метода одна: выбрать из БД объекты. То есть предоставляется целый язык для общения с БД-таблицей. Это больше сервис для программиста, нежели просто метод. И такие методы во фреймворках: каждый второй, потому что фреймворк должен быть прежде всего удобным программисту. Программист не должен вникать в "тысячи классов" - чтобы пользоватся фичей - вот это яркий пример решения вопроса читаемости кода, в котором пригодились бы union types. UPD3: Оказывается разработчики PHP, как подсказали в коментах, тоже думали ввести эту концепцию в 2015 году, но на голосовании - отклонили.
Ответы
Ответ 1
Интересный вопрос. Не знаю как в других языках обстоят дела, но в scala то что вы хотите сделать, решается так: Использованием pattern matching def f1(obj: Any) = obj match { case x: Array[String] => println("is array") case x: String => println("is string") case _ => println("") } Так же, есть возможность определить какие методы должны быть у типа аргумента. Например. case class A() { def m1() = "m1 is invoked" def m2() = println("m2 is invoked") } def f2(obj: {def m1(): String}) = { println(obj.m1()) } val obj = A() f2(obj)Ответ 2
Любая конструкция должна иметь обоснование: зачем это нужно, какие плюсы оно дает, какие минусы оно дает. Из теории проектирования мы знаем, что программная единица должна выполнять строго отведенную под нее задачу-алгоритм. Наличие подобной семантики подразумевает, что код будет как называется "и жнец, и чтец, и на дуде игрец". Это полностью рушит логику и порождает плохой стиль программирования. К тому же появляется очень много вопросов по поводу его использования. К примеру, что ожидать от него в ответ? И как потом с полученным предлагаете работать? int|bool|string someValueResult = goodNews(); ??? Это получается, что бы удостовериться в полученном результате, я должен сделать еще двадцать проверок действительно ли мне вернули строку или может быть массив? Или как предлагается работать внутри кода? Опять же, я не думаю, что есть алгоритмы которые умеют хорошо и гармонично работать с разнообразными типами. Все равно придется вернуться к комментариям и прочитать, что делает код и как. Если уж так хочется, проще сделать собственные классы или методы, который будет разруливать типы внутри и выдавать TypeError, чем гадать, что же на самом деле попало к нам в метод и как с этим работать. И проще всегда работать с чем-то унифицированным. Плюсов в общем-то почти не вижу, без этого можно спокойно обойтись.Ответ 3
Насколько я понимаю, в указании этого только на уровне документации беда в том, что написанное там непроверяемо или проверяемо очень тяжело: требует дополнительного разбора, статического анализа или ещё каких-то дополнительных этапов. Я иногда встречаю в clojure в этой роли протоколы. Протокол это абстракция, которая содержит несколько сигнатур методов, а в её документации пишутся указания и ограничения по их реализации. Пока что похоже на интерфейсы. И объявление протокола действительно приводит к созданию одноимённого интерфейса, который можно реализовать кодом не на Clojure, а, скажем, на Java. А теперь весёлая часть: реализовывать интерфейс может любой тип, и этот факт может быть объявлен где угодно. В самом интересном случае вы можете реализовать поддержку протокола из одной библиотеки типом из другой библиотеки в собственном коде. Без вмешательства в код библиотек! Реализовывать протоколы можно даже встроенными типами! Фактически выходит, что проверка на тип протокола автоматически проверяет объект на принадлежность тип-сумме* реализаций (правда, в рантайме: надо писать тесты). Но с возможностью добавить туда что-то ещё. В ООП это аналогично набору классов-обёрток с общим предком или интерфейсом. Значение при этом нужно заворачивать в обёртку до передачи по месту использования, вручную, поскольку на том этапе ещё неизвестно, какую обёртку выбрать. Clojure делает это сам, следуя типу значения. Просто синтаксический сахар, никакого волшебства. Но, в конце концов, мы о языковых средствах и говорим. И, конечно, есть разного рода ассерты. Некоторые из них могут автоматически исключаться при компиляции в релиз. Они обычно выражают то же, что и в документации, но в проверяемом машиной виде. Может показаться минусом тот факт, что за документацией нужно лезть в исходники, но... может, тот факт, что документация и код не разделены, это даже плюс? Банальное предусловие (и единственное встроенное в язык из представленных решений): (defn lookup [arg] {:pre [(or (vector? arg) (isa? arg String) (isa? arg Integer))]} (...реализация...)) Есть core.typed, дополнительная "статическая типизация" (работающая больше как тесты, не при компиляции, а только при явном запуске), там просто есть функция U, принимающая набор типов и возвращающая их тип-сумму*. Это кладётся в аннотацию, рядом с реализацией, хотя можно положить и отдельно. Правда, core.typed так и остался нишевой "игрушкой", которой надо аннотировать вообще всё, иначе от проверок мало толку, а вывод типов работает плохо. (t/ann lookup [(t/U t/Str (t/Vec t/Any) t/Int ) -> DbResult]) ;; А реализация где-то в другом месте Схемы, самый популярный вариант из них это prismatic/schema. Там either, прямой аналог U из core.typed, признан устаревшим, т. к. он был медленным и его нарушения выдавали странные сообщения об ошибках. Частично его функции теперь выполняет cond-pre (и наш случай тоже): (s/defn lookup [arg :- (s/cond-pre s/Str [s/Any] s/Int)] (...реализация...)) Это компактный вариант. Можно результат cond-pre записать куда-то и делать по нему validate/check явно. Необходимо, конечно, чтобы проверки запускались. В тестах, например. Это отдельный запуск, и можно было бы с тем же успехом разбирать комментарии и делать проверки на их основе, но это технически сложнее. *тип-сумма это операция над типами, возвращающая тип, под который подходят значения всех составляющих типов.
Комментариев нет:
Отправить комментарий