Страницы

Поиск по вопросам

Показаны сообщения с ярлыком теория-типов. Показать все сообщения
Показаны сообщения с ярлыком теория-типов. Показать все сообщения

четверг, 26 декабря 2019 г.

Об алгоритме вывода типов рекурсивных функций

#рекурсия #scala #f# #ocaml #теория_типов


В Scala не реализован вывод типов для рекурсивных функций, в качестве аргумента Кей
Хорстман в своей книжки пишет что "Алгоритм Хиндли-Милнера не стабильно себя ведёт
в языках с ООП". Но, например, в F# и OCaml реализован вывод типов для рекурсивных
функций и возникают вопросы: что же тогда имел ввиду Кей Хорстман? Есть ли примеры
программ на ООП языке в которых алгоритм даёт сбой(ошибка согласованности типов в рантайме
при успешно пройденной проверке типов на этапе компиляции)? Или косяк именно в каких-то
особенностях Scala?
    


Ответы

Ответ 1



Система типов Scala однозначно содержит F \sub. Вероятно содержит намного больше (это сейчас активное направление исследований у нас) но уже в F \sub многие вопросы, и в том числе этот - неразрешимая задача. Ссылка на статью Если честно, я не знаю как это работает в F# и OCaml. Думаю они делают best-effort.

воскресенье, 22 декабря 2019 г.

Возможно ли реализовать тип данных более компактный и оптимальный чем String в Java?

#java #строки #типы_данных #типы #теория_типов


Тип данных String (т.е. public final class String) в Java 1.8 занимает 3168 строк
исходного кода (включая все deprecated-методы).
Зачастую, никто не использует все возможности данного типа в конкретном проекте.

Ради спортивного интереса, хочется реализовать компактный тип данных а-ля public
final class CompactFastString с минимальным набором методов и свойств.

Цель - более компактный с точки зрения используемой памяти и более быстрый для работы.
Цель больше лабораторная, нежели реальная.

Но существует мнение, что сложно реализовать более оптимальное решение, чем стандартная
Java SE библиотека.

Что вы думаете на этот счет?
    


Ответы

Ответ 1



Более быстрый в общем случае не получится. Стандартные типы пакета java.lang очень оптимизированы на уровне JVM. Тот исходный код, который вы видите в этих классах - для информации и дебага, а в реальности он заменяется на исключительно быстрые инструкции с учетом возможностей процессора. Например, сравнение строк будет не посимвольным, а с помощью пакетных инструкций SSE, которые умеют сравнивать по 4 байта за такт процессора. В этом и есть преимущество JIT компиляции. P.S. Архитекторы виртуальной машины тоже не очень довольны занимаемой памятью под строки, поэтому планируется в будущих версиях сделать 2 режима работы строк - utf-16 и только latin-1. Но опять же это будет на уровне JVM.

понедельник, 9 декабря 2019 г.

Типизация - языки со “множественной типизацией” (union types) аргументов методов?

#php #ооп #любой_язык #теория_типов


В качестве языка примера буду использовать PHP , для простоты.  Поясню сразу суть
вопроса. В современных языках, особенно это касается интерпритируемых, есть типизация
традиционно не строгая:

function join($a){
   return is_array($a) ? implode('', $a) : $a;
}


А есть типизация строгая:

function join(array $a) : string{
  return implode('', $a);
}


Есть ещё, например в C++ шаблонная типизация:

template 
R* 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 явно. Необходимо, конечно, чтобы проверки запускались. В тестах, например. Это отдельный запуск, и можно было бы с тем же успехом разбирать комментарии и делать проверки на их основе, но это технически сложнее. *тип-сумма это операция над типами, возвращающая тип, под который подходят значения всех составляющих типов.

Что такое фантомный тип и как это связано с traits?

#scala #traits #дизайн_языка #теория_типов


Много раз натыкался на термин «фантомный тип», особенно в контексте обсуждения traits
в языке Scala.

Что это такое? При чём здесь traits?
    


Ответы

Ответ 1



Заранее прошу извинить за длину ответа, просто иначе понять, что такое фантомные типы и как они используются, будет трудно, а тема и правда интересная. Предположим, "Роскосмос" попросил написать нас систему управления запуском ракет. "Ракета должна быть заправлена топливом и кислородом, и только после этого можно запускать", - лаконично констатирует техническое задание. Не долго думая, мы пишем следующий код на языке Scala (в функциональном стиле, не в объектно-ориентированном, почему - будет видно впоследствии): object UnsafeRocketModule { case class Rocket private[UnsafeRocketModule] (hasFuel: Boolean, hasO2: Boolean) def createRocket() = Rocket(false, false) def addFuel(r: Rocket) = Rocket(true, r.hasO2) def addO2(r: Rocket) = Rocket(r.hasFuel, true) def launch(r: Rocket) = if (!r.hasFuel || !r.hasO2) throw new Exception("Попытка запустить неподготовленную ракету!") else println("3-2-1... Пуск!") } Модуль UnsafeRocketModule определяет тип Rocket, функцию-конструктор ракет (т.к. конструктор типа Rocket не экспортируется модулем в целях поддержания инкапсуляции), а также функции заправки ракеты топливом, кислородом, и функцию запуска ракеты. Мы также отслеживаем состояние ракеты (заправленность топливом и кислородом) и генерируем исключение при попытке запустить неподготовленную ракету. Загрузим этот модуль в Scala REPL, а затем попробуем ввести определение функции, которая подготавливает ракету (заправляет топливом и кислородом), и запускает ее: scala> def prepareAndLaunchRocket() { | import UnsafeRocketModule._ | launch(addFuel(createRocket())) | } prepareAndLaunchRocket: ()Unit Все скомпилировалось без ошибок, можно работать: scala> prepareAndLaunchRocket() java.lang.Exception: Попытка запустить неподготовленную ракету! at UnsafeRocketModule$.launch(:13) at $anonfun$prepareAndLaunchRocket$2.apply(:8) at $anonfun$prepareAndLaunchRocket$2.apply(:8) ... пропущено ... Вот незадача: в тексте функции prepareAndLaunchRocket мы заправили ракету топливом, но забыли заправить кислородом! Эта типичная для программ ошибка: попытка выполнить операцию над объектом, который находится в неподходящем для этого состоянии. Этот класс ошибок выявляется во время выполнения программы (run time), и очень часто, уже в процессе эксплуатации. Но мы ведь не хотим, чтобы из-за нашей программы взрывались ракеты, только потому, что в коде мы забыли подготовить ее должным образом, а из-за нехватки времени и внимания написали тесты, не покрывающие этот случай, что не позволило выявить проблему до сдачи в эксплуатацию. Поэтому, исправив функцию prepareAndLaunchRocket, мы понимаем, что пришло время что-то кардинально менять. А именно: мы должны придумать способ гарантировать, что перед запуском (то есть, вызовом функции launch()) ракета будет подготовлена должным образом, то есть будут вызваны обе функции addFuel И addO2. Такая гарантия означает, что попытка запуска неподготовленной ракеты должна теперь выявляться во время компиляции (compile time) программы! Иными словами, компилятор не должен нам позволить скомпилировать ошибочное определение prepareAndLaunchRocket, приведенное выше. А это означает, что это определение не должно пройти проверку типов. То есть, мы должны расширить систему типов, принятую в нашем языке программирования. С этого момента начинается магия. Первое, что мы делаем, это переписываем определение типа Rocket следующим образом: object SafeRocketModule { case class Rocket[Fuel, O2] private[SafeRocketModule] () // ... } Что мы поменяли? Тип Rocket приобрел два параметра типа (Fuel и O2), а атрибуты hasFuel и hasO2 были изъяты. Мы перенесли отслеживание состояния заправленности ракеты топливом и кислородом из системы времени выполнения в систему типов, то есть в систему времени компиляции. То, что отслеживалось атрибутами класса отныне отслеживается параметрами этого типа. Теперь добавим следующие вспомогательные типы: sealed trait NoFuel sealed trait HasFuel sealed trait NoO2 sealed trait HasO2 Эти типы будут использоваться нами как значения времени компиляции вместо значений времени выполнения (true и false для атрибутов hasFuel и hasO2) для индикации состояния заправленности ракеты. Это типы-маркеры, существующие только для подстройки системы типов, они не имеют атрибутов, а их значения (new NoFuel { ... }) нами никогда не будут использованы. Такие типы называются фантомными. А traits являются удобным механизмом их определения. С их помощью мы можем переписать оставшиеся функции модуля: def createRocket() = Rocket[NoFuel, NoO2]() def addFuel[O2](r: Rocket[NoFuel,O2]) = Rocket[HasFuel,O2]() def addO2[Fuel](r: Rocket[Fuel,NoO2]) = Rocket[Fuel,HasO2]() def launch(r: Rocket[HasFuel,HasO2]) = println("3-2-1... Пуск!") Обратим внимание: потребности в проверке заправленности ракеты функцией launch() больше нет. Система типов гарантирует, что launch() будет вызвана только для корректно подготовленной ракеты. Кроме того, становится понятно, почему нам следует использовать функциональный стиль, а не объектно-ориентированный. В последнем случае объект Rocket был бы создан единственный раз и функции addFuel, addO2 и launch вызывались бы впоследствии как его методы. Нам же необходимо менять тип ракеты при вызове соответствующих операций, и создавать значения этих типов с нуля, что и предполагает функциональный стиль. Загрузим модуль SafeRocketModule в Scala REPL, а затем снова попробуем ввести ошибочное определение функции prepareAndLaunchRocket: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(createRocket())) | } :8: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] launch(addFuel(createRocket())) ^ Компилятор не дал ошибочному определению попасть в код программы из-за ошибки во время проверки типов. Исправим определение функции: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(addO2(createRocket()))) | } prepareAndLaunchRocket: ()Unit scala> prepareAndLaunchRocket() 3-2-1... Пуск! Компиляция прошла успешно, пробный запуск осуществлен без ошибок. Теперь в эксплуатацию попадет гарантированно корректная версия этой функции, даже при отсутствии тестов - верификацию выполнила за нас система типов. Наша новая реализация имеет также и другие преимущества, по сравнению со старой: расширенная нами система типов гарантирует, что ракета будет заправлена топливом и кислородом только один раз, не допуская повторных вызовов функций заправки для ракеты, которая уже была заправлена: scala> addFuel(addFuel(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.HasFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,?] addFuel(addFuel(createRocket)) ^ scala> addO2(addO2(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] required: SafeRocketModule.Rocket[?,SafeRocketModule.NoO2] addO2(addO2(createRocket)) ^ Рассмотренный способ применения фантомных типов часто используется при проектировании публичных интерфейсов (API) в таких языках, как Haskell, особенно для сложных интерфейсов. В других языках, с менее развитой системой типов, эти приемы не используются вовсе. К примеру, в Java, насколько мне известно, все это не работает "из коробки", и надо делать уродливую "мумбу-юмбу", чтобы получить похожий результат. Также следует заметить, что это не единственная область применения фантомных типов. Более подробно о последних и о возможных областях применения можно почитать здесь (все источники англоязычные): Phantom Types In Haskell and Scala. Пример с ракетами был адаптирован из этой статьи, где параллельно приводится версия на Haskell. Phantom type (HaskellWiki) A Foundation for Embedded Languages by Morten Rhiger - основательная академическая публикация о фантомных типах и их применениях в Haskell

четверг, 29 ноября 2018 г.

Об алгоритме вывода типов рекурсивных функций

В Scala не реализован вывод типов для рекурсивных функций, в качестве аргумента Кей Хорстман в своей книжки пишет что "Алгоритм Хиндли-Милнера не стабильно себя ведёт в языках с ООП". Но, например, в F# и OCaml реализован вывод типов для рекурсивных функций и возникают вопросы: что же тогда имел ввиду Кей Хорстман? Есть ли примеры программ на ООП языке в которых алгоритм даёт сбой(ошибка согласованности типов в рантайме при успешно пройденной проверке типов на этапе компиляции)? Или косяк именно в каких-то особенностях Scala?


Ответ

Система типов Scala однозначно содержит F \sub. Вероятно содержит намного больше (это сейчас активное направление исследований у нас) но уже в F \sub многие вопросы, и в том числе этот - неразрешимая задача.
Ссылка на статью
Если честно, я не знаю как это работает в F# и OCaml. Думаю они делают best-effort.

четверг, 15 ноября 2018 г.

Возможно ли реализовать тип данных более компактный и оптимальный чем String в Java?

Тип данных String (т.е. public final class String) в Java 1.8 занимает 3168 строк исходного кода (включая все deprecated-методы). Зачастую, никто не использует все возможности данного типа в конкретном проекте.
Ради спортивного интереса, хочется реализовать компактный тип данных а-ля public final class CompactFastString с минимальным набором методов и свойств.
Цель - более компактный с точки зрения используемой памяти и более быстрый для работы. Цель больше лабораторная, нежели реальная.
Но существует мнение, что сложно реализовать более оптимальное решение, чем стандартная Java SE библиотека.
Что вы думаете на этот счет?


Ответ

Более быстрый в общем случае не получится. Стандартные типы пакета java.lang очень оптимизированы на уровне JVM. Тот исходный код, который вы видите в этих классах - для информации и дебага, а в реальности он заменяется на исключительно быстрые инструкции с учетом возможностей процессора. Например, сравнение строк будет не посимвольным, а с помощью пакетных инструкций SSE, которые умеют сравнивать по 4 байта за такт процессора. В этом и есть преимущество JIT компиляции.
P.S. Архитекторы виртуальной машины тоже не очень довольны занимаемой памятью под строки, поэтому планируется в будущих версиях сделать 2 режима работы строк - utf-16 и только latin-1. Но опять же это будет на уровне JVM.

суббота, 13 октября 2018 г.

Что такое фантомный тип и как это связано с traits?

Много раз натыкался на термин «фантомный тип», особенно в контексте обсуждения traits в языке Scala.
Что это такое? При чём здесь traits?


Ответ

Заранее прошу извинить за длину ответа, просто иначе понять, что такое фантомные типы и как они используются, будет трудно, а тема и правда интересная. Предположим, "Роскосмос" попросил написать нас систему управления запуском ракет. "Ракета должна быть заправлена топливом и кислородом, и только после этого можно запускать", - лаконично констатирует техническое задание. Не долго думая, мы пишем следующий код на языке Scala (в функциональном стиле, не в объектно-ориентированном, почему - будет видно впоследствии): object UnsafeRocketModule { case class Rocket private[UnsafeRocketModule] (hasFuel: Boolean, hasO2: Boolean)
def createRocket() = Rocket(false, false) def addFuel(r: Rocket) = Rocket(true, r.hasO2) def addO2(r: Rocket) = Rocket(r.hasFuel, true) def launch(r: Rocket) = if (!r.hasFuel || !r.hasO2) throw new Exception("Попытка запустить неподготовленную ракету!") else println("3-2-1... Пуск!") } Модуль UnsafeRocketModule определяет тип Rocket, функцию-конструктор ракет (т.к. конструктор типа Rocket не экспортируется модулем в целях поддержания инкапсуляции), а также функции заправки ракеты топливом, кислородом, и функцию запуска ракеты. Мы также отслеживаем состояние ракеты (заправленность топливом и кислородом) и генерируем исключение при попытке запустить неподготовленную ракету. Загрузим этот модуль в Scala REPL, а затем попробуем ввести определение функции, которая подготавливает ракету (заправляет топливом и кислородом), и запускает ее: scala> def prepareAndLaunchRocket() { | import UnsafeRocketModule._ | launch(addFuel(createRocket())) | } prepareAndLaunchRocket: ()Unit Все скомпилировалось без ошибок, можно работать: scala> prepareAndLaunchRocket() java.lang.Exception: Попытка запустить неподготовленную ракету! at UnsafeRocketModule$.launch(:13) at $anonfun$prepareAndLaunchRocket$2.apply(:8) at $anonfun$prepareAndLaunchRocket$2.apply(:8) ... пропущено ... Вот незадача: в тексте функции prepareAndLaunchRocket мы заправили ракету топливом, но забыли заправить кислородом! Эта типичная для программ ошибка: попытка выполнить операцию над объектом, который находится в неподходящем для этого состоянии. Этот класс ошибок выявляется во время выполнения программы (run time), и очень часто, уже в процессе эксплуатации. Но мы ведь не хотим, чтобы из-за нашей программы взрывались ракеты, только потому, что в коде мы забыли подготовить ее должным образом, а из-за нехватки времени и внимания написали тесты, не покрывающие этот случай, что не позволило выявить проблему до сдачи в эксплуатацию. Поэтому, исправив функцию prepareAndLaunchRocket, мы понимаем, что пришло время что-то кардинально менять. А именно: мы должны придумать способ гарантировать, что перед запуском (то есть, вызовом функции launch()) ракета будет подготовлена должным образом, то есть будут вызваны обе функции addFuel И addO2. Такая гарантия означает, что попытка запуска неподготовленной ракеты должна теперь выявляться во время компиляции (compile time) программы! Иными словами, компилятор не должен нам позволить скомпилировать ошибочное определение prepareAndLaunchRocket, приведенное выше. А это означает, что это определение не должно пройти проверку типов. То есть, мы должны расширить систему типов, принятую в нашем языке программирования. С этого момента начинается магия. Первое, что мы делаем, это переписываем определение типа Rocket следующим образом: object SafeRocketModule { case class Rocket[Fuel, O2] private[SafeRocketModule] ()
// ... } Что мы поменяли? Тип Rocket приобрел два параметра типа (Fuel и O2), а атрибуты hasFuel и hasO2 были изъяты. Мы перенесли отслеживание состояния заправленности ракеты топливом и кислородом из системы времени выполнения в систему типов, то есть в систему времени компиляции. То, что отслеживалось атрибутами класса отныне отслеживается параметрами этого типа. Теперь добавим следующие вспомогательные типы: sealed trait NoFuel sealed trait HasFuel sealed trait NoO2 sealed trait HasO2 Эти типы будут использоваться нами как значения времени компиляции вместо значений времени выполнения (true и false для атрибутов hasFuel и hasO2) для индикации состояния заправленности ракеты. Это типы-маркеры, существующие только для подстройки системы типов, они не имеют атрибутов, а их значения (new NoFuel { ... }) нами никогда не будут использованы. Такие типы называются фантомными. А traits являются удобным механизмом их определения. С их помощью мы можем переписать оставшиеся функции модуля: def createRocket() = Rocket[NoFuel, NoO2]() def addFuel[O2](r: Rocket[NoFuel,O2]) = Rocket[HasFuel,O2]() def addO2[Fuel](r: Rocket[Fuel,NoO2]) = Rocket[Fuel,HasO2]() def launch(r: Rocket[HasFuel,HasO2]) = println("3-2-1... Пуск!") Обратим внимание: потребности в проверке заправленности ракеты функцией launch() больше нет. Система типов гарантирует, что launch() будет вызвана только для корректно подготовленной ракеты. Кроме того, становится понятно, почему нам следует использовать функциональный стиль, а не объектно-ориентированный. В последнем случае объект Rocket был бы создан единственный раз и функции addFuel, addO2 и launch вызывались бы впоследствии как его методы. Нам же необходимо менять тип ракеты при вызове соответствующих операций, и создавать значения этих типов с нуля, что и предполагает функциональный стиль. Загрузим модуль SafeRocketModule в Scala REPL, а затем снова попробуем ввести ошибочное определение функции prepareAndLaunchRocket: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(createRocket())) | } :8: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] launch(addFuel(createRocket())) ^ Компилятор не дал ошибочному определению попасть в код программы из-за ошибки во время проверки типов. Исправим определение функции: scala> def prepareAndLaunchRocket() { | import SafeRocketModule._ | launch(addFuel(addO2(createRocket()))) | } prepareAndLaunchRocket: ()Unit
scala> prepareAndLaunchRocket() 3-2-1... Пуск! Компиляция прошла успешно, пробный запуск осуществлен без ошибок. Теперь в эксплуатацию попадет гарантированно корректная версия этой функции, даже при отсутствии тестов - верификацию выполнила за нас система типов. Наша новая реализация имеет также и другие преимущества, по сравнению со старой: расширенная нами система типов гарантирует, что ракета будет заправлена топливом и кислородом только один раз, не допуская повторных вызовов функций заправки для ракеты, которая уже была заправлена: scala> addFuel(addFuel(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.HasFuel,SafeRocketModule.NoO2] required: SafeRocketModule.Rocket[SafeRocketModule.NoFuel,?] addFuel(addFuel(createRocket)) ^
scala> addO2(addO2(createRocket)) :10: error: type mismatch; found : SafeRocketModule.Rocket[SafeRocketModule.NoFuel,SafeRocketModule.HasO2] required: SafeRocketModule.Rocket[?,SafeRocketModule.NoO2] addO2(addO2(createRocket)) ^ Рассмотренный способ применения фантомных типов часто используется при проектировании публичных интерфейсов (API) в таких языках, как Haskell, особенно для сложных интерфейсов. В других языках, с менее развитой системой типов, эти приемы не используются вовсе. К примеру, в Java, насколько мне известно, все это не работает "из коробки", и надо делать уродливую "мумбу-юмбу", чтобы получить похожий результат. Также следует заметить, что это не единственная область применения фантомных типов. Более подробно о последних и о возможных областях применения можно почитать здесь (все источники англоязычные): Phantom Types In Haskell and Scala. Пример с ракетами был адаптирован из этой статьи, где параллельно приводится версия на Haskell. Phantom type (HaskellWiki) A Foundation for Embedded Languages by Morten Rhiger - основательная академическая публикация о фантомных типах и их применениях в Haskell

Типизация - языки со “множественной типизацией” (union types) аргументов методов?

В качестве языка примера буду использовать PHP , для простоты. Поясню сразу суть вопроса. В современных языках, особенно это касается интерпритируемых, есть типизация традиционно не строгая:
function join($a){ return is_array($a) ? implode('', $a) : $a; }
А есть типизация строгая:
function join(array $a) : string{ return implode('', $a); }
Есть ещё, например в C++ шаблонная типизация:
template R* 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 году, но на голосовании - отклонили.


Ответ

Интересный вопрос. Не знаю как в других языках обстоят дела, но в 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)