Маргинальные языки программирования #1 - Red

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
Предисловие

Всем привет, с вами снова Патрик, и сегодня у нас под скальпелем (или микроскопом, тут как вам будет угодно) очень, не постесняюсь этого слова ОЧЕНЬ интересный язык программирования - Red lang.

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

Бывают си-подобные языки, мы все их знаем, любим и повсеместно используем. Их можно легко узнать по характерным конструкциям кода, обилию символов ";" в конце строки, а пишут на ниш чаще всего в ООП стиле (а иногда и без него вовсе). Сюда мы можем отнести очевидно C, C++, C#, Objective-C, D, Go, и с некоторыми допущениями возможно даже Java, и даже JavaScript и еще много-много всего. На сегодняшний день, это самая большая группа языков в нашей условной классификации.

Все эти языки могут быть абсолютно разными по назначению, но многие вещи в них реализованы если не идентично, то ну очень похоже. Грубо говоря если в С вы в курсе как реализовать алгоритм "найди нужный элемент в списке и верни его индекс", то и в Java, и в Go вы плюс минус понимаете что нужно сделать.

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

Вот так этот алгоритм реализуется в С:

C: Скопировать в буфер обмена
Код:
int factorial(int n) {
    int result = 1;
    for (int i = 1; i <= n; ++i)
        result *= i;
    return result;
}

А вот так в Go:

Код: Скопировать в буфер обмена
Код:
func factorial(n int64) *big.Int {
    if n < 0 {
        return nil
    }
    r := big.NewInt(1)
    var f big.Int
    for i := int64(2); i <= n; i++ {
        r.Mul(r, f.SetInt64(i))
    }
    return r
}

А вот так в JavaScript:

JavaScript: Скопировать в буфер обмена
Код:
function factorial(n) {
  if (n < 0) { throw "Number must be non-negative"; }

  var result = 1;
  while (n > 1) {
    result *= n;
    n--;
  }
  return result;
}

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

Есть другая достаточно большая группа под названием "семейство языков ML". Их фишка - функциональный стиль и крутецкая система типов Хиндли-Милнера. Сюда мы можем отнести OCaml, F#, Haskell, ReasonML и опять же, с некоторой натяжкой Rust.

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

В качестве примера, вычислим факториал в Haskell:

Код: Скопировать в буфер обмена
Код:
factorial :: Integral -> Integral
factorial 0 = 1
factorial n = n * factorial (n-1)

И сделаем то же самое в Rust:

Код: Скопировать в буфер обмена
Код:
fn factorial_recursive (n: u64) -> u64 {
    match n {
        0 => 1,
        _ => n * factorial_recursive(n-1)
    }
}

Не правда ли характерные и узнаваемые черты и конструкции?

Если лиспообразные языки, который тоже узнает кто угодно с первого взгляда - код там состоит из смайликов чуть более чем полностью. Сюда у нас попадают Common Lisp, различные реализации языка Scheme, Racket, Clojure, и еще много всего.

Бывают языки с синтаксисом, как у Ruby.
Бывают языки с синтаксисом, похожим на Delphi/Pascal (с бесконечно бесящим присваиванием через :=, скажем хором дедушке Вирту спасибо за наш туннельный синдром).

С этим всем все понятно.
А бывают странные языки. Языки, с синтаксисом, не похожим ни на одну из вышеперечисленных групп. Языки с абсолютно другим взглядом на то, как удобно и как все должно работать. У них привычные вещи делаются непривычно, алогично и вообще не так, как мы привыкли. И при изучении языка, набирая на клавиатуре очередную строчку кода, каждый раз ловишь себя на мысли и задаешь вопрос - что это, лютая всратая упопротость или все же гениальность?

И именно про такой язык и пойдет сегодня речь.
Дамы и господа, встречайте, Red.

Краткая историческая справка - REBOL

А началось все в уже теперь далеком, 1997 году, когда Карл Сассенрат (широко известный в узких кругах дядя, основной разработчик AmigaOS, человек благодаря которому современные оси умеют в многозадачность) выпустил первую версию языка REBOL.

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

Из того, что бросается в глаза сразу - более 60 (шестидесяти, Карл!) встроенных типов данных. Среди них есть адреса электронной почты, URL, теги разметки, денежные единицы, даты, время и даже пары координат.

Концепция языка вызвала бы ажиотаж в наше время больших языковых моделей и бума ИИ - сделать программирование простым и максимально приближенным к естественному языку. Базовая единица - слово. Из знаков препинания - только пробел и три варианта скобок "() {} []". Графический интерфейс - свой, из коробки.

Что получилось - давайте посмотрим в этой небольшой серии one-liner'ов:

Напечатать исходный код веб страницы (28 символ):
print read http://google.com

Открыть окно (да, графическое окно), запросить в качестве инпута адрес сайта и электронную почту и отправить содержимое сайта в письме (125 символов):
view layout [u: field "user@xss.is" h: field "http://" btn "Send" [send to-email u/text read to-url h/text alert "Sent"]]

Просканить открытые TCP порты (98 символов):
repeat n 100 [if not error? try [close open probe join tcp://localhost: n] [print [n "is open"]]]

Простенький куайн (93 символa):
RED[] do a: {view layout[t: field 500 rejoin["RED[] do a: {" a "}"] btn "do" [do do t/text]]}

Удалить все вхождения элемента из блока, строки или любой другой последовательности (35 символов):
while [ find list item ] [ remove find list item ]

Редактор файлов по FTP (размером в безумные 53 байта):
view layout[f: field btn"Edit"[editor to-url f/text]]

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

Но, как заметил внимательный и вдумчивый читатель - про Ребол или хорошо, или никак.
Язык был платным, с закрытыми исходными кодами и распространялся по лицензии (да-да, прежде чем писать на языке - нужно было купить интерпретатор). И в тот момент, когда развитие железа (переход на 64 битную архитектуру), веба, смартфонов и всего остального дало резкий скачок - у компании просто не было достаточных финансовых возможностей для конкурирования с опенсорс решениями. Вся эта история со странным языком потихоньку загибается, и по итогу, Карл в 2012 году открыл исходные коды под лицензией Apache, чем и воспользовался наш следующий герой.

На сцену выходит Red

В 2011 году на конференции по Rebol, французский программист с говорящим именем Ненад Ракоцевич представляет миру язык Red.

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

За основу был взят синтаксис Ребола (который напоминаю, интерпретируемый) и началась раскрутка компилятора.

Язык предполагалось сделать состоящим из двух частей​
  1. Red/System - тот самый низкоуровневый язык, похожий по задумке на С, но оернутый в синтаксис Ребола​
  2. Red - мета-язык высокого уровня, уже прямой наследник Ребола с возможностью как интерпретации, так и компиляции в исполняемый файл.​

Вся эта движуха завертелась закрутилась, потом ребята хайпанули на волне крипты и ICO, довели язык до состояния "все еще альфа, но уже можно щупать" и продолжают тихо-мирно пилить его по сей день.

Для разгона и чистоты эксперимента, посмотрим на факториал:

Код: Скопировать в буфер обмена
fac: function [n][r: 1 repeat i n [r: r * i] r]

Отлично!
Маргинальнее некуда, поэтому давайте щупать))

Установка на разных платформах

Несмотря на всю упоротость и маргинальность, к установке вопросов нет. Даже наоборот, я мечтаю что однажды разработчики более популярных языков программирования посмотрят как можно и сделают наконец так, чтобы мне не нужно было гуглить "как установить язык X на платформу Y", а также выкачивать гигабайты тулчейна.

Один исполняемый файл. Вес варьируется, но в среднем это 1мб (мегабайт) для консольной версии и 2.5 метра для гуи.

Никакой установки - этот файл и компилятор, и интерпретатор. И репл тоже.

Из доступных платформ - Windows, Linux, MacOS, а также ранее были различные варианты BSD, Android, и вот недавно появились варианты для ARM под линух.

Сразу огромная ложка дегтя - только 32 бита. Переезд на 64 битную архитектуру запланирован, но тянется уже достаточно долго чтобы забить. также с линуксом есть некоторые танцы с бубном, нужно таки выполнить пару команд ручками и поставить нужные версии библиотек. Ну и на современных макосях не запустится, потому что поддержку х32 тут дропнули уже достаточно давно.

Но минус ли это в мире малваре кодинга, где до сих пор в ходу и большом почете 32 битные версии софта? Оставлю этот вопрос для дальнейшего размышления читателю.

И сразу же следом за ложкой дегтя вброшу ложку меда - есть кросс компиляция, причем без всяких "если". Из любой доступной платформы и архитектуры в любую другую доступную платформу и архитектуру. За это конечно же ставим жирный плюс.

Структура кода в Red

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

Для начала, напишем простенький хэллоуворлд (все таки дань традициям):

Код: Скопировать в буфер обмена
Код:
Red []

print "Hello World!"

Обратите внимание, что код всегда начинается с заголовка Red …
Все, что идет до этой фразы - игнорируется интерпретатором (ставим себе заметочку, интересная фича для маскировки скриптов).

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

Код: Скопировать в буфер обмена
Код:
Red: [needs: 'view']

view [
    text "Hello World!"
]

Тут можно сразу обратить внимание на то, как происходит импорт модулей и как в Red реализованы DSL (киллер фича языка, но об этом чуть позже).

Запустить все это чудо проще простого. Вот так мы интерпретируем, компилируем и соответственно кросс-компилируем. Обратите внимание, флаг -c служит для компиляции в режиме development - рядом с исполняемым файлом будет лежать всякий нужный мусор. Для настоящей продакшн сборки нужно указывать флаг -r.

Вес итогового хэллоуворлда ~ 1 мб. Связано это с тем, что для высокоуровневых фич файл тащит с собой libRed. Уж не знаю что там происходит под капотом и насколько честно называть такой подход компиляцией, но имеем то, что имеем. По заявлениям разработчиков, там присутствует «частичная компиляция», но так ли это на самом деле - я лично не чекал.

Вес можно ощутимо уменьшить, используя низкоуровневый Red/System, но тут сразу возникают вопросы - для чего? Если гнаться за весом и писать все на лоулевеле, то почему бы не взять более зрелый язык? А если хочется использовать фичи Red, то один фиг придется его тащить с собой, тогда для чего использовать низкоуровневый DSL? Сейчас единственное адекватное применение, которое я могу придумать - это некий аналог wasm, для ускорения каких-либо критичных мест (естественно, сферических и в вакууме).

Синтаксис и интересные концепции

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

Базовая единица языка - слово (вспоминаем про natural language processing и глобальную великую идею). Слово может иметь разные типы и обозначать разные сущности. Программа на Ред - это последовательность слов.

Переменные в Ред задаются с помощью вот такого синтаксиса

Код: Скопировать в буфер обмена
Код:
>> x: 42
== 42
>> print x
42

Слова могут быть ассоциированы со значениями, а могут быть с целыми блоками кода (гомоиконность, добрый вечер):

Код: Скопировать в буфер обмена
Код:
>> a: [print "hello"]
== [print "hello"]
>> do a
hello

Неискушенный ум может подумать, что тут попахивает чем-то вроде функции eval, которая на сегодняшний день присутствует в каждом первом скриптовом языке и настоятельно не рекомендуется к применению везде, где только можно. Но есть тут очень важное отличие. Eval принимает в качестве аргумента строку. А здесь - натуральный кусок кода, то есть заявочка на гомоиконность действительно серьезная.

Окей, если есть сеттер, значит где-то рядом должен быть и геттер. И он реализован на удивление логичным образом - двоеточие перед названием переменной:

Код: Скопировать в буфер обмена
Код:
>> x: 42
== 42
>> y: :x
== 42
>> print y
42

Тут также стоит заметить, что и сеттер, и геттер имеют собственный тип данных (set-word! и get-word! соответственно), а также что при попытке гетнуть какую-нибудь дефолтную функцию, например print - все корректно отработает:

Код: Скопировать в буфер обмена
Код:
>> imprimire: :print
== make native! [[
   "Outputs a value followed by a newline"
   ...
>> imprimire "hello"
hello

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

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

Код: Скопировать в буфер обмена
Код:
>> print something
*** Script Error: something has no value
*** Where: print
*** Stack: 

>> print 'something
something

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

Код: Скопировать в буфер обмена
Код:
>> set 'test 33
== 33

Порядок выполнения

Как мы уже поняли, Ред работает с кодом как с набором слов. И перед тем, как мы продолжим погружение - я считаю очень важным остановиться на том, как Ред выполняет код.

Я опишу, как я вижу это с моей личной точки зрения. Опять же, это может оказаться неточным, но пока что это достаточно хорошо объясняет поведение Реда и позволяет не сломать себе голову на элементарных вещах.

После запуска Red начинает читать текст слева направо (→), выполняя все операции, которые он может найти. Если он распознает операцию, требующую аргументов, он выбирает аргументы из этого основного текста по мере необходимости, чтобы прийти к окончательному значению. Рассмотрим понятие оценочных групп и выборки аргументов. Red рассматривает текст (строки) как блок символов, поэтому основной текст кода Red - это просто большой блок для Red, даже без скобок и кавычек.

Выполнение кода триггерится командой "do" (как мы уже видели в примере выше). При запуске скрипта или нажатии клавиши Enter в консоли не всегда нужно набирать команду do, это означает, что вы применяете неявную команду do к следующему тексту. В случае скрипта оценка начинается только после того, как интерпретатор найдет символы "Red [".

Интересным следствием всего этого является то, что, хотя это и не считается хорошей практикой, вы можете действительно выполнять текст:

Код: Скопировать в буфер обмена
Код:
>> do "3 + 5"
== 8

>> 3 + 5
== 8

Окей, а как тогда получить результат?
Результатом интерпретации Red является результирующее значение последней оцениваемой группы. Конечно, по пути можно делать всякие интересные вещи: писать файлы, читать веб-страницы, создавать красивые рисунки на экране, но значение, возвращаемое Red (если оно есть), - это последний результат.

Код: Скопировать в буфер обмена
Код:
>> do "3 + 5 7 * 8 print 69"
69

Мы поняли, что триггернуть выполнение кода можно с помощью слова "do". Но в какой момент выполнение (нитерпретация) кода останавливается? Что является триггером для этой ситуации? Конечно, конец текста (кода) и комментарии.

Но, кроме того, интерпретатор Red пропускает блоки внутри основного текста (блоки внутри основного блока), просто оставляет их как есть. Он выполняет их только в том случае, если они являются аргументом операции. При этом эта операция может быть другой операцией do:

Код: Скопировать в буфер обмена
Код:
>> do {print "hello"  7 + 9  [8 + 2]}
hello
== [8 + 2]

При написании кода на Ред, иногда, вам может понадобиться не только результирующее значение. А, например, все значения, которые получались в процессе выполнения. Этого можно достичь, используя "reduce". Обратите внимание на то, что это НЕ то же самое, что применить do к каждому блоку по отдельности:

Код: Скопировать в буфер обмена
Код:
>> reduce [3 + 5 7 * 8 "print 69"] ; do "print 69" should print 69!
== [8 56 "print 69"]

Порядок выполнения математических операций

Честно, я не думал, что мне понадобится это когда-либо кому-то объяснять (в том числе себе самому). Просто потому что порядок выполнения мат операций и их приоритет одинаковые плюс минус в 99% языков, и на моей памяти последний раз неприятно было только при работе с Фортом. В остальных случаях все как-то само собой, логично и нативно.

Но не здась. Следите за руками, я постараюсь объяснить как это работает максимально просто.​
  1. Все операции с инфиксными операторами, аргументы которых только значения (не функции) - выполняются первыми. Если эти инфиксные выражения имеют более двух операндов, то они вычисляются слева направо ( → ) без приоритета операций (т.е. умножение не вычисляется автоматически перед сложением).​
  2. Затем все выражение вычисляется справа налево (← ).​
Пример:

evaluation6.png



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

Типы данных

Как я уже упоминал ранее, одна из особенностей Ред - огромное количество встроенных типов данных. Я не буду останавливаться подробно на каждом из них, поскольку некоторые из них достаточно очевидные (а некоторые еще и весьма бесполезные), пройдусь лишь по основным повседневным типам данных, которые чем-то отличаются от привычных нам, а также по сериям.​
  • none! - аналог null или undefined в других языках программирования. Не существующие данные.​
  • logic! - аналог булей. Отличительная особенность - помимо стандартных true/false, Ред также распознает on/off и yes/no. По аналогии с другими языками, все, что не равно false/off/no считается истиной​
  • string! - строка в Ред. Является серией и представляет собой последовательность символов, а значит - предоставляет нам стандартные возможности работы с последовательностями. Обозначается привычными двойными кавычками и, неожиданно, фигурными скобками (которые используются для мультилайн строк)​
  • char! - тип символа, задается с помощью решетки и двойных кавычек. Представляет собой целое число от 00 до 10FFF (в хексе), соответствует юникод символам и поддерживает математические операции​
Все остальное - плюс минус как у людей, за исключением совсем уж упоротых типов данных. Например, 11% - это тип данных "проценты".

Последовательности

Я думаю все уже привыкли к исключительной неоднозначности нашего сегодняшнего подопытного, а также мы все держим в голове мысль о DSL.
Чтобы понять последовательности в Ред, нужно понять следующее:

Есть Блоки. Ред состоит из блоков. Все, что окружено квадратными скобками, считается блоком.

Есть Последовательности - это группы элементов. И они по сути являются основополагающим типом в Ред. Даже программы, написанные на Ред сами по себе - это последовательности. Элементами последовательности может быть что угодно из лексики Ред - данные, слова, функции, объекты или другие последовательности.

Как я уже говорил ранее, строки - это последовательности символов в Ред, поэтому к ним применимо все то, о чем мы будем сейчас говорить. Как и большинство более-менее сложных сущностей в Ред.

Для примера, начнем с массивов - известной многим структуры данных из других языков программирования. В Ред массив, естественно, является последовательностью. А многомерный массив, соответственно - последовательность из других последовательностей:

Код: Скопировать в буфер обмена
Код:
>> a: [[1 2][3 4][5 6]]
== [[1 2] [3 4] [5 6]]

Чтобы получить элемент массива, используется весьма неочевидный символ - слэш:

Код: Скопировать в буфер обмена
Код:
>> a/1
== [1 2]

>> a/1/1
== 1

>> a/3/2
== 6

Использовать переменную в качестве ключа можно с помощью уже известного нам геттера:

Код: Скопировать в буфер обмена
Код:
>> a: ["me" "you" "us" "them" "nobody"]
== ["me" "you" "us" "them" "nobody"]
>> b: 4
== 4
>> a/:b
== "them"

Навигация по последовательностям

Да-да, строго говоря, массив - не совсем массив. Последовательности представляют собой не что иное, как односвязный список с некоторыми особенностями реализации:​
  • Первый элемент последовательности - head​
  • В конце каждой последовательности есть tail - у него нет значения​
  • У каждой последовательности есть сущность, под названием "стартовая точка". Наиболее человеческое объяснение - это место, где начинается значимая часть последовательности. Это очень важно, потому что многие операции с последовательностями опираются на эту, хм, "стартовую точку".​
  • У каждого элемента есть индекс, и начинается он с единицы (не с нуля)​
Важные функции:​
  • head - передвигает стартовую точку в начало последовательности​
  • tail - передвигает стартовую точку в конец последовательности, прям в самый конец, после последнего элемента​
  • next - двигает стартовую точку на один шаг вперед​
Важно - ни одна из этих функций не изменяет оригинальную последовательность, поэтому не получится использовать next несколько раз для итерации - делать это нужно через присваивание (s: next s).

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

Небольшой пример для наглядности и понимания, как это работает:

Код: Скопировать в буфер обмена
Код:
>> s: [ "cat" "dog" "fox" "cow" "fly" "ant" "bee" ]
== ["cat" "dog" "fox" "cow" "fly" "ant" "bee"]

>> s: next s
== ["dog" "fox" "cow" "fly" "ant" "bee"]

>> print s
dog fox cow fly ant bee

>> head? s
== false

>> print first s
dog

>> index? s
== 2

Обратите внимание на то, что хоть функция first и возвращает теперь значение "dog", index? возвращает 2 (абсолютное значение).
Ну вы поняли, есть список, а есть "стрелочка-указатель", которую мы можем двигать взад-вперед и от нее зависят результаты вызовов функций.

Еще немного про навигацию.
back - как next, только двигает указатель на один назад
skip - двигает указатель на N шагов вперед. Если число больше, чем длина последовательности - остается указывать на tail.

Геттеры последовательностей

Тут у нас тоже достаточно интересный зоопарк, но поверьте, все это постепенно сложится в одну общую картину, когда мы дойдем до DSL, и наконец-то осознаем почему все так странно называется и еще более странно работает (не топлю за то, что это хорошо, просто констатирую факт - картина сложится).​
  • pick - берет элемент из последовательности с заданным индексом и возвращает его​
  • at - возвращает последовательность, начиная с заданного индекса​
  • select и find - оба ищут заданный элемент в последовательности. Разница в том, что select возвращает следующий за найденным элемент, а find - последовательность, начиная с искомого элемента​
  • extract - возвращает новую последовательность из элементов с заданным шагом. По простому - вернет каждый второй или каждый третий элемент​
Сеттеры последовательностей

Честно, чем глубже мы погружаемся, тем более стойким становится ощущение, что некоторые названия функций авторы меняли просто потому что "мы хотим, чтобы это называлось по-другому".​
  • clear - очищает последовательность​
  • poke - заменяет элемент по заданному индексу на указанный​
  • append - очевидно, добавляет элемент в конец​
  • insert - как append, только добавляет значения не в конец, а в текущую точку указателя​
  • replace - находит и заменяет заданный элемент​
  • remove - удаляет первый элемент последовательности​
И еще много-много-много других вариаций вставок, удалений, замен, перемещений, телепортаций и других жонглирований данными.

Глобально - конечно здорово, всегда лучше когда какой-то функционал есть, чем когда его нет. Но блин, почему бы не вынести совсем уж экзотичные функции (типа выборки каждого третьего элемента) в отдельную библиотеку? Назвать ее "жонглируем данными вдоль и поперек" и пусть ее используют те, кому это надо. Такая бибилотека есть в каждом первом языке, но для чего все это напихивать в stdlib? Тайна сия велика

Типы последовательностей

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

Hash!

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

Это хэш. Специальный список, который "хэширует значения для ускорения поиска".
Ну допустим, допустим. На деле знакомый некоторым из нас проперти лист:

Код: Скопировать в буфер обмена
Код:
>> a: make hash! [a 33 b 44 c 52]
== make hash! [a 33 b 44 c 52]

>> select a [c]
== 52

Vector!

Тут без долгих объяснений - оптимизированная последовательность только для чисел. Из особенностей - можно умножить вектор на число :D

Код: Скопировать в буфер обмена
Код:
>> a: make vector! [33 44 52]
== make vector! [33 44 52]

>> print a
33 44 52

>> print a * 8
264 352 416

map!

А вот это как раз наш словарь:

Код: Скопировать в буфер обмена
Код:
>> a: make map! ["mini" 33 "winny" 44 "mo" 55]
== #(
       "mini" 33
       "winny" 44
       "mo" 55
...

>> print a
"mini" 33
"winny" 44
"mo" 55

>> print select a "winny"
44

Обратите внимание, что map - НЕ последовательность, соответственно все абракадабры с указателем, в которых мы так долго разбирались, тут не работают. Как и не работают большинство функций для последовательностей. Настолько, что не осилили даже сделать генерик сеттеры и геттеры - поэтому для получения и записи элементов используются отдельные слова select и put.

Если вам еще не настолько смешно, как мне, то вот вам аналог функции append специально для словарей - extend:

Код: Скопировать в буфер обмена
Код:
>> extend a ["more" 23 "even more" 77]
>> probe a
#(
   "mini" 33
   "winny" 44
   "mo" 55
   "more" 23
   "even more" 77
)

На этом пожалуй закончим с великими и ужасными типами данных языка Red и перейдем к управлению выполнением, а именно - циклам и условным операторам

Флоу

Вариантов условных операторов нам, как и всего остального - завезли с запасом (и все они одинаково упоротые).

И на этом моменте я напоминаю вдумчивым читателям, что если вы думаете, что уж if-else то точно в Red работают как везде, то имейте в виду - мы имеем дело с чешско-французской магией высокого уровня, и ее логика недоступна смертным:​
  • if - выполняет блок кода, если условие истинно. Если условие ложно - ничего не выполняет, и никто из нас его не заставит​
  • unless - то же самое, что if not. Ну мало ли, вы по религиозным соображениям не можете написать if not или у вас клавиши какие-то заедают​
  • either - аналог if-else. Даже блин не спрашивайте.​
  • switch - тут слава богу все как у всех, обычный классический свитч​
  • case - это как свитч, только мы проверяем услоия и выполняем блок для того, которое истинно. У здоровых людей это называется match​
  • catch-throw - нет, не обработка исключений. Просто еще одна конструкция для ветвления, если предыдущих пяти вам мало​
Код: Скопировать в буфер обмена
Код:
>> if 10 > 4 [print "large"]
large

>> unless 4 > 10 [print "large"]
large

>> either 10 > 4 [print "bigger"] [print "smaller"]
bigger

>> switch 20 [
...       10 [print "ten"]
...       20 [print "twenty"]
...       30 [print "thirty"]
...]
twenty

>> case [
...       10 > 20 [print "not ok!"]
...       20 > 10 [print "this is it!"]
...       30 > 10 [print "also ok!"]
...]
this is it!

>> a: 10
>> print catch [
...       if a < 10 [throw "too small"]
...       if a = 10 [throw "just right"]
...       if a > 10 [throw "too big"]
...]
just right

Также просто не могу не поделиться с дорогими читателями информацией о функциях all и any. Обе принимают в качестве аргументов блок из выражений и работают вот так:​
  • all - если хоть одно значение ложно, возвращает none. Иначе, возвращает результат последнего выражения​
  • any - возвращает первое не равное false выражение. Если такого нет, возвращает none​
Циклы

Боже, как я радовался одному единственному циклу в V.
Поехали:​
  • loop - аналог for, выполняет блок заданное количество раз​
  • repeat - то же самое, что loop, только с индексом​
  • forall - выполняет блок при этом двигаясь по последовательности. Не обманывайтесь названием, это даже не близко не map и не foreach - просто посмотрите пример и попробуйте осознать​
  • foreach - это уже ближе к правде, выполняет блок для каждого элемента последовательности​
  • while - ну прям белая ворона, просто обычный while​
  • until - выполняет блок до тех пор, пока блок не вернет true​
  • forever - бесконечный цикл, для тех кто вышел из лесу и не знает о существовании while true​
Код: Скопировать в буфер обмена
Код:
>> loop 3 [print "hello!"]
...

>> repeat i 3 [print i]
...

>> a: ["china" "japan" "korea" "usa"]
>> forall a [print a]
...

>> foreach i a [print i]
...

>> i: 1
...while [i < 5] [
...print i
...       i: i + 1
...]

>> i: 4
>> until [
...       print i
...       i: i - 1
...       i < 0        ;  <= condition
...]

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

Пара слов про функции и объекты

Функции тут создаются с помощью двух ключевых слов - func и function.

Разница между ними в том, что все переменные, объявленные внутри func - глобальные. То есть работает это вот так:

Код: Скопировать в буфер обмена
Код:
Red []

mysum: func [a b] [
       mynumber: a + b
       print mynumber
]
mynumber: 20
mysum 3 4      ; 7
print mynumber ; 7

mysum: function [a b] [
       mynumber: a + b
       print mynumber
]
mynumber: 20
mysum 3 4      ; 7
print mynumber ; 20

А function в свою очередь уже создает локальные переменные, что конечно ближе и привычнее многим из нас.

Из прикольных особенностей, которые мне действительно понравились - механика уточнений. Мы уже встречали ее несколько раз в коде, и в целом Red этим изобилует.

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

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

Судите сами:

Код: Скопировать в буфер обмена
Код:
>> round 2.3
== 2.0

>> round/to 6.8343278 0.1
== 6.8

>> round/down 3.9876
== 3.0

Объекты в Red представляют собой достаточно базовую реализацию принципов ООП. Можно создавать поля и методы, есть наследование в зачаточном состоянии и удобный доступ к полям/методам через слэш.

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

DSL

И вот, мы наконец-то подобрались к самому интересному. К тому, зачем вообще стоило смотреть на этот странный, необычный и местами даже несуразный язык.

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

DSL (Domain Specific Language) переводится как "предметно-ориентированный язык".

Простыми словами - вот есть языки общего назначения, типа Python, C, и того же Red. Они полные по Тьюрингу и на них можно реализовать любую программу, прям вообще что угодно. Но к сожалению, это не всегда просто и удобно.

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

Если вам кажется, что вы никогда не сталкивались с такими языками, то вот пара примеров:​
  • Язык гипертекстовой разметки, он же HTML. Я думаю ни у кого не возникает желания описывать структуру веб страниц как-то иначе, потому что html простой и удобный.​
  • JSON, Yaml, Toml - туда же. На них не напишешь малварь, но они отлично подходят для структурного описания данных и используются повсеместно​
  • SQL - тоже представляет собой предметно-ориентированный язык, и также применяется ежедневно по всему миру​
Я думаю с этим все понятно, DSL - это такие мини-языки для конкретной задачи. Едем дальше.

Существует парадигма программирования под названием Языково-ориентированное программирование (сокращенно ЯОП). Суть идеи очень проста. Есть задача. Мы придумываем язык, с использованием которого эту задачу решить легко и просто. Затем мы реализуем такой язык на каком-то языке общего назначения. И наконец решаем нашу исходную задачу легко и просто с помощью нашего нового инструмента.

Ультражирный и не всегда очевидный плюс такого подхода - возможность разделить такой подход на две стадии:​
  1. Разработка языка Х​
  2. Решение задачи с использованием языка Х​
И прикол в том, что если для пункта один нужен толковый разраб, то пункт два при достаточно мощном языке может реализовать даже обычный пользователь. То есть планка входа в DSL может быть реально очень низкой, а это и экономия времени, и экономия на оплате труда. Наглядный пример - Microsoft Excel, которым пользуется огромное количество рядовых пользователей и комфортно работают с формулами, не являясь при этом программистами.

А теперь давайте вернемся к нашему языку Red.
По задумке авторов, он как раз таки был запланирован как язык для удобного создания DSL (которые здесь называются диалектами). Из уже разработанных и имеющихся из коробки:​
  • system - язык для низкоуровневого программирования​
  • view - язык для создания GUI​
  • draw - язык для рисования разного​
  • parse - язык для парсинга и работы с текстом​
Давайте пройдемся по примерам и посмотрим, как это работает.

Небольшая программа с использованием диалекта parse для валидации адресов электронных почт:

Код: Скопировать в буфер обмена
Код:
Red []

digit:   charset "0123456789"
letters: charset [#"a" - #"z" #"A" - #"Z"]
special: charset "-"
chars:   union union letters special digit
word:    [some chars]
host:    [word]
domain:  [word some [dot word]]
email:   [host "@" domain]

print parse "john@doe.com" email
print parse "n00b@lost.island.org" email
print parse "h4x0r-l33t@domain.net" email

Изящно, не правда ли?
Как может заметить вдумчивый читатель, это безумно похоже на расширенную форму Бэкуса - Наура, которая используется повсеместно для формального определения синтаксиса. А значит и писать диалекты на языке Red будет очень просто.

Чуть более сложный пример - валидатор математических выражений, с использованием рекурсивных правил:

Код: Скопировать в буфер обмена
Код:
Red []

expr:    [term ["+" | "-"] expr | term]
term:    [factor ["*" | "/"] term | factor]
factor:  [primary "**" factor | primary]
primary: [some digit | "(" expr ")"]
digit:   charset "0123456789"
  
print parse "1+2*(3-2)/4" expr        ; will return true
print parse "1-(3/)+2" expr           ; will return false

Также как на языке Red будет очень удобно морфить и обфусцировать код, потому что можно легко и просто разобрать программу на блоки, изменить и собрать обратно.

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

PoC Mini Brainfuck Interpreter + PoC Clipper

Окей, разобрались с приколами синтаксиса, посмотрели как тут что устроено, давайте теперь покодим что-нибудь боевое.

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

И начнем мы пожалуй с клиппера.

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

Код: Скопировать в буфер обмена
Код:
Red []

my-address: "YOUR_ETH_ADDRESS_HERE"

digits:      charset "0123456789"
letters:     charset [#"a" - #"f" #"A" - #"F"]
chars:       union digits letters
eth-address: ["0x" 40 chars]

data: read-clipboard
parse data [to eth-address change eth-address my-address]
write-clipboard data

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

То есть замена произойдет не только для соло-адреса, но и для адреса внутри какого-то сообщения или куска текста.

Обратите внимание - программа в представленном выше стиле будет проще и понятнее для разработчика с ростом кодовой базы, чем те же регулярки.

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

Второй концепт, который я хотел бы показать - использование Red в качестве фабрики новых языыков и виртуальной машины. Не просто так я выше заметил, что описание синтаксиса для DSL очень похоже на BNF. Это очень широко используемая нотация, а значит если читатель захочет реализовать, допустим, интерпретатор Си или асма - формальное описание языка в формате BNF будет достаточно легко найти и адаптировать под синтаксис Ред.

Как пример - небольшой интерпретатор еще одного маргинального языка Brainfuck:

Код: Скопировать в буфер обмена
Код:
Red []

bf: function [prog [string!]][
    size: 30000
    cells: make string! size
    append/dup cells null size

    one-back: [pos: (pos: back pos) :pos]

    jump-back: [
        one-back
        any [
            one-back
            ["]" jump-back "[" | "[" resume: break | skip]
            one-back
        ]
    ]

    cmd: complement charset "[]"
    nested: [any cmd | "[" nested "]"]

    brainfuck: [
        some [
              ">" (cells: next cells)
            | "<" (cells: back cells)
            | "+" (cells/1: cells/1 + 1)
            | "-" (cells/1: cells/1 - 1)
            | "." (prin cells/1)
            | "," (cells/1: first input "")
            | "[" [if (cells/1 = null) nested "]" | none]
            | "]" [pos: if (cells/1 <> null) jump-back :resume | none]
            | skip
        ]
    ]
    parse prog brainfuck
]

; Print Hello World! in brainfuck

bf {
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.
>++.<<+++++++++++++++.>.+++.------.--------.>+.>.
}

Здесь мы используем все то же самое. Единственное существенно отличие в том, что мы сразу же интерпретируем встречающиеся символы (а могли бы морфить, обфусцировать, ну вы поняли намек).

Вместо заключения

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

Язык все еще в альфе. Язык безнадежно морально устарел. Только 32 бита, нет многопоточности, нет пакетного менеджера. Да половины того что я ежедневно использую нет, зато есть нескучные дизайнерские решения.

Но суть не в этом и сказать я хотел совсем не это. Red - по сути фреймворк для создания DSL. И именно на эту фишку я хотел обратить внимание. С помощью Ред действительно просто создавать, тестировать и внедрять новые языки. С помощью Ред вы можете пощупать ЯОП подход и начать работать в этом стиле. Можете создать свой личный идеальный язык, подходящий именно под ваши задачи. А потом фиг с ним, с Редом - можно написать интерпретатор на чем-то вменяемом.

Концепции, которые зародились изначально в Rebol, а затем перекочевали в Red - могут очень сильно упростить жизнь малваре кодерам, да и всем остальным разработчикам тоже, поскольку показывают нам привычные задачи с совершенно новой стороны. И напоминают, что язык - это в первую очередь инструмент. И если существующие инструменты не подходят под вашу задачу, это повод задуматься - возможно, пора создать свой?

Патрик.
Специально для XSS​
 
Сверху Снизу