Пользователь
0,0
рейтинг
26 июня 2013 в 09:14

Разработка → Развитие пользовательских типов данных в программировании из песочницы

Хотелось бы остановиться и посмотреть на развитие языков программирования с точки зрения развития пользовательских типов данных (ПТД).
Сразу хочу оговориться, под пользователями понимаются программисты, как люди, пишущие код на этих языках. Ну, и те, кто этот код сопровождает или просто читает.

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

Пользователи желают иметь примерно такие типы данных

Пользователи хотели иметь возможность составлять данные так, как они сами того хотят. Хотели, хотят, и наверняка будут хотеть. Всё больше, всё разнообразней и сильнее.
Именно поэтому полезно проследить за развитием пользовательских типов данных в программах и языках программирования.

Отсутствие пользовательских типов данных



Когда-то пользовательских типов данных не было. Как-то выкручивались

На заре компьютерной эры языки были не ахти – машинные: или ты подчиняешься их диктату (а диктат там простой: или ты используешь малобитовые числа в двоично-десятичной системе исчисления (ну, или с какой процессор работает) и команды процессора) или нет.
Затрагивать те «тёмные века» мы не будем.
Одно можно сказать – пользовательских типов данных там не было, но программисты как-то выживали и как-то писали программы.

Встроенные типы данных



Встроенные типы такие удобные! Пока ими пользуешься так, как было запланировано разработчиками...

Первым более-менее нормальным языком был Ассемблер (на самом деле их много ассемблеров, мы говорим о костяке языков, который появился в 50х годах). Помимо читабельности, он принёс в себе много нового в плане пользовательских данных.
Самое крупное и неоспоримое достижение – это возможность создания переменных! С тех пор, почти во все языки вставлена такая возможность – возможность создавать переменные.
Второе не менее крупное достижение ассемблера, сделанное из благих побуждений, — попытка вставить в язык все типы данных (из тех, что возможно понадобятся программисту) прямо в язык.
Ну и остальное по мелочи – прежде всего, возможность записывать числа не только в двоичной, но и 16-ричной, восьмеричной, двоично-десятичной системах.
Тогда казалось, ну что ещё пользователю может понадобиться?

Шли годы, а необходимость не только в высокоуровневых абстракциях, но и в пользовательских типов данных всё росла.
И вот грянул 1957й год с Фортраном.
Написанный код на нём выглядел почти как современные языки, хотя его же вариант на перфокартах может подвергнуть в шок людей, которые захотят его читать.
Фортран дал всё что необходимо… для расчёта полёта баллистических ракет – такие данные как целые числа (типа int), с запятой (типа float), и комплексные.
Строки? Кому они нужны на перфокартах? Они в Фортране появятся позже – настоящие строки только через 20 лет, а их имитация – через 10 (вместе с логическим типом данных).
А ещё Фортран дал почти-настоящий пользовательский тип данных как массив (хотя его использование несколько отличается от современного), более детально об этом мы поговорим в главе о Групповых пользовательских данных.

Но пользователям мало — они хотят ещё и ещё пользовательских данных.

И вот появляется он – Алгол, уже в 1958, программы на котором легко читаются и в наши дни.
Вот как раз Алгол принёс основу того, что сейчас есть везде — булевские типы, строковые, разнообразные целочисленные типы и числа с запятой. Немногим позже всё это применит и Фортран, но Алгол был пионером.
Казалось бы — все аппетиты удовлетворены, какие ещё типы необходимы пользователям? Да всё уже реализовано — только бери и пользуйся.

И тут наступая на пятки Алголу с Фортраном, в 1958 появился ещё один, совершенно непохожий на язык язык – Лисп.


Лисп может сделать невообразимые функции. Только как с этим жить?

Он дал ещё один, совершенно новый тип данных, настоящих пользовательских типов данных – функции (вида С-выражений), прочно начавших входить во все современные языки только с начала 21 века (прежде всего, благодаря мультипарадигменным языкам Питон и Руби). Если учесть, что Лисп даёт оперировать макросами – что-то вроде программирование eval-ами (в 58-то) – неудивительно, что мир был к нему не готов.
А вот готов ли сейчас мир к Лиспу? Наверное, нет.
Заострю внимание, почему. Лисп, как и любое другое функциональное программирование, работает с сильно-взаимосвязанными объектами, чем-то напоминает сцепленные шестерёнки механических часов. В отличие от императивных языков, любое вклинивание в шестерёнку – стопорит весь механизм. Из-за этого, требования к языку, в том числе и к пользовательским типам данных значительно строже. Те, проблемы, которые возникли у Лиспа сразу же, у императивных языков они обострились лишь к концу 80х годов.
Лисп даёт возможность построить любые С-выражения. Но он даёт лишь один инструментарий – стандартный и простой инструментарий работы с С-выражениями.
Получается, есть возможность написать любые пользовательские данные, но работать с ними можно лишь как с примитивами. Развитие Лисп-подобных языков показало что пока хороший инструментарий не найден для ненаписанных С-выражений.

Недолго все были довольны. На почти десятилетие замедлилось развитие пользовательских данных.
Если что-то и хотели, то допиливали эти типы данных прямо в язык. Но в те времена языки развивались и обновлялись не так быстро как сейчас, поэтому в основном занимались имитацией создания пользовательских типов данных.

Имитация от оригинальных типов данных почти не отличается по функциональности. В основном в те времена имитация основывалась на эмуляции функциями (процедурный стиль).
Главное отличие от настоящих пользовательских типов одно – если захочется «немного» изменить тип данных, придётся переписывать весь функционал. Это никому не хотелось делать.
Поэтому и имитировать стали гибче.
Сейчас скажу то, что знают все, однако тут важна не сама описываемая техника имитации, а угол зрения на эту технику.


Иногда надо работать только с теми возможностями, которые есть

Вначале появился конфигурируемый функционал на основе флагов. Это не самый простой метод, зато он очень сильно напоминал ассемблер, а его тогда ещё знали.
Суть передачи флагами проста — передача числа как параметра, число же представляется как двоичный ряд флагов. Несмотря на широкий способ использования, до сих пор в языках нет специальных типов данных вроде ряда флагов. Вместо этого нашли хороший заменитель – именованные константы и битовые операции, выглядящие как логические.
Наиболее простой метод – он до сих пор широко применяется везде – конфигурирование параметрами: чем больше надо конфигурировать, тем больше параметров передаётся. Один только имеет минус – слишком легко запутаться в порядке передачи параметров. Поэтому стараются такими методами передавать до 4-5 аргументов.
Для большего количества аргументов, с развитием настоящих пользовательских данных, прежде всего групповых и композитных, появилась возможность передавать один сложный аргумент – это то же самое конфигурирование только не горизонтально, а вертикально.
Развитием этого метода можно назвать создание встроенных языков (DSL) для конфигурирования функций.
Третий метод гибкости имитаторов — это изобретение манипуляторов (handler-ов), хотя в то время они так не назывались, и зачастую они были суррогатами манипуляторов — представляли собой числа и строки, а не ссылки или указатели.

Заканчивалась эра встроенных типов данных.
Но наступил 1972 год, появился… Си. И эра господства динозавров (встроенных типов данных) продолжилась ещё на одно десятилетие, хотя пользовательские типы данных начали отвоёвывать своё место под солнцем.
В том числе и в самом Си.

Но пока вернёмся к ещё одному встроенному типу данных, ставшим одной из причин роста популярности языка. Си ввёл низкоуровневый тип данных, который был в ассемблере, и начисто был забыт в первых высокоуровневых языках — динамические типы. Прежде всего, это ссылки(reference) и указатели(pointer). К ним добавился обслуживающий нулевой тип данных – null.


Динамические типы данных как отмычки — неказистые, зато в какие только потаённые комнаты нельзя с ними попасть?!

Ссылку можно рассматривать как один из вариантов реализации такого типа пользовательских данных как синоним.
Развитие синонимов можно найти в ПХП и его концепцией переменных переменных, когда можно вернуть значение переменной или функции, имя которой записано в значении вызываемой переменной.
В Си вызов функции можно вызвать фукнцией-вызовом (call), или можно передать функцию обратного вызова — callback.
Вдобавок к этому, динамические типы данных помогают хорошо ускорить выполнение скомпилированного кода.
К этим плюсам у динамических типов данных есть ещё один огромный плюс – с ними достаточно просто можно реализовывать то, что не было заложено в самом языке. Лишь одно сокрушает, к написанному с их использованием можно обращаться только с помощью инструментария работы с динамическими данными. Но технику обхода при имитации данных известна – замкнуть в функциях и возвращать ссылку/указатель на созданное – манипуляторы (handler). Манипуляторы — один из тех типов данных, в разних языках которых он может называться совершенно по-разному.
Например, в ПХП они называются ресурсами, в Эрланге — портами и идентификаторами процессов. В Коболе есть такой тип данных как файл и картинка.

Однако динамические типы данных – не только одни плюсы. Минусы тоже есть, порой очень большие.
Чем больше свободы в использовании динамических типов данных даётся языком, тем:
1) увеличиваются возможности по созданию того, чего не было заложено в языке (и не обязательно это позитивные возможности)
2) компилятор всё меньше вмешивается в действия пользователя, и вся ответственность за действия ложится на программиста
2) резко увеличивается небезопасность кода
3) резко увеличивается возможность инъекций в пространство имён и значений
4) сборщик мусора всё меньше вмешивается, и вся ответственность ложится на пользователя
Дальнейшая история показала, что создатели последующих языков (или при добавлении к уже существующим языкам) при добавлении динамических данных балансировали межу безопасностью и возможностями.

Близился конец 70х, и эти базовые встроенные типы данных стали уходить в периферию, в рутину, уступая место настоящим пользовательским данным.

Однако реальность преподносит иногда удивительные сюрпризы.
Кто знает, сколько ещё можно найти в старых-добрых и давно понятных типах данных?!


Иногда надо просто увидеть новое там, где всё давно известно. Как, например, эта сортировка М&Мs

Тогда, в конце 70х появился скриптовый язык AWK (использовавший разработки утилиты grep), а, десятилетием позже на его основе, в 1987 появился такой язык как Перл. И среди прочего у него был (до сих пор есть) такой экзотический встроенный тип данных, как регулярные выражения.
Перл помог взглянуть на такой старый тип данных как строки с новой стороны. Инструментарий по работе с ним в ранних языках можно было рассматривать как упрощённые парсеры.
Языки регулярных выражений оказались очень гибким и супер-мощным инструментарием для работы с символьными типами данных.

Групповые типы данных


Некоторые группы большие. Хочется с ними быстро работать

По сути, групповые типы данных – это много чего-то того, что есть в языке, как правило мономорфная группировка. Зачастую эти типы данных не пользовательские, а встроенные, однако они порой столь гибкие, что этого бывает достаточно.
Уже Фортран поддерживал групповые типы данных – это массивы (array), хотя они выглядели немного не так, как сейчас. Массивы, очень похожие на современные были уже в Алголе. В Паскале были множества (set)
В Лиспе были списки (list).
Потом появились хеш-таблицы, ассоциированные массивы, вектора, кучи, очереди, стеки, деревья…
Что-то встраивалось, что-то имитировалось или создавалось с помощью пользовательских типов данных.
Дальнейшее развитие групповых типов данных привело к 2 различным веткам развития
1) необходимость использования своего функционала с каждым групповым типом данных было не самое удовлетворительное, хотелось работать с ними единообразно. Основным инструментарием для этого в императивных языках являются коллекции и итераторы. В основном были добавлены в ранние 2000е.
2) В 80е годы с развитием роста данных, необходимость в расширении инструментария работы с групповыми типами данных росла как на дрожжах. Появились базы данных, а с ними и запросы и языки запросов (query language). В середине 80х Стуктурированный язык запросов (SQL) становится доминирующим. Как и парсеры для строк, язык запросов дал понять, что инструментарий, который использовался для групповых типов данных можно рассматривать как примитивный язык запросов. Базы данных, как правило, вынесены из языка, а в языке существуют лишь методы работы с ними, поэтому их нельзя считать полноценными пользовательскими типами данных. Хотя из-за их гибкости, это несущественно.

Настоящие пользовательские типы данных


В эру встроенных типов данных в разных языках можно найти поддержку весьма экзотических встроенных типов данных.
Например, в Си таким типом данных было перечисление (enum). Его до Си, впервые вводит Паскаль (1970), хотя и называл скаляром.


Что такое перечисление легко объяснить даже детям на пальцах

Перечисление — самый настоящий пользовательский тип данных! Казалось бы, надо Си поставить памятник за это. Нет, разве что надгробный.
Дело в том, что пользователи имеют возможность построить любые перечисления. Только вот для работы с ними в Си нет ничего (в Паскале был базовый набор). Вообще ничего. Создавать перечисления можно, а работать с ними — нельзя.
Поскольку Си был в мейнстриме, мало кто хотел добавлять этот тип данных в другие языки. Только в Си++11 появился хоть какой-то инструментарий по работе с перечислениями.
Этот пример, как и развитие Лиспа, показал, насколько важно иметь не только пользовательские типы данных, но и инструментарий работы с ними.

Композитные типы данных


Записи такие разнообразные. Ещё бы научится как ими пользоваться

Зато в Си был другой настоящий пользовательский тип данных. Хотя его придумали значительно раньше – ещё в Коболе, вышедший в ранние 60е (сам язык создан в 1959).
Это запись (record), в Си называется структурой (struct).
Запись – не что иное, как группа разнородных типов данных.
К записям прилагается инструментарий с работой с записями. Например, Си не в полном объёме даёт стандартный ныне минимум работы с записями (например, лишь однобокая инициализация).
С записями легко уже не сымитировать, а создать настоящие списки и деревья.
Неужели, снова всё есть в языке?
Нет, и ещё раз нет.
Уже мало иметь просто пользовательские типы данных. Мало иметь инструментарий работы с ПТД как с ПТД. На первый план выходит другое.
Никто не даёт никакого инструментария для несозданных типов данных!
Теперь уже языки с поддержкой записей попались в похожую ловушку на ту, в какую попал Лисп – свои данные создавать можно, а работать с ними – только базовый набор.
Только у Лиспа ситуация хуже: в этом языке всё является С-выражениями, а в Си, Коболе, и других, запись – самостоятельный тип, к тому же у него есть свой, хоть и небольшой, инструментарий.
Благо, решение этого тупика был давно известен — имитация работы с пользовательскими типами данных с помощью функций.

Именно благодаря записям/структурам (в том числе и в Си), пришло к программистам осознание важности пользовательских типов данных.
При этом явно обозначилась острая нехватка инструментов по работе с ещё не созданными типами данных.

А ответ был. Как ни удивительно звучит, но был ещё за пару лет до создания Си, он был в Европе, и назывался Симула (1967). И тогда, когда Си начал задыхаться в нехватке инструментария пользовательских типов данных, Си++ (в 1983) перенял всё лучшее у Симулы и применил к Си синтаксису.

Объекты


Объекты могут всё делать. Сами и над самими собою

Объекты(object) – ещё один тип пользовательских данных. Он обладает значительно большими возможностями, чем было у записи.
Это дало ему возможность завоевать просто бешеную популярность.
По иронии доли, как и Симула, вышедшая за пару лет до С, в котором не было объектов, так и за пару лет до Си++ Смолтолк в 1980 объявил парадигму «всё объекты».
Но Смолтолк не завоевал большой популярности, ему пришлось ждать, пока Си++ дойдёт до уровня стагнации, и только после этого, Ява в 1995 вновь смогла поднять парадигму «всё объекты» гордо над головой.
Чем же так хороши объекты, ведь они не так уж сильно отличаются от записей. По сути – те же записи с добавлением методов.
Во-первых, инструментарий по работе с самими объектами значительно богаче и сильней, чем инструментарий по работе со структурой.
А во-вторых, инструментария по работе с ещё не созданными объектами… тоже не было.
Стоп, спрашивается, где же тут «во-вторых», если и у объектов нет никакого инструментария с несозданными типами данных, и у записей нет. И, тем не менее, во-вторых! Для записей необходимо было имитировать этот инструментарий, в то время как у объектов можно просто реализовать этот инструментарий внутри самого объекта!
А если вдруг надо было немного изменить пользовательский тип данных, удобно было с помощью инструментария объектов — наследованием создать потомка, и в нём исправить поведение.
Бум и тотальное использование объектов привело ныне к стагнации.
Что же сейчас мешает объектам и дальше развиваться?
Как мы помним, реализация инструментария нового объекта полностью лежит в ответственности у программиста, а не на языке, поэтому уровень переиспользования кода не такой большой, как мог бы быть.
Не менее важна и всё большая закрытость. Объект сам всё будет делать, хотя и делать всего от него почти никогда не нужно. И наоборот, обладая возможностью всё делать самому, другим этот объект ничего делать не будет. Может, но не будет.
Частично проблему помогает решать введение интерфейсов (interface) и примисей (mixins, traits).
Интерфейсы впервые вводят Делфи (в 1986 ещё как Объектный Паскаль), позже Ява и Си#. И это понятно – они были лидерами в объектных языках.
А вот что удивительно, примести/трейты появились при попытке присоединить объекты к Лиспу (Flavors, CLOS) (CLOS — часть Коммон Лиспа), позже добавлены в разные языки.
Однако даже такие абстрактные помощники, как интерфейсы и примеси не всегда помогают, например, к старому объекту с меткой «финализированный» (final). Частично можно решить проблему гибридизации на основе прототипного наследования (открытого языком Селф(диалектом того самого Смолтолка) в середине 1980х и получившего популярность благодаря, прежде всего, ЯваСкрипту десятилетием позже), однако у этого метода есть свои минусы относительно классового наследования.
Интересна поддержка метаклассов (metaclass), которые были заложены в Смолтолк ещё в 1980 и ныне поддерживаются некоторыми языками, например, Питоном. Метаклассы работают с классами как объектами (такой себе рекурсивный подход). Это сильно улучшает инструментарий работы с объектами как объектами.
Ныне на первое место приходит не создание нового объекта, а грамотный подход к проектированию системы с использованием шаблонов(паттернов) проектирования.
Что будет дальше? Вопрос риторический.
Есть ли альтернативы столь мощным пользовательским типам данных, как объекты, структуры? Есть, и даже лучше, чем объекты! Стоит взглянуть на них получше, что бы понять, как могут в дальнейшем развиваться объекты.

Поиск альтернативы


Христос и Кришна вместе. Императивность и Функциональность могут быть вместе

Куда бы посмотреть в поисках альтернативы?
Декларативные (типа HTML) и логические языки (типа Пролога) на сегодня альтернативы не содержат. Они основаны на том, что вместо программиста работает компилятор/интерпретатор.
И тут надо либо
1) просто бросить попытки добавить пользовательские типы данных и войти в симбиоз с другим языком (например HTML + ЯваСкрипт)
2) подключить другие парадигмы программирования.

Кстати, на счёт подключения других парадигм, казалось бы, чем хорошо иметь мультипарадигменные языки? Питон (1991) и Руби (1994) так не думали.
И, оказались правы. Там, где легко уделывал всех Лисп – удобно применять парадигму функционального программирования, где нужна простота — там процедурный стиль, на остальные случаи — хорошо справляются объекты.
Казалось бы, никаких новых пользовательских данных не добавилось, а эффективность написания кода сильно возросла.
И вот когда на дворе 2011, даже в Си++ пришли лямбда-функции из Лиспа.
Из Явы выросла Скала с 2003, приняв посылку, что объекты — это ещё и функции.

Метапрограммирование – это хорошо. Но, как правило оно позволяет либо расширить(сделать гибче) встроенные типы данных, либо для порождения нового кода или просто возможности создать обобщения, работает с встроенными типами данных. Пока что только со встроенными. Но даже без пользовательских типов данных, за метапрограмированием будущее(а уже во многом и настоящее) в плане избавления от рутины программирования.

Алгебраические Типы Данных


АТД такие красивые, что хочется вставить в рамочку и повесить. А иногда, там оставить и больше не трогать

Ух ты, как грозно звучит!
Стандартный МЛ в 1990 впервые вводит Алгебраические Типы Данных (АТД).
Алгебраические типы данных – это один из самых мощных пользовательских типов данных. Найдены они были математиками Хиндли и Милнером в лямбда-исчислении.
АТД — это «всё в одном» — унитарные типы данных (типа null), перечисление (типа enum, bool), защищённые базовые типы (типа как resource в противовес ссылке), переключательные типы (что-то вроде в Си — union), спископодобные структуры, древовидные структуры, функции, кортежи, записи, объекто-подобные структуры (но не объекты). И любые комбинации всего этого.
Это значительно лучше объектов! По крайней мере, с точки зрения создания разнообразных самих пользовательских типов данных – да, лучше, безусловно.
Только с других точек зрения этот тип данных как в Стандартном МЛе, так и в более позднем ОКамле – весьма и весьма скуден.
Инструментарий для работы с АТД как с АТД не сильно больше, чем работой с записями, и значительно меньше, чем с объектами.
Во-вторых, инструментария для работы с ещё несозданными типами данных нет. И, в отличие от объектов, прятать самописный инструментарий некуда. Только имитировать.
Камль, как потомок Стандартного МЛ, недолго думая, добавил к себе объекты и стал ОКамлем (к 1996му). И параллельно ОКамль начал развивать альтернативное, более функциональное решение, куда спрятать реализацию инструментария по работе с пользовательскими данными – в параметрические модули. И, за лет 15 ОКамль построил достойную функциональную замену объектам. Тут ещё интересен подход – как мы помним, параметрические модули-функции стали вводить для того, что бы избавиться от проблемы отсутствия инструментария для ненаписанных данных АТД, только вот сами параметрические модули ныне почти не отличаются от АТД, а значит, сейчас нет инструментария для ещё не написанных… модулей. Прямо рекурсия!
А за эти 15 лет ОКамль находит ещё один вариант решения проблемы отсутствия инструментария по несозданным типам данных, он их так и называл — вариант (variant). Это иной, не АТД тип данных, хотя внешне похож. Это переключательный тип данных, заодно им можно делиться в любых пропорциях, включая смешивание (АТДанными делиться в пропорциях нельзя, равно как и смешивать). Хорошо, что можно пропорционально делиться (с объектами можно добиться лишь с помощью интерфейсов или трейтов, да и то – не полностью), зачастую заманчиво смешивание (такое с объектами не сотворишь), плохо, что в любых пропорциях. Тут есть ещё над чем работать. Этот тип данных недоразвит. Из простых путей развития – добавить в доселе скудный инструментарий работы с вариантами инструментарий множеств.

Алгебраические Типы Данных в паре с Классами типов


АТД вместе с классами типов выглядят грубо, но способны на многое

В начале 90х на основе Стандартного МЛ, а так же нескольких академических языков, был разработан ещё один язык, впервые стандартизированный лишь в 1998. Им был Хаскель.
У него были те же Алгебраические типы данных, что у Стандартного МЛ и ОКамля. И такой же скудный набор по работе с АТД как с АТД. Но у Хаскеля было (и до сих пор есть) то, чем не обладал доселе ни один тип пользовательских данных. У Хаскеля есть инструментарий для ещё ненаписанных пользовательских типов данных – это классы типов (class).
Сами классы типов представляют собой что-то вроде интерфейсов или примесей, наиболее близко к ролям (roles) в Перле, только введение классов отличается от введения интерфейсов/трейтов.
Интерфейсы для простых классов типов, примеси – для усложнённых.
Причём для сложных классов аналогия с примесью уже не будет подходить. К сложным классам нет необходимости подключать полностью данные, достаточно присоединиться в точках входа, или в одной из нескольких точек входа, если это разрешено.
Причём с помощью реализации класса (instance) можно делиться инструментарием, причём не только с кодом, написанным позже создания класса, но и для данных, созданных ранее (как если можно было добавить поведение финализированным объектам). Если брать аналогию в объектных языках, это достигается, прежде всего, путём присоединения не интерфейсов и примесей к объектам, а наоборот, объектов к примесям и интерфейсам (частично это уже есть у ролей в Перле).
Но Хаскель не остановился даже на этом. Он реализовал автоматическое выведение (deriving) классов для различных типов данных.

Это уже однозначно значительно больше по возможностям пользовательских типов данных, чем заложено в объектах.

Хаскель бурно развивается. Уже сейчас Алгебраические Типы Данных – лишь часть большего.
Ныне вполне можно создать семьи данных, Обобщённые Алгебраические Типы Данных (GADTs), экзистенциальные, многоранговые.
Классы типов тоже не стоят на месте. Уже сейчас можно использовать многопараметрические, функционально-зависимые, разновидовые классы.
Расширяется инструментарий работы с АТД как с АТД в контексте метапрограммирования.
Многое из этого нового существует как расширения языка, а в 2014 году это может войти в стандарт языка.

Заключение


История открыто показала острую необходимость в пользовательских типов данных.
Пользовательские типы данных нужны всё больше. Всё разнообразней. С глубокой поддержкой инструментария, способного работать как с обобщённым типом пользовательских данных, так и с тем, что только будет создано программистами.
Как только удалось спрятать самописный инструментарий в сами пользовательские данные — начался бурный рост пользовательских данных.
Однако, как показала история, иногда недостаточно языку владеть прогрессивными технологиями, для того, что бы стать популярным. Изобретение объектов в Симуле до появления Си (в котором даже не было объектов) не сделало прорыва для самой Симулы.
История показала, что находить под ногами то, что никто не видит — тоже полезно. Объединение нескольких парадигм и достижением этим большего, чем по отдельности (Руби, Питон, ..), открытие низкоуровневых динамических типов данных(Си), обобщение работы со строками — как работой парсерами (Перл) — очень помогло как этим самим языкам, так и программированию в целом.
Найдут ли Лисп-подобные языки свой инструментарий для ещё не созданных типов данных?
Остановятся ли объекты в развитии? Если нет, то куда пойдут — по пути языка Скала? Руби? Перл?
Когда наступит золотой век Алгебраических типов данных? Есть ли шансы развиться у вариантного типа данных?
Будут ли заимствованы хаскелевские классы другими языками? Куда пойдёт Хаскель?
Время покажет и ответит на вопросы.
@Vitter
карма
36,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (49)

  • +13
    Картинки!
  • 0
    Экспоненциальные типы (то бишь лямбды)? Экзистенциальные? Зависимые?
    • 0
      экспоненциальных? ))

      Экзистенциальные данные — со скрытыми параметрами.

      data Counter a = forall self. NewCounter
          { _this    :: self
          , _inc     :: self -> self
          , _display :: self -> IO ()
          , tag      :: a
          }
      


      Зависимые классы помогают выявить зависимость между передаваемыми параметрами класса и примерно выглядят так:
         class Collects e ce | ce -> e where
            empty  :: ce
            insert :: e -> ce -> ce
            member :: e -> ce -> Bool
      
      • +1
        Экспоненциальные — это лямбды. A -> B.

        Зависимые типы — это другое. Их в хаскеле нет, они есть в Agda и прочих Coq-ах.

        В Scala есть такая фигня под названием «path-dependent types» (Шишков, переводи сам), это такой маленький кусочек настоящих зависимых типов.

        Я просто перечисляю то, что в статье не упомянуто.
        • 0
          На счёт Скала — спасибо, попробую добавить. Эти «путе-зависимые» объекты добавляют межобъектное (одного класса) взаимодействие, но не межклассовое. Правильно?

          Зависимые типы — отлично замечено. Я их не рассматривал, так как Agda и Coq используются для доказательств, а не программирования естественных задач.

          Лямбда функции, хоть их техническая реализация может отличатся от обычных функций в языке, тем не менее, являются обычными функциями, с такими же возможностями. Были ещё в раннем Лиспе.
          • 0
            Эти «путе-зависимые» объекты добавляют межобъектное (одного класса) взаимодействие, но не межклассовое. Правильно?


            Либо я вас не понял, либо не правильно.

            Вот в докладе Living in a Post-Functional World с конференции flatMap(Oslo) есть хорошее объяснение что такое path-dependent types. Примерно с 14 минуты идет введение из других языков, пример на скале начинается с двадцатой минуты.

            При этом если просто гулить, то зачастую находятся гораздо менее общие объяснения, которые могут сформировать не правильное представление.
            • 0
              В этом докладе рассказывается, какие они построили модули первого порядка, которых нет в Стандартном МЛ. Зато почему-то промолчали, что они есть в ОКамле.
              И заодно рассказывают, почему классы типов — это так плохо на Хаскеле, зато так классно в Скале.

              Что касается path-dependent types — в докладе об этом не сказано.
              Тем не менее, path-dependent types — это разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип:
              res: java.lang.Class[_ <: VariableName.InnerClassName] = class OuterClassName$InnerClassName
              
              • +1
                Модули первого порядка и path-dependent types в скале — это одно и то же, на сколько я это понимаю.

                Такой модуль — это самый обычный объект.

                И либо я не понял, что вы описали, либо это не path-dependent types.

                Если у вас есть 2 перменные:

                val a: OuterClassName = ???
                val b: OuterClassName = ???

                To a.InnerClassName и b.InnerClassName — разные типы.

                PS: не заметил там негатива в сторону хаскеля.
                • 0
                  PS Чего только стоит "#haskellfail" ))
                  • 0
                    Так это про модули, а не про тайпклассы. Я неоднократно встречал жалобы апологетов хаскеля на отсутствие модулей.

                    Хаскель — давно и надежно lingua franca ФП. Всерьез наезжать на него ни кто не будет, но идеала не существует.
                • 0
                  Модули первого порядка и path-dependent types в скале — это одно и то же, на сколько я это понимаю.

                  Если это так, то это уже есть в статье: модули первого порядка (не знаю как в Скале, а в ОКамле они к тому же могут быть рекурсивными) — достойная функциональная замена объектам.
                  По мощности они соизмеримы с объектами.

                  To a.InnerClassName и b.InnerClassName — разные типы.

                  Верно, а вместе с
                  val c: a.type = a
                  

                  a.InnerClassName и с.InnerClassName — одинаковы
                  Что означает «разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип»?

                  То, что в вышеприведённом примере `а` не отличит себя от `с`.
                  • 0
                    Если это так, то это уже есть в статье: модули первого порядка (не знаю как в Скале, а в ОКамле они к тому же могут быть рекурсивными) — достойная функциональная замена объектам.


                    Я, видимо, не достаточно разобрался с терминологией. Спасибо, что прояснили.

                    В скале это не замена, это синонимы. Объект называют модулем если его используют как модуль.

                    Вложенность, рекурсивность, наследование (включая множественное) и все остальное прилагается.

                    В данном случае это не просто такой же тип, это еще и знание компилятора о том, что это в точности тот же объект/модуль. Крайне полезно если надо объединить 2 модуля, каждый из которых зависит от третьего с использованием одного и того же «третьего».

                    Звучит запутанно, выглядит чуть проще:

                    val parser = new { val xc: c.type = c } with XPathParsers
                    


                    Пример из моего кода.

                    Я вас не сразу понял, так как для меня фраза "`а` не отличит себя от `с`" (привычка с SO? жаль на хабре не работает) бессмысленна — `a` и `c` это два имени одного объекта/модуля.
              • 0
                Что означает «разрешение ввергнутся во внутреннее пространство объекта другому объекту, поскольку он имеет такой же тип»? Если вы про область видимости private, то это наследство java и есть дополнительно private[this].
  • +1
    Внесу пару замечаний по тексту, дабы восстановить историческую справедливость:
    Интерфейсы впервые вводят Ява и Си#. И это понятно – они были лидерами в объектных языках.
    Неправда, интерфейсы в чистом виде были еще в Delphi, задолго до Java и C#. Да и полностью абстракный класс в С++ — точно такой же интерфейс.

    Дело в том, что пользователи имеют возможность построить любые перечисления. Только вот для работы с ними в Си нет ничего. Вообще ничего. Создавать перечисления можно, а работать с ними — нельзя.
    Поскольку Си был в мейнстриме, мало кто хотел добавлять этот тип данных в другие языки.
    Очень странный вывод. Опять же надо отметить, что в том же Паскале давным давно был простой инструментарий для работы с перечислениями — функции low(), high(), итерация в цикле for.

    • +1
      На счёт Паскаля — да, я об этом написал.
      На счёт Делфи — похоже да, ошибся, исправлю, спасибо
      • 0
        Добавлю ещё замечание по перечислениям — в паскале уже давно есть множества.
        set of вместе с перечислениями даёт более мощные возможности, чем побитовые операции. Пересечения, объединений, сумма, вычитание множеств. это гораздо удобнее a |b &c

        наглядно:
        type
          TEnumType = (etOne, etTwo, etThree, etFour);
          TEnums = set of TEnumType;
        var
          operations: TEnums;
        begin
          operations := [etTwo, etThree];
          if etOne in operations then Foo;                //если etOne есть во множестве
          if [etTwo, etThree] * operations = [] then Bar; //если etTwo и etThree не входят во множество (то есть если нулевое пересечение, ни одно из значений не встречается)
        end;
        

        И это весьма удобно.
        • 0
          Вижу, всё таки есть возможность разночтения. Поэтому внёс правку.

          Я даже больше скажу — в Паскале перечисления были с самого начала.

          И можно было даже интервалы делать:
          operations := [etOne .. etThree];
          
  • +2
    Простите уважаемый — а почему Generic класс ограниченный интерфейсами по типам-параметрам в Scala или C# — по вашему не предоставляет инструментария для ещё не описанных типов?.. Учитывая неявные приведение, и методы расширения (с параметрами в виде интерфейсов или базовых классов неописанных типов) — очень даже предоставляет.
    • +1
      Generic — метапрограммирование.
      Он не предоставляет новые пользовательские данные.
      Он «всего лишь» избавляет от рутины написания порождающих Generic классов
      • 0
        Если я правильно понял речь идёт о том что новый класс по определённым признакам должен быть дополнен уже существующими методами, которые были описаны когда никто и не догадывался что такой новый класс понадобиться. В шарпе — метод расширения для интерфейса прекрасно с этой задачей справляется, факт того что класс реализует интерфейс определяется динамически.

        Про метапрограммирование — функционал для неописанных типов мы получаем когда ограничиваем тип-параметром интерфейсом. Это не такой изящный метод как методы расширения. Да нужно будет создать новый объект обобщённого типа с параметром — новым пользовательским типом. Но разница в возможностях не принципиальная, а разница в количестве символов не критичная.

        public static class MyExtensionMethods
        {
            public static void MyMethod(this ITestInterface Test)
            {
        
                Console.WriteLine("Extension Method...");
            }
        }
        public interface ITestInterface
        {
            void Display(string name);
        }
        public class TestClass : ITestInterface
        {
            public void Display(string name)
            {
                Console.WriteLine("Hello" + name);
            }
        }
        public static void Main()
        {
            ITestInterface test = new TestClass();
            test.Display("Mitesh");  //Output - Hello, Mitesh
            test.MyMethod();    // Output - Extension Method...
        }
        • 0
          Собственно, что меняет этот пример, относительно простого объекта?

          Мы можем добавить новые методы, использующие интерфейс, который вшит в класс.
          Да, это более гибко, чем простой интерфейс, однако, ничего более возможностей вшитого интерфейса мы не получим от объекта.
          • 0
            простого объекта какого типа? Можно писать методы расширения и для всех объектов просто будет public static bla name(this object...) — и это не добавит удобства в работе. Добавлять методы, новые функции прям всем объектам подряд — не круто. Почему это мы не получим «ничего более» ???
            Будьте добры пример кода где ярко вот эта фича используется и то что по вашему ближайший аналог на еретическом ООП? Так будет нагляднее.
            • 0
              Условно говоря, можно посмотреть на многие объекты как на фреймворки. Их используют, а не правят. Если не походят — просто не используют.
              Прежде всего это касается чужих и нетривиальных объектов.

              У нас один разработчик написал класс Вектор.
              Второй разработчик написал класс Механика (например, для игр).
              Мы (третий разработчик) молимся, что бы у нас вышел паттерн Адаптер, что бы Вектор подошёл к Механике.
              Ок, вышло. (эта ситуация сама по себе показательна, могло и не выйти)

              Но нам нужны не Вектора (слишком банально для заказчика), а Вектор с Хризантемкой.
              Тут уже полный тупик — в Механику уже никаких цветочков не вставишь.
              И тут приходится выкручиваться: либо отказываться от цветочков, либо изменять Механику, либо добавлять всевозможные костыли.
              • 0
                и всё таки… код в студию пожалуйста…
                • 0
                  У нас есть 2 объекта, Механика и Вектор:

                  1)Более простой вариант — объект Механика, принимающий на вход(сеттер) массив пар чисел, и на выходе(геттер) — новые пересчитанный массив пар чисел.
                  2)Усложнённый вариант Механики, возвращающий несвязанный массив пар чисел.

                  Объект Вектора с Хризантемкой:
                  3)Простой вариант — у каждого Вектора своя хризантема.
                  4)Усложнённый вариант — при этом хризантема во время движения должна крутиться в противофазно, относительно Вектора, к которому прикреплена.

                  1+3 можно исправить расширенными методами.
                  2+3, 1+4 лишь в некоторых случаях можно тоже, но так вряд ли кто будет делать. Будут уже править
                  • 0
                    несколько запутанная предметная область…
                    я хочу посмотреть на две версии кода на хаскель и тот как он эволюционировал после появления новых требований.
                    • 0
                      Хаскель не объектно-ориентированный язык.
                      В нём нет объектов вообще.

                      Покажу пример 1+3+4:

                      Функция
                      mechanic :: Movement m => m ->m
                      
                      instance Movement (Double,Double) where
                           move (x, y) = (x + dx, y+dy)
                      
                      instance Movement FlowerVector where
                           move (FlowerVector {..}) = FlowerVector {x = x + dx, y = y + dy, 
                                                   flower_x = flower_x - dx, flower_y = flower_x - dy,
                                                   flower = flower}
                      

                      Можете добавить свои данные. Например, как зависит цвет, форма цветка в зависимости от тех или иных событий.
                      Можно вообще любые данные обрабатывать, только надо написать экземпляр instance Movement MyData
                      • 0
                        итак для того что бы обрабатывать «любые данные» нужно определить это поведение для этих новых инстанс типов данных, так?
                        Смысл моего вопроса — в том как эволюционирует код на хаскель.
                        Вероятно разница в том как этот код можно использовать дальше — я бы хотел увидеть пример использования этого кода и то что по вашему мимикрирует этот подход в более традиционном ООП — а лучше всего с использованием Extenstion methods.
                        • 0
                          Есть такая замечательная штука как Generic Extension Methods — можно расширить не знаю какие типы, но все которые реализуют определённый интерфейс. Мне это кажется наиболее близкой идеей, если я вас правильно понял.
                          • 0
                            Да, это наиболее близко.
                            Разница заключается в доступе изменения данных.
                            Интерфейсы дают доступ лишь к прописанным(в интерфейсе) полям, а инстансы — ко всем полям.

                            Пы.Сы. Кстати, в Хаскеле есть свои Генерики. Даже не один, а 3 направления
                            — Generic
                            — Template
                            — Data
                          • 0
                            wiki/Concepts_(C++)

                            Вот почти та же самая идея для Cи++, что и классы типов для Хаскеля
                        • 0
                          Код на Хаскеле несколько отличается от ООП-шного.
                          И развитие кода тоже имеет несоответствия.
                          В основном код излишне общ.
                          И в большинстве случаев достаточно просто использовать код.
                          При этом нет нужны во всяких фабриках и фабриках фабрик.
                          Из-за обобщённости нет необходимости изменять поведение, достаточно дописать функционал.
                          Если и нужно менять, то в основном, добавляется очень малое количество кода — до нескольких строчек.
                          Хотя конечно же, если код не оптимальный — изменения могут быть существенными.

                          Необходимо отметить, на Хаскеле данными могут быть не только данными, а и действиями, или совокупностью.
                          Скрытый текст
                          Например, монады — это данные, с которыми можно работать в императивном стиле, стрелки — данные, с которыми можно работать как блок-схемами, линзы — данные, с которыми можно работать как с объектами.

                          Поэтому на Хаскеле не надо отдельно заниматься данными, отдельно действиями, отдельно архитектурой.

                          Если интересна именно эволюция кода, советую почитать интересную статью автора библиотеки, где он описывает, что он добавил в неё.
                          pipes-3.3.0:…
                          отрывок
                          pipes-2.4 впервые идентифицировал существование трёх дополнительных категорий, два из которых я назвал «request» и «respond». Эти категории очень полезны, особенное, если можно использовать ListT с обоими. Но я открыл, что нельзя использовать (/>/) и (\>\) композиции операторов для некоторых прокси-траснформеров, особенно
                          MaybeP
                          EitherP
                          StateP
                          WriterP
                          Это очень разочаровывало и казалось действительно странным, что прокси-трансформер может поднять одинаковость этих категорий (т.е. request и respond), но не всегда может поднять соответственную им композицию операторов.
                          Я принял временное решение, отделить (/>/) и (\>\) в отдельный ListT класс типов.

                          (работая с pipes-directory и ExceptionP обнаружилось), ExceptionP — это всего лишь синоним EitherP, а EitherP не имплементировал ListT класс, а это означало, что я не мог использовать монаду ProduceT. Поэтому я пересмотрел EitherP и открыл, что есть закон, по которому можно сделать инстанс EitherP для ListT, тот, который я искал ранее. Более того, я мог использовать то же самое для применения MaybeP к ListT.
                          А это означало, что только 2 прокси-трансформера остались без импементирования ListT:
                          StateP
                          WriterP
                          Кроме того, WriterP внутренне определялся под полом через StateP, означая, что если я смогу найти решение по StateP, я смогу объединить ListT назад к Proxy классу.
                          Попутно, работая с pipes-parse, я открыл несколько неверных частных случаев для StateP, которые давали неверное поведение. Также оказалось, что и WriterP тоже давал неверное поведение в широких вариантах случаев.
                          Это означало, что я имплементировал оба этих прокси-трансформера неверно, поскольку оба давали много неверных результатов для многих случаев. И оба этих трансформера не могли быть подключены к ListT.
                          Это наблюдение позволило мне открыть правильное решение: разрешить StateP и WriterP делится эффектами глобально в линии (pipeline), вместо локально.
                          Это решение фиксировало обе проблемы:
                          — оба могли имплементировать List и подчинялись законам ListT
                          — Оба давали правильное поведение во всех частных случаях.
                          Вследствие этого, я снова объединил ListT к классу Proxy и объединил request и respond к их соответствующим композитным операторам.
                          Ну, и сейчас все прокси-трансформеры поднимают все четыре категории верно.
  • +1
    Предложения по применению расширенной (девятиуровневой) модели взаимодействия открытых систем

    functional — Функциональное — для разделов, использующих функциональное программирование.
    aspect — Аспектное — для интерфесов и аспектов. Для методов могут указываться необходимые свойства и дополнения, используемые перед методом (before), после метода (after) и при выполнении каждого оператора (invariant)
    predicate — Логическое — соответствует Булевым переменным, доказательству теорем, Аристотелевкой логике, работе с запросами SQL, LINQ или простейшим операциям Prolog.
    controller — Управляющее — соответствует контроллеру (Controller) модели MVC
    publish — Изменяемое — соответствует представлению (View) для модели MVC
    public — Соединяющее — соответствует модели базы данных (Model) модели MVC
    protected — Защищенное — внутренние элементы класса
    private — Внутреннее — скрытые элементы класса
    local — Блоковые — переменные методов и блоков

    При этом должны выделяться классы, описывающие функционал без физической реализации, реализующие структуры и использование для конкретных применений

    подробнее habrahabr.ru/post/176249/
    • 0
      А где применение-то?
      • 0
        У меня — так в процессе вызревания идеи :-)
        Но эти фрагменты «вылазят» в существующих языках и технологиях.

        На мой взгляд, для полного задействования необходимо иметь описания предметных областей на уровнях абстрактных классов, распределенных по своим (предметной области) пространствам имен.
  • +4
    Я считаю, что после проверки орфографии это вполне может пойти в золотой фонд Хабра.
  • +1
    Расскажите, пожалуйста, для тех, кто не в курсе, чем хаскелевские классы типов отличаются от type class в scala (оно же context bounds).
    • +1
      Хороший вопрос, как отличаются.
      Одно можно сказать точно, это попытка ввести хаскелевские классы типов в Скала.
    • +1
      В скале нет классвов типов, они эмулируются с помощью неявных параметров. Context bounds — синтаксический сахар для этого, причём не всегда применимый.

      С точки зрения решаемых задач — ничем не отличаются. С точки зрения реализации — по разному выражаются классы, типы и инстансы классов для типов. В хаскеле это делается отдельными языковыми конструкциями, выбор инстанса — статический. В скале всё описывается классами (в ОО-значении этого слова), выбор инстанса можно осуществлять динамически.

      Если кратко в хаскеле можно описывать сложные классы, но нельзя манипулировать несколькими инстансами. В скале _очень_ сложно описывать сложные классы, но инстансы являются first-class values, со всеми вытекающими.
      • 0
        Спасибо. Про скалу я в курсе, было интересно про Хаскель.

        То есть я правильно понимаю, что в хаскеле для одного типа не получится в части программы использовать одну реализацию тайпкласса, а в части — другую?

        Кстати, что подразумевается под _очень_ сложно ? Тайпкласс — трейт. Реализация — создание экземпляра.
        • 0
          В разных модулях можно, в рамках одного — один статический импорт. Ну и в принципе instance не first class value, хотя подозреваю что есть расширения на эту тему.

          Моё любимое: github.com/scalaz/scalaz/blob/v7.0.0/core/src/main/scala/scalaz/std/Either.scala#L74 Я это пробовал читать частями, по 10ку лексем за вечер, на 3й забил (:
          • 0
            Нечитаемость — общее свойство scalaz. Даже shapeless кажется на таком фоне простым и понятным.

            Но этот пример я бы не назвал выдающимся. Реализуется сразу 3 тайпкласса, каждый метод вполне читабелен, если знать используемые понятия.

            Единственно что громоздко — определение типа `l``. Остальное сложно не из-за синтаксиса, а из-за сложности реализуемых абстракций на мой взгляд.

            Scalaz — это постоянный эксперимент. Не думаю, что большую часть оттуда у кого-то возникнет желание использовать в реальных проектах.
            • 0
              К методам нет претензий, проблема в сигнатуре самого объекта-инстанса. И в том что говорит компилятор когда инстанс не находится. Ну и отдельные функции доставляют: def liftM[G[_[_], _]](implicit G: MonadTrans[G]): G[F, A] = G.liftM(self)
        • +1
          То есть я правильно понимаю, что в хаскеле для одного типа не получится в части программы использовать одну реализацию тайпкласса, а в части — другую?

          Можно, если эти реализации закрыты для экспорта/импорта друг к другу.
            module TestZero (Test(..)) where  -- TestZero.hs
               data Test = Test1 | Test2
          
            module TestOne(testf1)  where     -- TestOne.hs
              import TestZero
              instance Eq Test where
                 _ == _ = True
              testf1:: Test -> Test -> Bool
              testf1 = (==)
          
            module TestTwo(testf2)  where     -- TestTwo.hs
              import TestZero
              instance Eq Test where
                Test1 == Test2 = True
                Test2 == Test1 = True
                _     == _     = False
             testf2:: Test -> Test -> Bool
             testf2 = (==)
          
            module TestThree  where          -- TestThree.hs
              import TestZero
              import TestOne
              import TestTwo
              test  =  (testf1 Test1 Test2) == (testf2 Test1 Test2)
          
  • 0
    > АТД вместе с классами типов выглядят грубо, но способны на многое
    Да я вас умоляю!.. Божественно они выглядят. Система типов в Хаскеле изящна с математической точки зрения и удобна с практической.
  • 0
    тема безусловно интересная и похоже, что автор обладает поистине энциклопедическими знаниями в этой области. однако читать такой поток сознания с картинками совсем не просто. :)
  • 0
    Указатели впервые появились не в С, а (из распространенных языков) в PL/1. В языке Алгол-68 их рафинировали, а уже оттуда они попали в С.
    • 0
      Указатели впервые появились не в С, а (из распространенных языков) в PL/1.

      Гм, просмотрел я «PL/I: Language Specifications. 1965» — не нашёл там такого.
      Появились впервые в ассемблере.
      В Алголе-68 есть что-то, а Паскаль куда более близкий к современному понимаю указателей, он не до конца их добавил.
      И уже Си их добавил в самодостаточном варианте.
      • 0
        Тип хранилища CONTROLLED и BASED, операторы ALLOCATE и FREE, тип переменной POINTER и OFFSET. Практически ничем от С не отличаются. Возможно, это были расширения от IBM, но весьма распространенные.

        В Алголе 68 было все, примерно как в Паскале, но при очень вычурном синтаксисе. Отдельный модификатор типа ref (имя), разыменование и генератор переменной в стеке/куче.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.