J для смертных. Коробки и циклы

Предыдущая статья цикла J для смертных. Массивы

Коробки

Мы уже столкнулись с тем, что существительное в J - это массив. Даже над одиночными константными значениями допустимы векторные операции. В совокупности все это составляет удобную векторную гомогенную среду программирования.

Однако, очевидно, что у массивов есть и свои ограничения. В связи с тем, что в J по умолчанию только прямоугольные массивы, то и нет возможности стандартными средствами создавать т.н. ступенчатые (jagged) массивы. Кроме того, для списков, состоящих из разнородных элементов, массивы также не подходят.

Для решения этих проблем, в J предусмотрены средства для создания и использования гетерогенных последовательностей – коробки (box). Коробка – это своего рода контейнер, который может хранить в себе произвольный элемент. Массив же из коробок - это, соответственно, своего рода массив из элементов типа (void *). Поэтому, например, первым элементом коробочной последовательности может быть одномерный массив, вторым - число, а третьим - матрица целых чисел.

Для того, чтобы создать коробку надо вызвать монадный глагол «<», чтобы извлечь («раскрыть») элементы из коробки - монадный глагол «>»:

Сама коробка рисуется в ASCII-графике. Извлечем теперь значение коробки:

Для того, чтобы добавить в коробку несколько элементов, предназначен глагол «;», который связывает элементы в последовательность коробок:

Для перечисления элементов такой коробочной последовательности можно использовать уже известные нам части речи. Например, наречие «между»:

Или композиция глаголов:

Далее воспользуемся глаголом «{» для извлечения второго элемента коробки:

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

Из предыдущего выражения можно сделать вывод, что, если мы хотим применить к каждому элементу коробки некоторый глагол, то каждый раз глагол будет принимать операндом обернутое в коробку существительное. Для того чтобы при каждом обращении «вытаскивать» элемент из массива, а после действия «засовывать» результат назад в коробку, можно воспользоваться уже известным нам союзом «&.».

Союз «&.» применяет правый глагол к операнду, и к результату применяется левый глагол. Затем к результату применяется глагол, обратный правому глаголу. Таким образом, мы фактически повторили описанный абзацем выше алгоритм. Воспользуемся этой схемой для удвоения каждого элемента коробки

В связи с тем, что выражение &.> применяется достаточно часто, то оно по умолчанию связано с символом each:

Заметим, что в скорости обработки данных коробки значительно (до 3 порядков!) отстают от численных массивов.

Определение многострочных глаголов

Секрет того, как создать элегантный язык программирования, заключается в том, чтобы он выполнял то, что вы от него ожидаете

Кеннет Айверсон

Не всегда есть возможность представить глагол в тацитной форме. Для этого в J есть специальные конструкции, которые мы будем называть императивными. В первую очередь это операции «3 :» и «4 :» (не путайте с константными глаголами «3:» и «4:»). Правый операнд по умолчанию называется «y», левый «x». Например:

Если вы чувствуете, что ваше императивное определение можно записать и в тацитной форме, но вы не знаете как, то можно воспользоваться замечательным стандартным глаголом-преобразованием:

Для записи многострочных глаголов используются схожие конструкции. Причем, как и в большинстве функциональных языках, возвращается последнее высчитанное значение и потому аналога оператора return не требуется. В многострочных глаголах можно также использовать локальные переменные - они определяются с помощью операции «=.». Символом окончания определения глагола является символ «)»

В J также есть и специальные конструкции для проверок условия (.if) , циклов(.while) и некоторые другие. За более подробной информацией рекомендуем обратиться к документации.

Рекурсия

Рекурсивная функция быстрой сортировки была показана еще во введении. Разберем другой пример. Запишем на Python простую функцию определения длины последовательности так, как будто встроенной такой функции нет.

Для того, чтобы записать в J рекурсивную функцию вычисления длины вектора, нам придется ввести дополнительный глагол-предикат для определения того, является ли существительное последовательностью с хотя бы одним элементом. Назовем этот предикат «isa» от фразы «is array». Напишем в начале пример использования этого глагола:

Будем определять, является ли операнд вектором длины более чем 1, через глагол извлечения из вектора элемента на определенной позиции. Это глагол «{»

Таким образом, если выражение «1&{» отрабатывает корректно, то считаем, что операнд является массивом с длиной большей, чем 1. Иначе возвращаем ложь (нуль). Мы также добавили в определение запрет на диадный вызов глагола.

Для того, чтобы сымитировать проверку условия воспользуемся союзом @., который вызывает из коробки глагол на той позиции, которая определяется правым операндом. Т.е.

Нам необходимо возвращать длину=1 в том случае, если правый операнд является вектором длиной=1, т.е. если предикат isa на этом существительном возвращает 0.

На месте многоточия мы должны реализовать рекурсивный вызов вычисления длины для хвоста переданной последовательности. Воспользуемся тем, что рекурсивный вызов в J реализуется глаголом «$:». Т.о. получим

> len =: 1:`(>:@$:@}.) @. isa 
> len 1 2 3 
3 
> len 17 
1  

Следующим шагом перепишем наш глагол таким образом, чтобы рекурсивный вызов стал хвостовым. Для этого будем хранить накапливаемое значение в левом операнде, и глагол, следовательно, становится диадным:

> len2 =: [`((>:@[) $: (}.@])) @. (isa@]) 
> 1 len2 i.7 
7

Определение глагола выглядит достаточно неаппетитным, и в самом деле оно такое и есть. Проиллюстрируем алгоритм глагола len2 примером на языке Python:

def len2(acc,xs): 
    if xs == []: 
        return acc 
    else: 
        return len2(acc+1, xs[1:])  

Интересно сравнить скорость написанного нами кода. Для этого воспользуемся глаголом «6!:2», который исполняет свой правый операнд столько раз, сколько указано в левом операнде и возвращает среднее время работы каждого запуска в секундах.

> time =: 6!:2 
> x =: i.1000 
> 10 time 'len x' NB. тест рекурсивного глагола 
0.00614873 
> 10 time '1 len2 x' NB. тест глагола с хвостовой рекурсией 
0.00553225 
> 10 time '# x' NB. тест встроеннго глагола для вычисления длины массива 
1.67641e_6  

Как видим, в данном случае использование встроенных средств J на 3 порядка быстрее наших самостоятельных реализаций. Кроме того, в J есть ограничение на глубину рекурсии и нет оптимизации хвостовой рекурсии.

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

Пространства имен

Подробно на использовании в J объектно-ориентированного подхода к программированию мы останавливаться не будем. Интересующимся же скажем, что поддержка ООП в J есть. Подробней читайте, например, в «Learning J».

Отметим, однако, что в J есть специальные конструкции для использования пространств имен, которые имеют схожий с ООП-инструментарием синтаксис.

Начало нового пространства имен следует начинать с выражения:

cocurrent 'name'  

Где в кавычках надо писать имя пространства имен, которое станет текущим. По умолчанию используется пространство имен 'base'. Следовательно, как только блок кода в пространстве имен закончен, надо возвращаться в область видимости по умолчанию с помощью выражения:

cocurrent 'base'  

При обращении к инкапсулированным членам пространства имен необходимо добавлять к каждому элементу суффикс name, где «name» – имя пространства имен. Рекомендуется называть пространства имен в верхнем регистре. Приведем наглядный пример:

> cocurrent 'INNER' 
> rate =: 10 
> v =: 3 : 'rate + y' 
> cocurrent 'base' 
> v_INNER_ 1 
11 
? rate_INNER_ =: 1 
> v_INNER_ 1 
2

Еще один пример

В заключении раздела приведем один хрестоматийный пример – глагол быстрой сортировки. Сортировку Хоара на J можно записать следующим образом:

sel=: 1 : 'x # [' 
quicksort=: 3 : 0 
    if. 1 >: #y do. y 
    else. 
        (quicksort y <sel e),(y =sel e),quicksort y >sel e=.y{~?#y 
    end.
)  

Разберем определение построчно:

Как мы уже показывали ранее, быструю сортировку можно записать и одной строкой:

> quicksort=: (($:@(<#[) , (=#[) , $:@(>#[)) ({~ ?@#)) ^: (1<#)  

Напомним, что «$:» означает рекурсивный вызов глагола, а выражение «@» - определяет последовательное вычисление глаголов.

Заключительная статья цикла J для смертных. Заключение

20 октября 2013