LXF146:tut8
|
|
|
Erlang: Сущности языка. Кортежи
- Андрей Ушаков продолжает рассказ о программировании на Erlang.
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Мы продолжаем обзор базовых сущностей языка Erlang. В предыдущем номере (LXF145) мы рассмотрели, что представляют собой функции. В этой статье мы поговорим о другой, не менее важной сущности – о кортежах (и основанных на них записях). Кортежи очень важны по той причине, что это основной строительный блок для структур данных (наряду со списками) во всех функциональных языках программирования, в том числе и в Erlang. Поэтому практически в любой программе вы столкнетесь с кортежами либо с основанными на них записями (то же самое можно сказать и про другие базовые сущности: функции и списки). Давайте рассмотрим их более подробно.
Кортеж [Tuple] – это контейнер для разнородных данных, т. е. для данных, обработка которых будет происходить по-разному. При этом не важно, имеют ли элементы кортежа один и тот же или разные типы. В этом кортеж принципиально отличается от списка: предполагается, что в списке все элементы будут обработаны одним способом, даже если у них разный тип (разговор о списках будет в следующей части учебника). Кортеж напоминает структуры языка C, за одним исключением: если доступ к данным в структуре осуществляется по имени, то доступ к данным в кортеже осуществляется по индексу. С другой стороны, упомянутый выше список напоминает вектор из стандартной библиотеки C++ (за одним исключением: в Erlang список неизменяемый, в отличие от вектора).
Как объявить переменную типа кортеж? Как мы помним (LXF143), Erlang – это язык со строгой динамической типизацией. Это означает, что тип переменной определяется в момент ее инициализации. Инициализатор для кортежа выглядит очень просто: внутри фигурных скобок “{” и “}” мы через запятую перечисляем значения элементов кортежа. Например, мы объявляем переменную типа кортеж с ее инициализацией следующим образом:
TupleVar = {1, abc}.
Размером кортежа называют количество элементов кортежа; в приведенном выше примере размер кортежа равен 2. Минимальный возможный размер кортежа – 0 (и это будет пустой кортеж). Максимальный возможный размер кортежа обусловлен только системными ограничениями и составляет 67 108 863 элементов.
После объявления кортежа, мы, скорее всего, захотим иметь доступ к отдельным элементам кортежа. Есть несколько способов, как это сделать, и один из них – использовать операцию соответствия шаблону [pattern matching]. Для этого мы создаем шаблон (очень похожий на инициализатор), который выглядит как список неинициализированных переменных либо конкретных значений, перечисленных через запятую, расположенный внутри фигурных скобок “{” и “}”. После этого шаблон сопоставляется кортежу (при помощи оператора соответствия шаблону “=”, причем шаблон стоит слева, а кортеж справа; см. LXF143). Кортеж будет соответствовать шаблону при выполнении следующих условий:
- Размеры кортежа и шаблона совпадают.
- Конкретные значения в шаблоне и значения элементов кортежа, стоящих на одной позиции, совпадают.
При этом неинициализированные переменные будут содержать значения элементов кортежа, стоящих в той же позиции. Вместо неинициализированной переменной может стоять специальный символ “_”, означающий, что значение элемента в данной позиции нас не интересует. Давайте рассмотрим несколько примеров, для пояснения этой далеко не самой простой операции. В следующих двух примерах операция соответствия шаблону выполняется успешно:
{A1, _ A2} = {1, abc, 2}. {B1, abc, B2, xyz} = {1, abc, 2, xyz}.
В следующих двух примерах операция соответствия не выполняется, и все заканчивается ошибкой времени выполнения:
{C1} = {1, abc, 2}. {D1, xyz, D2, abc} = {1, abc, 2, xyz}.
- Метамодернизм в позднем творчестве В.Г. Сорокина
- ЛитРПГ - последняя отрыжка постмодерна
- "Ричард III и семиотика"
- 3D-визуализация обложки Ridero создаем обложку книги при работе над самиздатом.
- Архитектура метамодерна - говоря о современном искусстве, невозможно не поговорить об архитектуре. В данной статье будет отмечено несколько интересных принципов, характерных для построек "новой волны", столь притягательных и скандальных.
- Литература
- Метамодерн
- Рокер-Прометей против изначального зла в «Песне про советскую милицию» Вени Дркина, Автор: Нина Ищенко, к.ф.н, член Союза Писателей ЛНР - перепубликация из журнала "Топос".
- Как избавиться от комаров? Лучшие типы ловушек.
- Что делать если роблокс вылетает на windows
- Что делать, если ребенок смотрит порно?
- Почему собака прыгает на людей при встрече?
- Какое масло лить в Задний дифференциал (мост) Visco diff 38434AA050
- О чем может рассказать хвост вашей кошки?
- Верветки
- Отчетность бюджетных учреждений при закупках по Закону № 223-ФЗ
- Срок исковой давности как правильно рассчитать
- Дмитрий Патрушев минсельхоз будет ли преемником Путина
- Кто такой Владислав Поздняков? Что такое "Мужское Государство" и почему его признали экстремистским в России?
- Как правильно выбрать машинное масло в Димитровграде?
- Как стать богатым и знаменитым в России?
- Почему фильм "Пипец" (Kick-Ass) стал популярен по всему миру?
- Как стать мудрецом?
- Как правильно установить FreeBSD
- Как стать таким как Путин?
- Где лучше жить - в Димитровграде или в Ульяновске?
- Почему город Димитровград так называется?
- Что такое метамодерн?
- ВАЖНО! Временное ограничение движения автотранспортных средств в Димитровграде
- Тарифы на электроэнергию для майнеров предложено повысить
Для работы с кортежами, помимо операции соответствия шаблону, существует набор функций (API). Этот набор функций достаточно невелик (в отличие от набора функций для работы со списками); все функции определены в модуле erlang, и часть из них является встроенными – BIF [built-in functions]. Давайте рассмотрим их более подробно.
Выше уже упоминалось, что количество элементов в кортеже – это его размер. Единственный способ узнать размер кортежа – это воспользоваться одной из BIF: size/1 или tuple_size/1. Вся разница между этими методами в том, что tuple_size/1 работает только с кортежами, а size/1 – с кортежами и двоичными данными (рассказ про которые будет в одной из следующих частей). Для доступа к отдельным элементам кортежа, помимо операции соответствия шаблону, существует следующая BIF: element(Index, Tuple) (или element/2), где Index – индекс элемента, Tuple – кортеж. Данная BIF позволяет осуществить доступ к конкретному элементу кортежа (по его индексу), не используя шаблон, что особенно удобно в тех случаях, когда размер кортежа большой или может меняться.
Все значения в языке Erlang (согласно концепции функциональных языков) неизменяемы; применительно к кортежам это означает, что если мы хотим изменить какие-либо элементы кортежа, то должны создать кортеж заново. Это не очень удобно, если мы хотим изменить всего один (или несколько, в случае, когда размер кортежа достаточно большой) элемент. В этом случае удобно применять следующую BIF: setelement(Index, OldTuple, NewValue) (или setelement/3), где Index – индекс изменяемого элемента, OldTuple – исходный кортеж, NewValue – новое значение элемента. Данная BIF возвращает новый кортеж (копию кортежа OldTuple), у которого элемент с индексом Index установлен в значение NewValue, а значения остальных элементов совпадают с соответствующими элементами кортежа OldTuple.
Создание кортежа при помощи инициализатора не всегда удобно, особенно когда размер кортежа большой и мы хотим для некоторых (или для всех) элементов задать значения по умолчанию. В этом случае нам приходят на помощь следующие функции (следует особо заметить, что эти функции – не BIF): make_tuple(Size, InitialValue) (или make_tuple/2) и make_tuple(Size, Default, InitList) (или make_tuple/3). Функция make_tuple/2 создает кортеж размером Size, все элементы которого имеют значение InitialValue. Функция make_tuple/3 более хитрая: она создает кортеж размером Size и заполняет его элементы в соответствии со списком инициализации InitList. Список InitList является списком пар позиция–значение (каждая пара – кортеж). При вызове функции make_tuple/3 те элементы кортежа, позиции которых есть в этом списке, устанавливаются в соответствующие значения; а элементы, позиций которых нет в списке InitList, принимают значение Default. Давайте приведем пример. Так, вызов
erlang:make_tuple(2, abc).
создает кортеж {abc, abc}, а вызов
erlang:make_tuple(3, abc, [{1, xyz}, {3, uvw}]).
создает кортеж {xyz, abc, uvw}. У нас осталась еще пара BIF: tuple_to_list/1 и is_tuple/1. Первая (tuple_to_list/1) позволяет преобразовать кортеж в список, вторая (is_tuple/1) – проверить, является ли некоторое значение кортежем. Ну и напоследок про функции: size/1, tuple_size/1, element/2 и is_tuple/1 можно применять в охранных выражениях (см. LXF145).
Главный недостаток кортежей в том, что доступ к их элементам осуществляется по индексу. Очень легко забыть, какой элемент в какой позиции находится, и перепутать несколько элементов. Это приведет к ошибке в логике работы с данными, понять причины которой очень сложно. Если кортеж используется только внутри одного модуля, то правильность его использования можно отследить; но если кортеж используется как входной параметр или возвращаемое значение экспортируемой функции, то отследить правильность его использования становится невозможно. И остается только одно: задокументировать структуру кортежа и молиться суровым северным богам, чтобы эту документацию все-таки прочитали и использовали кортеж в соответствии с ней. Для решения этих проблем в языке Erlang были введены записи – контейнеры для разнородных данных, доступ к элементам которых осуществляется по имени.
Перед использованием записи следует ее определить. Определение записи выглядит следующим образом:
-record(Name, {Field1 [= Value1], ... FieldN [= ValueN]}).
Здесь Name, Field1, FieldN – это имена записи и полей соответственно (имена являются атомами – см. LXF143). Каждое поле может иметь значение по умолчанию (значения Value1, ValueN); если значение по умолчанию не задано, таковым становится атом undefined. Для совместного использования записи в нескольких модулях ее определение удобно вынести во внешний подключаемый файл (файл с расширением .hrl и подключаемый директивой -include). Возникает вполне логичный вопрос: как внутри устроены записи, если мы говорим про них в рамках статьи о кортежах? Ответ достаточно очевиден: записи являются лишь «синтаксическим сахаром» компилятора Erlang и являются на самом деле кортежами вида {Name, Field1Value, … FieldNValue}. Следует сказать, что, несмотря на наличие специального синтаксиса, с записями можно работать как с обычными кортежами.
Следующий шаг – это создание записи. Запись создается точно так же, как и кортеж: при помощи инициализатора. Инициализатор для записи имеет следующий вид:
#RecordName{Field1=Expr1, ..., FieldK=ExprK}.
где RecordName, Field1, FieldK – имена записей и полей. Поля в инициализаторе можно задавать в любом порядке, и любое поле в инициализаторе можно пропустить: тогда оно получит значение по умолчанию. Если мы хотим, чтобы все не упомянутые в инициализаторе поля имели одно и то же конкретное значение (DefaultExpr), то это можно сделать следующим образом:
#RecordName{Field1=Expr1, ..., FieldK=ExprK, _=DefaultExpr}.
Возникает вполне логичный вопрос: как получить доступ к конкретному полю в записи? Для этого есть немного неочевидный синтаксис (не очевидный с точки зрения таких языков, как C):
RecordExpr#RecordName.Field.
Что интересно, простое выражение #RecordName.Field дает позицию поля в записи (точнее, позицию поля в кортеже, представляющем запись).
Следующая важная операция, которая может нам потребоваться – это изменение существующего экземпляра записи. Мы помним, что под капотом записи – это кортежи, поэтому изменение означает создание копии записи (кортежа), в которой изменены одно или несколько полей по сравнению с исходной записью. Операция изменения выглядит как инициализатор, примененный к экземпляру записи, в котором задаются только изменяемые поля:
RecordExpr#RecordName{Field1=Expr1,...,FieldK=ExprK}.
И последнее развлечение с синтаксисом записей: операция соответствия шаблону. Здесь (так же, как и в случае с кортежами, что неудивительно), мы используем шаблон, который имеет тот же вид, что и инициализатор (только он стоит слева от оператора соответствия). В шаблоне (см. выше) Expr1, ExprK могут быть как конкретными значениями, так и неинициализированными переменными. Алгоритм проверки на соответствие точно такой же, как и в случае кортежа (что опять же неудивительно). Но есть одно отличие: в шаблоне для кортежа мы вынуждены перечислить все поля (того кортежа, который стоит справа в операции соответствия шаблону), а в случае записи – только те, которые интересуют нас (при этом не перечисленные поля никакой роли в операции соответствия шаблону не играют).
Чтобы не запутаться в синтаксисе операций с записями, давайте рассмотрим несколько примеров. Первый шаг, который мы должны сделать, это определить запись:
-record(demo, {left = “”, middle = null, right}).
В данном случае мы определяем запись с именем record, которая содержит три поля: поле left со значением по умолчанию “”, поле middle со значением по умолчанию null и поле right со значением по умолчанию undefined. Дальше мы создаем экземпляра записи со следующими значениями полей: left – “lvalue”, middle – значение по умолчанию (null), right – rvalue:
SimpleRecord = #demo{left = “lvalue”, right = rvalue}.
Теперь создадим еще один экземпляр записи, задав всем полям одно и то же значение none:
EmptyRecord = #demo{_ = none}.
Далее, получим доступ к полям: SimpleRecord#demo.left возвратит нам значение “lvalue”, а #demo.left вернет нам позицию поля left в кортеже, которым является запись demo, а именно 2. В следующем примере мы изменим экземпляр записи SimpleRecord (мы помним, что экземпляр SimpleRecord не меняется, а вместо этого создается новая запись, отличающаяся от SimpleRecord лишь значением поля middle):
OtherSimpleRecord = SimpleRecord#demo{middle = 333}.
Экземпляр записи OtherSimpleRecord – это результат операции изменения экземпляра записи SimpleRecord; он содержит следующие значения полей: left – “lvalue”, middle – 333, right – rvalue.
И, наконец, приведем несколько примеров операции соответствия шаблону для записей. В следующих примерах операция соответствия шаблону выполняется успешно:
#demo{left = “lvalue”} = SimpleRecord. #demo{left = “lvalue”} = OtherSimpleRecord. {demo, “lvalue”, Middle, Right} = OtherSimpleRecord.
В следующих примерах операция соответствия шаблону не выполняется, и все заканчивается ошибкой времени выполнения:
#demo{left = “lvalue”, middle = 333} = SimpleRecord. #demo{left = “rvalue”} = OtherSimpleRecord. {demo, Left, Right} = OtherSimpleRecord.
Выше мы познакомились с такими сущностями, как кортежи и основанные на них записи. После этого знакомства возникает вопрос: а как обстоят дела с инкапсуляцией данных? Ответ достаточно очевиден – никакой инкапсуляции данных нет (она не поддерживается моделью представления данных в кортежах и записях). Давайте разберемся, насколько это плохо для нас как для разработчиков. Для чего нужна инкапсуляция? Для того, чтобы скрывать детали реализации, и для поддержания целостности данных. Рассмотрим целостность данных. Кортежи и записи в языке Erlang неизменяемы. Поэтому, если мы изначально создали кортеж или запись с правильными данными, то этот кортеж или запись так и останутся с правильными данными во время своей жизни (пока не будут собраны сборщиком мусора). Защититься же от неправильного созданного кортежа или записи просто: достаточно проверить на корректность передаваемый кортеж или запись в качестве аргумента функции. Теперь перейдем к вопросу о сокрытии деталей реализации. Детали реализации в языке Erlang никакими средствами не скрыть (это особенно хорошо заметно, когда мы начинаем работать со словарями из модуля dict; но об этом в следующей части). В нашем случае это означает, что все поля кортежа либо записи доступны любому коду. К тому же, язык Erlang никакого состояния не хранит (ибо он функциональный язык программирования), поэтому мы часто вынуждены передавать кортежи, записи и данные других типов, которые содержат все необходимые детали для обработки (как, например, с вышеупомянутыми словарями). Нельзя однозначно сказать, что это плохо, т. к. в большинстве случаев барьер этого сокрытия данных не так уж сложно преодолеть (в тех языках, где он есть). Поэтому отсутствие сокрытия данных (по мнению автора) не является фатальной вещью и при развитой культуре программирования проблем не представляет.
При работе с кортежами и записями возникает еще один вопрос: а можно ли как-то связать данные и код, их обрабатывающий (как это сделано в объектно-ориентированных языках программирования), или же данные у нас сами по себе, а код – сам по себе? Сначала кажется, что связать данные и код в языке Erlang невозможно: не хватает соответствующих языковых конструкций (таких как классы). Но давайте подумаем более тщательно (а также вспомним тему про функции в LXF145). В языке Erlang функции являются полноправными типами данных, поэтому нам никто не мешает создать кортеж либо запись, одним (или несколькими) из членов которого будет функция. Таким образом, мы связываем в пределах кортежа либо записи данные и методы для их обработки.
Возникает вполне естественный вопрос: а как метод обработки данных узнает о том, с какими данными он связан? В традиционных объектно-ориентированных языках у любого метода класса существует указатель на экземпляр (например, this в C++). В нашем же случае ничего подобного нет, что естественно. В этом случае, его стоит эмулировать, передавая в качестве первого параметра экземпляр кортежа либо записи, с которым связан метод обработки (при этом мы не забываем, что переданный экземпляр мы изменить не сможем).
Давайте рассмотрим пример. Для начала определим запись, которая будет содержать данные и метод для их обработки:
-record(class, {data, method = fun(This) -> This end}).
После этого создаем экземпляр записи и вызываем метод-обработчик, который изменяет экземпляр записи; таким образом мы и получаем новый экземпляр записи:
Object = #class{data = none, method = fun(This) -> #class{data = modified, method = This#class.method} end}. ProcessMethod = Object#class.method. NewObj = ProcessMethod(Object).
Следует заметить, что подобные конструкции напоминают скорее не обычные классы, а объекты – прототипы (например, из языка JScript).
В качестве финального аккорда, давайте рассмотрим небольшой пример и применим часть знаний на практике. Пусть у нас есть иерархические данные (например, XML), которые мы представляем в виде дерева в памяти. Наша задача – отфильтровать и обработать эти иерархические данные. Иерархические данные мы представляем в виде дерева узлов; для этого мы определяем запись следующего типа:
-record(node, {value = “”, children = [], attr = []}).
Из определения видно, что каждый узел содержит некоторое значение, список дочерних узлов и список атрибутов (предполагаем, что значение, связанное с узлом – строка, а атрибутом может быть любой объект). Следующая и самая важная часть – это сам метод для фильтрации и обработки узлов. Несмотря на свою важность, он выглядит очень просто:
process_node(Node, Filter, Map) -> Children = lists:map(fun(Child) -> process_node(Child, Filter, Map) end, lists:filter(Filter, Node#node.children)), Map(Node#node{children = Children}).
В этом методе мы сначала фильтруем и обрабатываем список дочерних узлов для текущего узла, а потом обрабатываем и сам текущий узел (фильтровать текущий узел не надо, т. к. он уже отфильтрован, когда был в списке дочерних узлов родительского узла). Вся работа по фильтрации списка дочерних узлов и обработке отдельного узла вынесена в аргументы метода Filter и Map, которые, очевидно, являются функциями. Следующий шаг – генерация тестовых данных, для демонстрации работы нашего метода. Понятно, что в реальной системе мы бы парсили XML-файл и преобразовывали его в нашу структуру. В нашем случае достаточно объявить метод, создающий жестко заданные тестовые данные:
test_data() -> NodeList2 = [#node{value = “most_inner”, attr = [“attr1”, “attr2”]}, #node{value = “most_inner”}], NodeList1 = [#node{value = “inner”, children = NodeList2, attr = []}, #node{value = “inner”, attr = [“attr3”]}], #node{value = “doc”, children = NodeList1}.
Теперь нам нужен метод, который все собирает вместе и запускает обработку (и который мы экспортируем из модуля). В методе мы объявляем функции для фильтрации и обработки:
go() -> Doc = test_data(), Filter = fun(Node) -> length(Node#node.attr) == 0 end, Map = fun(Node) -> Node#node{value = Node#node.value ++ “ processed”} end, process_node(Doc, Filter, Map).
Остался финальный штрих – объявление модуля и списка экспортируемых функций:
-module(hierarchy_demo). -export([go/0]).
Сохраняем исходный код в файле с именем hierarchy_demo.erl, запускаем среду выполнения Erlang. В консоли Erlang запускаем компиляцию: командой c(hierarchy_demo)., после чего запускаем (hierarchy_demo:go()). и наблюдаем результат фильтрации и обработки.
В данной статье мы рассмотрели и обсудили, что такое кортежи и записи и как их правильно «готовить». Мы увидели, что кортежи (наряду с функциями и списками) – это фундаментальные строительные блоки для создания структур данных, и без них никуда. А в следующей статье мы рассмотрим следующую базовую сущность функциональных языков (и Erlang в том числе) – а именно, списки.