Abnormal programming
Functional Programming
24 November 2013

Язык программирования J. За что любить?

image
J – самый ненормальный и самый эффективный язык из известных мне языков. Он позволяет быстро разрабатывать, а также вызывать ненормативную лексику у людей, незнакомых с ним и смотрящих на код.

J слишком необычный. И сложный для изучения. У людей, сталкивающихся с J не хватает мотивации, чтобы его изучить. Синтаксис непривычный.
В этом посте я хотел помочь вам заглянуть дальше, что будет, если вы его изучите и чем он интересен. По своему опыту знаю, что преимущества этого языка сразу не очевидны. В посте я не собираюсь останавливаться на разборе конструкций. Только в обзоре. Предлагаю просто окунуться в примеры, попробовать ощутить мощь языка. Узнать, чем прекрасен язык, без изучения. Писать статьи, обучающие программированию на нем – дело сложное и думаю, не нужное. Он не так прост, чтобы это сделать кратко, а с обучающими материалами на официальном сайте нет никаких проблем. Главное – желание. Им и займемся.

Немного истории



До J был APL. Язык APL разработан Кеннетом Айверсоном во время преподавания в 50-х годах в Гарварде вычислительной математики. Специально для преподавания он разработал свою нотацию. Далее этот язык был перенесен на машины и стал известен как APL (скромное: A Program Language). APL работает с массивами и оптимизирован под работу с ними.

У языка APL есть небольшой недостаток. Операции обозначаются нестандартными символами. Для работы с ним нужна специальная клавиатура. Таким образом, назрела необходимость создать новый язык, более дружественный к стандартным устройствам ввода. Айверсон инициировал создание диалекта APL, который использует ASCII символы. Так Кеннет Айверсон совместно с Роджером Хуэем в начале 90-х создали язык J. В это время от команды Айверсона «откололся» Артур Уитни. Он имел разногласия по поводу того, как создавать J, и занялся созданием своего языка – K. Который также стал успешным языком.

Краткий обзор J



Скачать и установить J можно по ссылке www.jsoftware.com. Там же есть обучающие материалы и различная документация.

J – функциональный язык. Но язык не обычный, не классический функциональный. Не лиспоподобный. Единица данных — n-мерный массив, а не список.

J является интерпретатором (в этой реализации). С динамической типизацией.

Выражения строятся из глаголов, имен существительных, наречий, герундиев, союзов и прочего. Нас в данном посте будут интересовать глаголы (аналог функции/операции) и имена существительные (данные). Имена существительные – это не совсем данные, как в императивных языках. Они имутабельны. Таким образом, то, что в J называют присвоением, скорее является «именованием».

Имя существительное – это n-мерный массив. Создать имя существительное можно, например, так:

   3 5 $ 0

Мы получаем двумерный массив 3 на 5, состоящий из нулей. Можно получить массив любой мерности. Частным случаем является массив нулевой мерности — это скаляр. Одномерный массив – вектор. Таким образом, единица данных в J – n-мерный массив. Чтобы построить массив, заполненный не нулями, а нужными нам числами, после знака $ мы можем записать в строку нужные данные через пробел. Например строка:

   3 5 $ 1 2 3

Создаст и выведет массив 3 на 5 и заполнит по очереди повторяющейся последовательностью 1 2 3:

1 2 3 1 2
3 1 2 3 1
2 3 1 2 3


Для создания скаляра достаточно одного числа:

  a =: 4

Здесь я уже показал, как присваивается имя. Т.е. a у нас уже равно 4. Для создания вектора достаточно писать числа в строку через пробел:

   4 5 3 6
4 5 3 6


* Как вы уже догадались, J работает в интерактивном режиме, сразу выводит результат

Теперь о типах. В J кроме мерности массива, есть тип его элементов. Т.е. элементы всегда однотипные. Числовых типов несколько: логический (0, 1), целый, целый с любым количеством знаков, рациональный, действительный, комплексный. Также вместе с числовыми типами есть специальные числа: минус бесконечность, бесконечность, неопределенность. Динамическая типизация J замечательно подходит для математических операций – он на каждой операции сам определит, какие в нем числа. Например, если к матрице целых чисел применить квадратный корень, то можем получить либо действительные, либо комплексные числа. В зависимости от элементов первоначального массива.

Также в J, кроме числовых типов есть
1) символ (literal). В J также можно обрабатывать текст. Текст в случае J – это вектор из символов. Чтобы задать символ, нужно его заключить в одинарные кавычки. Чтобы задать вектор из символов – достаточно текст заключить в одинарные кавычки.
2) бокс (коробка, boxed) – это упакованный тип. Т.е. любую матрицу можно упаковать и получить бокс. А из боксов можно составить массив. С помощью боксов, таким образом, можно создавать деревья.
И еще несколько специальных типов, рассматривать которые не будем.

С существительными разобрались. Теперь, что представляют собой глаголы.
Глагол похож на операцию в математическом выражении в других языках. В J глаголы бывают двух видов: монадные (monad) и диадные (dyad). Это аналогично унарной и бинарной операциям. И это дает замечательную возможность: для создания глаголов не нужны имена для входных данных. Входных данных всегда одно или два – правое и левое.

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

Итак, первый стандартный пример:

  avg =: +/%#

Глагол avg – среднее арифметическое вектора. Обратите внимание на его тело – всего 4 символа. Здесь у нас всего три глагола – сложить числа (+/), взять длину вектора (#) и разделить первое на второе (%).

* Сложить числа в векторе обозначается символами + и /. Первый – сумма двух чисел, а второе – поставить операцию между всеми элементами (т.е. левый фолд). Так как слеш занят, то операция деления в J обозначается знаком %.

Создадим вектор и применим к нему наш глагол:

  a =: 5 3 7 0 2 4
  avg a
3.5

Получили среднее арифметическое вектора.

Как же происходит эта магия? В J используются такие простые правила. Во-первых, операции в J не имеют приоритета и выполняются справа налево. Если мы напишем выражение:

  2 * 3 + 4
14

Получаем 14, а не 10. Что, в общем-то, упрощает построение выражений. Если надо изменить порядок – используются скобки:

  (2 * 3) + 4
10

Не пугайтесь, это только что были выражения, вычисляющие значения напрямую, а когда мы писали avg =: +/%#, то мы задавали глагол, комбинируя глаголы.

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


Если глагол состоит из 5 глаголов, то сначала выполняются три правых, как показано выше, потом берутся два левее и в таком же порядке выполняется дальше. Например, глагол взятия максимального из двух элементов:  «>.». Следовательно, максимальный из списка: «>./»
И напишем глагол, который находит сумму максимального значения и среднего арифметического:

  d =: >./++/%#

Применяем к вектору:

  d a
10.5

Как это работает:

Обратите внимание, что когда я пишу «как это работает», то я не имею ввиду, что знаю, в какой последовательности выполняет эти глаголы J. Я только объясняю смысл, как надо понимать такую строку глаголов. Строкой глаголов мы говорим, что мы хотим. А как — решает J.

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

Иногда невозможно описать задачу так, чтобы глаголы принимали только два простых массива как аргументы (левый и правый). Иногда нужно писать глаголы, аналоги функций с бОльшим количеством аргументов. Это можно обойти боксированием. Т.е. можно задавать вектор боксов, в которых лежат, по сути, ряд разных значений, аналогично входным параметрам функции в других языках. А в глаголе их можно оттуда доставать и распаковывать. Также в глаголах реализуется рекурсия.

Описанная выше форма записи глаголов в J называется таситной (tacit).

Теперь несколько примеров, без объяснения, как они работают.

Примеры



1. INNER JOIN

Рассмотрим такой пример на SQL. У нас есть две таблицы:
CREATE TABLE Customers (ID INT, Name TEXT, Age INT, Salary INT)
CREATE TABLE Orders (OID INT,  Date  DATETIME,  CustomerID INT, Amount INT)

Как-то их заполняем. И такой запрос:
SELECT * FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.ID AND o.Amount < 2000 

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

  createTable =: [:|:,.&a:
  union =: ([:{.[),:[:([:<[:;]#~~:&a:)"1([:{:[),.([:{:]){~([:{.])i.[:{.[
  insert =: [union[:({.,:([:([:<[:(]$~[:1&,#)>)"0{:))[:|:[:>]
  rows =: {.([:<,:)"1 1[:|:[:([:<"1[:,.>)"0{:
  value =: [:(]`{.@.([:1&=#))[:,[:>[((([:<[)=[:{.])#[:{:])[:>]
  pr =: [:,[:(([:>[)(([:<([:>[),.[:>])"0 0)"0 1[:>])/[:([:<[:rows>)"0]
  join =: 1 : '((([:{.[:>{.),:[:([:<(>"0))"1[:{:[:1 2 0&|:[:>([:,u"0)#]) (pr y))'

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

  Customers =: createTable 'ID';'Name';'Age';'Salary'
  Orders
=: createTable 'OID';'Date';'CustomerID';'Amount'

  Customers
=: Customers insert (<'ID'; 1),(<'Name'; 'Ramesh'),(<'Age'; 32),(<'Salary'; 2000)
  
Customers =: Customers insert (<'ID'; 2),(<'Name'; 'Khilan'),(<'Age'; 25),(<'Salary'; 1500)
  
Customers =: Customers insert (<'ID'; 3),(<'Name'; 'kaushik'),(<'Age'; 23),(<'Salary'; 2000)
  
Customers =: Customers insert (<'ID'; 4),(<'Name'; 'Chaitali'),(<'Age'; 25),(<'Salary'; 6500)
  
Customers =: Customers insert (<'ID'; 5),(<'Name'; 'Hardik'),(<'Age'; 27),(<'Salary'; 8500)
  
Customers =: Customers insert (<'ID'; 6),(<'Name'; 'Komal'),(<'Age'; 22),(<'Salary'; 4500)
  
Customers =: Customers insert (<'ID'; 7),(<'Name'; 'Muffy'),(<'Age'; 24),(<'Salary'; 10000)

  
Orders =: Orders insert (<'OID'; 102),(<'Date'; '2009-10-08'),(<'CustomerID'; 3),(<'Amount'; 3000)
  
Orders =: Orders insert (<'OID'; 100),(<'Date'; '2009-10-08'),(<'CustomerID'; 3),(<'Amount'; 1500)
  
Orders =: Orders insert (<'OID'; 101),(<'Date'; '2009-11-20'),(<'CustomerID'; 2),(<'Amount'; 1560)
  
Orders =: Orders insert (<'OID'; 103),(<'Date'; '2008-05-20'),(<'CustomerID'; 4),(<'Amount'; 2060)


Проверим, что у нас в таблицах:

  Customers
ID Name Age Salary
1
2
3
4
5
6
7
Ramesh
Khilan
kaushik
Chaitali
Hardik
Komal
Muffy
32
25
23
25
27
22
24
2000
1500
2000
6500
8500
4500
10000

  Orders
OID Date CustomerID Amount
102
100
101
103
2009-10-08
2009-10-08
2009-11-20
2008-05-20
3
3
2
4
3000
1500
1560
2060

И запрос с предикатом:

  (([:2000&>'Amount'&value)*.'CustomerID'&value='ID'&value) join Customers; <Orders
ID Name Age Salary OID Date CustomerID Amount
2
3
Khilan
kaushik
25
23
1500
2000
101
100
2009-11-20
2009-10-08
2
3
1560
1500

Вот так всё просто. Обратите внимание, что J — язык общего применения и в нем нет средств для обработки реляционных данных. Код, который выше, до создания и наполнения таблиц — вся логика. join может принимать любой предикат.

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

2. Решатель судоку

  i =: ,((,|:)i.9 9),,./,./i.4$3
  
c =: (#=[:#~.)@-.&0
  
t =: [:(([:*/_9:c\])"1#])i&{+"1 1(>:i.9)*/[:i&=i.&0
  
r =: [:,`$:@.(0:e.,)[:;(<@t)"1
  
s =: 9 9&$@r@,


Это весь код. Создадим входную матрицу:
  ]m =: 9 9 $"."0 '200370009009200007001004002050000800008000900006000040900100500800007600400089001'
2 0 0 3 7 0 0 0 9
0 0 9 2 0 0 0 0 7
0 0 1 0 0 4 0 0 2
0 5 0 0 0 0 8 0 0
0 0 8 0 0 0 9 0 0
0 0 6 0 0 0 0 4 0
9 0 0 1 0 0 5 0 0
8 0 0 0 0 7 6 0 0
4 0 0 0 8 9 0 0 1


Применяем к ней глагол и получаем решение:
  s m
2 8 4 3 7 5 1 6 9
6 3 9 2 1 8 4 5 7
5 7 1 9 6 4 3 8 2
1 5 2 4 9 6 8 7 3
3 4 8 7 5 2 9 1 6
7 9 6 8 3 1 2 4 5
9 6 7 1 4 3 5 2 8
8 1 3 5 2 7 6 9 4
4 2 5 6 8 9 7 3 1

Более короткие решения
Показанный выше код состоит из 148 символов, включая пробелы и переносы строк. Есть решения короче. Например:
http://nsl.com/k/sudoku/aw3.k
Это решение занимает всего 72 (!!!) символа. Но это решение Артура Уитни, создателя языка К. И решение на К. Артур Уитни – Бог программирования, соревноваться с ним не имеет смысла.
Мое решение содержит незначащие пробелы, вокруг присваивания (минус 10 символов) и можно еще «заинлайнить» глаголы, что тоже поможет выиграть несколько символов. Но не хочу ради краткости ломать красивый и понятный код ))

Уверен, что для J существуют способы написать короче, а у меня сказывается недостаток опыта.


3. Prolog

Хочется показать код на J для более практичной задачи и не близкой к матрицам/массивам. Слышал сомнения, что векторный подход подходит для парсинга. Пришла идея выбрать простую задачу для поста, решение которой связано с парсингом. Реализую Prolog. Точнее, не весь Prolog, т.к. задача не практическая и времени и усилий много тратить не хочется. Это будет подмножество Prolog-а, чтобы показать, что в принципе таситная форма записи годится и для таких задач. В этом Prolog-е, если его так можно назвать, не будет даже отсечения. Только простые факты и правила, операция «и», «или». Итак, код:

  frq =: [:-.[:(2:|+/)\''''&=
  
sp =: (#@[)([:<[}.[:>])"0 0[(]</.~[:+/\E.*.[:frq]),
  
spf =: [:<[:([:','&sp[:-.&')'=&'('{"0 1,.&',')>
  
cl =: #~[:-.e.&(33{.a.)*.frq
  parse
=: [:([:<[:((spf@{.),(}.`([:<[:([:<[:spf"0[:'),'&sp>)"0[:');'&sp[:>{:)@.(2:=#)))[:':-'&sp>)"0 _1:}.[:'.'&sp cl

  isVar
=: [:(91&>*.64&<)[:a.&i.[:{.>
  
replace =: ((]i.~[:{.[){([:{:[),]`([:<[$:[:>])@.([:32&=[:3!:0[:>]))"2 0
  
gp =: [:>[:{.>
  
gv =: [:(#~[:+./"1 isVar"0),.
  
suit =: ([(0:`(([:(#=[:#[:~.[:{.|:)[:~.[:(#~[:-.[:isVar"0[:{:|:)gv)*.([:*./[:+./[:(isVar"0,=/),:))@.(([:#[)=[:#]))[:gp])"1 0#]
  
sr =: [(](replace~[:|:])"2[:(([:-.[:isVar{:)"1#])[gv~[:gp])"1 0 suit
  groupVars
=: [:([:<]$~2:,~[:-:#)"1[:>[:([:<[:;(>@[)([:<,"1 1)"1 2(>@]))/]</.~[:{.|:
  
isRuleTrue =: ([:+./([:*./](isTrue~[:>])"1 0[:>[)"0 1)`(0:<[:#getVarsFromRule)@.(0:<#@gv@;@;@[)
  
isTrue =: ]((a:&e.@])+.[:+./[(isRuleTrue~[:>])"1 0[:-.&a:])[:{:[:|:[:-.&(a:,a:)[:(0 2$a:)&,[:>sr
  getVars
=: ;(([:<[:~.(>@{.@[)gv[:gp])`((>@{.@[)$:(<@<@gp@])([replace~[:|:[:>])"0 0(}.@[)getVarsFromRule~[:>[:{:[:>])@.([:<:[:#[:>]))"1 0 sr
  getVarsFromRule
=: ](([:{.])#~[(isRuleTrue~[:>])"1 0[:{:])[:|:[(],[:<[replace~[:|:[:>])"1 0[:]`groupVars@.(0:<#)[:~.[:;[:;]([:<[getVars~[:>])"1 0[:;[

  goal =: ([:<S:0[:{.[:parse[:,&'.'])([:{&(>'No';'Yes')isTrue)`([:(]`((>@{.),[:' = '&,[:>{:)@.(2:=#))"1[:>getVars)@.([:+./[:isVar"0[)([:parse[)

Это весь код. Собственно, парсер в первой части решения. Задача парсинга такого простого Prolog-а, конечно, примитивна. Но всё же, J можно применять для парсинга. И для более сложных задач парсинга.
Единственная «сложность» в парсере — Prolog допускает создание атомов из произвольных символов, если они берутся в одинарные кавычки. Иначе парсер был бы значительно короче. И еще, я специально не пользуюсь никакими библиотечными глаголами. Для того, чтобы вы оценили мощь самого языка, насколько быстро можно что-то написать с нуля. Есть и библиотека для работы с текстом. Есть библиотека для работы с регулярными выражениями.

Зададим код на Prolog-е:
  prolog_code =: 0 : 0
male(jeff).
male(fred).
female(mary).
parents(jeff, vince, jane).
parents(mary, vince, jane).

sister_of(X,Y) :- female(X),
      parents(X, F, M),
      parents(Y, F, M).
)
  

И пару вопросов.
«Приходится ли Мери сестрой Фреду?»:
  prolog_code goal 'sister_of(mary, fred)'
No

«Кому Мери приходится сестрой?»:
  prolog_code goal 'sister_of(mary, X)'
X = jeff

X = mary


Заключение


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

Цель примеров – показать код в таситной форме. Вы обратили внимание на то, что в коде практически нет каких-то нестандартных библиотечных глаголов? Весь код почти состоит из пунктуации. Это элементарные глаголы J. Конечно, никто не заставляет писать так, не вводя свои глаголы, не давая им осмысленных имен. Но высокоуровневость J предрасполагает в силу краткости кода не вводить новые имена. Цена усилий на создание нового кода мала, что приводит к тому, что реже хочется код прятать за именем. В целом со временем это приводит к тому, что программист на J читает одни и те же глаголы, которых не так много, и со временем учится всё лучше и лучше читать код. Код становится яснее и яснее.

Я думаю, что каждый программист, работающий на каком-то мейнстримном ООП-языке, с грустью согласится, какой сейчас ад с библиотеками и фреймворками. Правильные практики программирования призывают инкапсулировать код, прятать за осмысленными именами. Если усилия на создание кода достаточно велики – больше появляется имен. И когда вы, например, приходите в новый коллектив работать с проектом, от языка в коде мало что остается – вам нужно изучать новые для вас сущности, всё, чему дали имена предыдущие программисты. Изучение нового языка всегда грозит изучением библиотек, а их количество стремительно растет. J не решает эту проблему, но отдаляет.

J как язык имеет достаточно высокий порог вхождения. Поначалу он практически не читаем. Вы, изучив все глаголы, сможете с трудом на нем писать, но читать даже свой код не сможете. Но со временем, сможете это делать всё легче и легче. Это не произойдет с другими языками.

Также, надо упомянуть, что это не значит, что J имеет слабую поддержку и в нем недостаточно библиотек. Много уже наработок, от работы с excel-файлами, базами данных, до работы с видео. Также J мультипарадигменный, на нем можно писать не только в tacit форме, но и в императивной. С циклами и ветвлениями. С обработкой ошибок. J поддерживает ООП.

Работа с J напоминает не кодирование, а вывод формулы. Вы, поначалу, будете всматриваться в код, скорость набора значительно ниже. Но при всем при этом, результат даже поначалу будет получаться быстрее, чем в других языках. Даже не имея сверхбольшого опыта и читаемости кода, через время окажется, что сесть решить задачу на J кажется гораздо проще, чем решить на любом другом, более знакомом языке. Работа с J напоминает работу с мощным калькулятором, а не языком программирования. У вас нет ощущения, что вы создаете что-то на века. Скорость получения результата дает больше смелости в кодировании и вы легче пробуете различные варианты.

Благодаря этому у J есть ниша – обработка данных, особенно в финансовой сфере. На J легко «вращать» данными, проверять различные гипотезы. А если нужно – сохранить скрипт и использовать как программу. J удобен в проектах для обработки данных. Там где нужны сложные алгоритмы. Причем, под сложностью алгоритмов я подразумеваю «полезную сложность», т.е. сложность преобразования данных, а не алгоритмы для оптимизации. При всём при этом, J достаточно производителен. Пусть вас не пугает то, что он интерпретатор. Интерпретатор он только в том плане, что выполняет выражения построчно. Но если в строке записано выражение глагола, то он его выполняет один раз, запоминая, а далее при применении глагола к данным, работает уже быстро. Т.к. обрабатываются обычно данные большого объема, то оптимизации в J показывают неплохую производительность. Также J может работать с библиотеками, написанными на других языках. Что позволяет оптимизировать проблемные места.

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

Это всё хорошо, но есть одна проблема в использовании J в командных проектах – мало специалистов. Если вы решите использовать J в проекте, то высокая вероятность, что не найдете людей.

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

Также этот язык очень удобен для работы, как вспомогательный язык. У меня среда J всегда открыта и я не пользуюсь калькулятором или excel. Когда нужно распарсить что-то или сгенерировать код для другого языка – J – отличное средство.

Ссылки


1. Официальный сайт: www.jsoftware.com
2. Предлагаю почитать отличную статью — Язык программирования J, введение

+74
86.8k 193
Comments 130
Top of the day