Как стать автором
Поиск
Написать публикацию
Обновить

Язык программирования J. Взгляд любителя. Часть 2. Тацитное программирование

Время на прочтение8 мин
Количество просмотров13K
Предыдущая статья цикла Язык программирования J. Взгляд любителя. Часть 1. Введение

Вопрос: Если функции изменяют данные, а операторы изменяют функции, тогда кто изменяет операторы?
Ответ: Кен Айверсон
Chirag Pathak


В J используется идея тацитного (от слова «tacit», неявный) программирования, не требующего явного упоминания аргументов определяемой функции (программы). Работа в тацитном подходе происходит, как правило, с массивами данных, а не с отдельными их элементами.
Интересно заметить, что тацитное программирование было открыто Бэкусом еще до APL и реализовано им в языке FP. Среди современных языков, поддерживающих такой подход, (кроме, естественно, J) можно назвать Форт и другие конкатенативные языки, а также Haskell (за счет point-free подхода).

1. Глаголы


Определим наш первый глагол. Для удобства, можно считать, что глагол – это функция с аргументами, заданными по умолчанию. Открываем интерпретатор J, вводим

	neg =: -


где «neg» – имя нашего глагола, «=:» – оператор присваивания, «-» – собственно тело глагола, которое состоит из вызова встроенного глагола «-». Попробуем теперь применить наш глагол к значению (т.е. вызвать его). Например, к единице:

	neg 1
_1


Как видно из примера, в результате получилось число «-1» (в J для записи отрицательного числа используется знак подчеркивания, т.к. минус занят под встроенный глагол).

Теперь определим переменную (в J это называется «существительное») «x» и присвоим ей значение -1:

	x =: _1


Мы ожидаем, что конструкция «neg x» вернет нам единицу. Это действительно так:

	neg x 
1


Этот вызов аналогичен простому

	- x 
1


Другой пример:

	x
_1
	neg
-


Т.е. значением переменной «x» является «-1», а значением глагола — его тело. Изменить способ вывода значений на экран можно с помощью глагола «9!:3».
Определим еще один глагол, который также будет простым синонимом для встроенного глагола:

	twice =: +:


Этот глагол удваивает значение своего аргумента. Тут можно проследить закономерность в именовании глаголов – так, например, если глагол «+:» удваивает свой аргумент, то глагол «-:» свой аргумент уменьшает вдвое.

	NB. Ранее мы определили x =: _1
	NB. Кстати, после «NB.» следует однострочный комментарий.
	NB. Это сокращение от латинского «Nota Bene», что в переводе «обрати внимание».
	twice x
_2
	twice (neg x)
2


И даже так:

	twice (-: 2)
2


И так

	+: (+: (- 2))
_8


2. Монады и диады


Этот раздел не имеет отношения к Haskell


Глаголы можно вызывать как с одним, так и с 2 аргументами. Глагол с одним аргументом (операндом) в терминах J называется монадой, а с двумя аргументами — диадой. В последнем случае аргументы записываются слева и справа (первый и второй аргумент соответственно) от вызываемого глагола. Вспомним нашу первую монаду:

	minus =: -
	minus 1
_1
	minus 7
_7


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

	4 minus 2
2
	12 minus 5
7


В таком случае глагол minus используется как диада и в результате подстановки вычисляется выражение «4 — 2» и «12 -5» соответственно.

Два аргумента — это максимум, который можно использовать при определении и вызове глагола.
Можно представить себе, что, если бы язык J поддерживал двумерный синтаксис (как, например, эзотерический язык Befunge), то аргументы можно было бы записывать не только слева и справа, но сверху-снизу. К счастью, в J поддерживается только линейный синтаксис.

Приведем еще один пример — определим глагол «div» как синоним встроенному глаголу «%». Причем «%» — это оператор деления, более привычный нам символ «/» используется для других задач (позже мы покажем для чего).

	div =: %
	1 div 2
0.5
	4 div 3
1.3333


В данном примере глагол вызван как диада и делит 1 на 2 и 4 на 3 соответственно. Причем обратите внимание, что хотя операнды — целые числа, результат деления — число с плавающей точкой.

Будучи вызванным с одним операндом глагол «%» по умолчанию делит единицу на этот операнд.

	div 2  
0.5
	div 4
0.25


Приведенные выше выражение, можно было бы записать и как «1 % 2» и «1 % 4» соответственно.

В приведенном выше примере используется достаточно важная особенность языка — монадный и диадный вызовы одного и того же глагола могут совершать достаточно разные по смыслу действия. Так, например, глагол «*:» в монадном случае вычисляет квадрат своего аргумента, а в диадном — выполняет логическую операцию «Не-И»:

	*: 3
9
	*: 4
16
	0 *: 0
1
	1 *: 1
0
	0 *: 1
1
	1 *: 0
1


Для того, чтобы глагол явно использовал свой левый или правый аргумент предназначены глаголы «[» и «]». Первый из них возвращает свой левый аргумент, второй — правый. Например:

	11 [ 22
11
	11 ] 22
22
	] 33
33
	[ 44 NB. как видим, если аргумент один, то [ возвращает правый аргумент.
44


Дополним наш багаж константными глаголами «0:», «1:», «2:», …, «9:». Константными их называют потому, что с какими бы аргументами мы бы их не вызывали, они всегда вернут одно и тоже значение. «0:» вернет нуль, «1:» — единицу и т.д. Например:

	7 3: 9
3
	3: 7
3


3. Союзы


Предположим далее, что для одного и того же глагола надо определить различное поведение для монадного (с одним аргументом) и для диадного (с двумя аргументами) вызовов. Это вполне обоснованное желание, т.к. J — язык динамический, и наш код не застрахован от некорректного использования. Для того, чтобы разделить монадное и диадное поведение глагола, есть специальный союз «:».

Если мы говорим, что глагол – это функция с одним или двумя аргументами по умолчанию, а значением аргументов может быть как переменная, так и непосредственное значение, то, в таком случае, «союз» — это такая функция, которая в качестве двух (и только двух) аргументов принимает глаголы. Союзы, также как и глаголы, пользователь может определять самостоятельно.

Тут можно было бы провести аналогию с частями речи (в J, как мы увидим позже, есть еще и наречия), но автору кажется, что это приведет к лишней путанице.

Вернемся к нашим монадам. По левую сторону от союза «:» записывается монадный вызов глагола, по правую – диадный. Пример:

	v =:  1:  :  2:
	11 v 22
2
	v 11
1


Если вызвать глагол «v» как диаду (в примере — с аргументами «11» и «22»), то произойдет обращение к выражению «2:». Если как монаду (в примере — с аргументом «11»), то к «1:». А это константные глаголы, которые всегда возвращают свое значение: «2» — в первом случае и «1» — во втором.

До сих пор мы оперировали только численными значениями. На сей раз воспользуемся строками, которые записываются в одинарных кавычках (двойные кавычки в J – это союз). Переопределим описанный ранее глагол «v»:

	v =: 'monadic call' : 'dyadic call'
	v 11
|domain error
	11 v 22
|domain error


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

Работать со строками в J можно также, как и с массивами (с которыми мы познакомимся несколько позже). Например, глагол «#» в монадном вызове возвращает длину массива:

	# 23
1
	# '12ab'
4
	# '012345'
6
	# ''
0


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

	neg =: -
	1 neg 2
_1


Т.е. после подстановки наш глагол сработал как вычитание «1 – 2». Целесообразно было бы, ограничить область применения глагола только монадным вызовом. Как уже говорилось ранее, динамическая природа J принуждает нас проводить подобные проверки во время исполнения программы. Как, пожалуй, и все остальное в J, такая проверка записывается максимально коротко:

	neg =: - : [:


Мы воспользовались новым для нас глаголом «[:». В книге «J for C programmers» этот глагол назван «глаголом-самоубийцей» за то, что будучи вызванным, приводит к аварийному останову программы:

	neg 1
_1
	1 neg 2 NB. Здесь срабатывает правая часть определения - глагол-самоубийца
|domain error


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

Союз «::» выполняет выражение слева от себя, а если оно завершилось с ошибкой, то исполняет выражение справа. Пример:

	v =: [: :: 3:
	1 v 2
3
	v 7
3


Этот глагол всегда будет возвращать 3, потому что, как мы сказали, сначала выполнится выражение слева. А слева мы поставили глагол-самоубийцу. Следовательно, союз «::» при каждом вызове глагола «v» отлавливает самоубийцу «[:» и передает все аргументы глаголу, который мы записали справа, т.е. глаголу-константе «3:».

3.1. Союзы для последовательных вычислений



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

	([: +: +:) 10 NB. эквивалентно (+: (+: 10))
40
	(+: @ +:) 10  NB. эквивалентно (+: (+: 10))
40


Следующий союз в нашу коллекцию «&.», принцип работы которого следующий.

	u&.v y     NB. эквивалентно v_инверсия u v y
	x u&.v y   NB. эквивалентно v_инверсия (v x) u (v y).
	+:&.*:  10
14.1421
	%: @: +: (*: 10) NB. аналогично примеру выше
14.1421
	%: (+: (*: 10))  NB. или так
14.1421  


где «v_инверсия» — значит глагол, обратный «v». В выражении «+:&.*: » глагол, от которого берется инверсия — это «*:» (т.к. он стоит справа от союза «&.»). А глагол, обратный «*:» — это «%:». Т.е. операция, обратная возведению в квадрат — это взятие квадратного корня.

Другой полезный для нас союз «&», который делает из диадного вызова монадный, присоединяя к глаголу существительное (операнд-значение). С определенным допущением можно сказать, что этот союз осуществляет каррирование. Например:
	inc =: 1&+   NB. инкремент
	inc 41
42
	inc _3
_2
	div3 =: %&3
	div3 9
3
	div3 12
4


Определим инкремент только для монадного случая:

	inc =: 1&+ : [:
	inc 1
 2
	1 inc 2
|domain error: inc
|   1     inc 2   


4. Наречия



Ранее мы уже упомянули о наречиях. В терминах языка J наречие – это выражение, которое принимает в качестве аргумента глагол и формирует их него новый глагол с другим поведением.

Приведем наречие «~», которое меняет местами аргументы в своем диадном вызове («x u~ y» подставляется на «y u x») и заменяет монадный вызов глагола — диадным, копируя операнд («u~ y»подставляется на «y u y»).

	% 2 NB. Как мы помним, это аналогично вызову «1 % 2»
0.5
	%~ 2 NB. аналогично «2 % 2»
1
	2 % 3
0.666667
	2 %~ 3 NB. аналогично «3 % 2»
1.5


Одним из самых часто используемых наречий является наречие / («между»), рассмотрение которого мы отложим до раздела о массивах.

5. Хуки и форки



В J весьма своеобразный способ композиции функций — если указаны 2 подряд идущих глагола, то это не означает, что они будут применяться последовательно. У них будет свой специальный порядок вызова отдельно для диадного и для монадного вызовов. Такая композиция для 2 глаголов называется хуком(крюком). Определим глагол v, который является хук-вызовом глаголов f и g:

	v =: f  g


Если вызвать v как монаду на некотором существительном y:

	v y


то этот вызов будет эквивалентен:

	y f (g y)


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

Если же вызвать v как диаду на некоторых существительных x и y:

	x v y


то этот вызов будет эквивалентен:

	x f (g y)


Вспомним глагол решетку «#», который в монадном вызове возвращает длину строки/массива, а в диадном копирует правый операнд столько раз, сколько указано в левом операнде. Определим диаду:

	v =: # #


Разложим диадный вызов глагола v по приведенной выше схеме:

	х # (# y)


Как видно, v копирует число, равное длине операнда y, х раз. Например:

	2 v '1234'
4 4


Можно было бы предположить, что 3 подряд идущих глагола являются хуком от хука. Однако это не так – вместо этого реализуется форк-композиция (т.н. вилка). Так, монадный вызов форка

	(f g h) y


равнозначен следующему вызову:

	(f y) g (h y)


Примером такого выражения можно привести известный уже нам глагол, высчитывающий среднее от массива чисел:

	mean =: +/%#
	mean 0 1 2 3 4 5
2.5


Такое выражение равнозначно следующему:

	((+/ @ [ )%(# @ ])) 0 1 2 3 4 5
2.5


Рассмотрим еще один пример форка:

	v =: # # #
	v '12345'
5 5 5 5 5


Как видно, это выражение копирует число, равное длине операнда, столько раз, какая длина операнда.

Предыдущей глагол можно было бы переписать и без использования форков:

	((#@]) # (#@])) '12345'
5 5 5 5 5


Определим на диадном вызове форк на существительных x и y:

	x (f g h) y


который будет равнозначен следующему выражению:

	(x f y) g (x h y)


Проиллюстрируем диадный форк с помощью нашего старого знакомого – глагола mean:

	2 mean 3
1.66667 1.66667


Раскроем данное выражение:

	(2 +/ 3) % (2 # 3)


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

	5 % 3 3
1.66667 1.66667


Особенно важно понимать, что в J если указаны 4 глагола, то это выражение будет хуком от форка, если 5 – то форком от форка, 6 — хуком от форка от форка и т.д. Т.е. выражения «(# # # # # #)» и «(# (# # (# # #)))» равнозначны.

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

	(# # #) 3
1


Но

	(# (# #)) 3
1 1 1 1 1 1 1 1 1
	


Последнее выражение равнозначно следующим:

	3 # (3 # (# 3)
	3 # 1 1 1 NB. Повторить 3 раза правый операнд.


Покажем более сложный пример с композицией функций

	(* + - %) 3
8


Это выражение равнозначно следующему:

	3 * ((+ 3) -  (% 3))


Комбинация из 5 глаголов может выглядеть так:

	(>: * + - %) 3
10.6667


Последнее выражение равнозначно следующим:

	(>: 3) * ((+ 3) -  (% 3))
	4 * (3 – 0.333)


Следующая статья цикла Язык программирования J. Взгляд любителя. Часть 3. Массивы
Теги:
Хабы:
Всего голосов 33: ↑29 и ↓4+25
Комментарии10

Публикации

Ближайшие события