2
arseniivЦитата:
А чтобы не мучиться с синтаксисом
Не, ваша первоначальная идея очень хороша. Очень не хватает именно такого императивного языка с такими возможностями. А то всякие хаскели да лиспы кругом. :) Да, парсер будет сложнее; ну и что, прототип компилятора все-равно будет занимать не больше 500-2000 строчек кода, не страшно.
Насчет tcl все внимательно прочитал, но поддержать здесь тоже вас не могу. Да, tcl -- замечательный язык, но в своей области. Я его вообще не более чем языком пакетных сценариев считаю (чем он впрочем и является). И, как я понимаю, без особых ухищрений придется использовать префиксную запись в выражениях; зачем такие ограничения (кои в тоже время вполне естественны для командно-ориентированного языка вроде tcl'а и --- но уже для случая дуальной, постфиксной нотации --- стек-ориентированного вроде форта)?
Думаю, не стоит бояться придумывания нового необычного синтаксиса. Это же все-равно эксперимент, деньги-то не плотют. :)
Цитата:
Ещё в таком случае, всё-таки, наверно лучше отойти от только одних множеств и ввести числа — целые, рациональные (при этом логично использовать библиотеку для чисел произвольной длины)
Пока стоит попробовать все-таки все выражать через множества. Даже числа. Согласитесь, так красивше.
Цитата:
и действительные с плавающей точкой (тут, думаю, усложнения только повредят — мы же не собираемя строить ещё одну «всемогущую» CAS), а также кортежи, а то представление их множествами не очень удобно.
Действительные пока, имхо, там вообще ни к чему, а кортежи уж пусть будут пока множествами. Если нужна упорядоченность, просто положите каждый элемент множества в двуэлементное множество [номер, [элемент]]. А вот лексические кортежи пригодятся, я имею ввиду в первую очередь конструкцию множественного присваивания
a, b = c, d, что эквивалентно
a=c; b=d;. В довесок получается, что формальные параметры функции -- тоже лексический кортеж. Но тут надо все-таки подумать. То ли можно обойтись функцией
tuple или
pair, позволяющей упорядочивать множество, помещая данные в кортеж-список, то ли действительно стоит ввести нотацию с перечислением в круглых скобках + неплохо бы было все это как-то хитро скомпоновать с лексическими кортежами, C'шными вычислительными последовательностями --- я имею ввиду оператор '
,' --- и массивами (чем массив не кортеж? что если кортежи индексировать также как и массивы, т.е. через
a[i]?) ... То есть, хотелось бы достичь определенной синтаксической однородности...
Кортежи ещё можно заменить такой несколько экзотичной, чуть ли не ООП'шной конструкцией. Когда нам нужен кортеж с фиксированным числом элементов, да ещё и с удобным доступом по имени, т.е., фактически C'шная
struct мы создаем функцию с пустым телом, e.g.
func(param1, param2){}, вызываем её с определенными фактическими параметрами, e.g.
func(5, []), а результат передаем туда, где ожидается кортеж. Например,
a=func(5, []). Теперь мы можем обращаться к элементам кортежа таким способом:
a.param1. А присваивания
b=a.param1; c=a.param2; можно тогда кратко записать как
b, c = a;.
Может-быть на какие-нибудь мысли натолкнет простой факт -- кортеж порождается декартовым произведением.
Ниже я парочку примеров S1-кода попытался привести. Так вот там я использую обычную
(...) нотацию для кортежей (подразумевается, что котрежи можно порождать конструкторами, аналогичными конструкторам множеств, правда на этом пути есть серьезные синтаксические проблемы). Возможно, стоит считать литералы кортежей именно синтаксическим сахаром, на самом деле создающим множество:
(a, b, ...) [[1, [a]], [2, [b]], ...]. Как вам?
Ещё фичи: Думаю не помешают именованные фактические параметры функций, что-то вроде
f(param1=1, param2=2). Также неплохо бы было внедрить сопоставление с образцом (кстати, предлагаю ввести ещё и универсальный идентификатор
_), хотя здесь, с учетом специфики разрабатываемого языка, не все так просто.
Цитата:
Есть интересный вопрос: пусть мы имеем какое-нибудь множество, заданное в форме выражения. Как определить, конечно ли оно?
Ну вот вы там сами и ответили прекрасно. Кроме того, мне вот понравился вот этот ваш подход:
Цитата:
Я предлагаю всё-таки представлять бесконечные множества специальными объектами, показывающими, как они строятся из других конечных и «атомных» бесконечных.
В общем, это пока не представляется какой-то страшной и непреодолимой проблемой. Я вот навскидку так думаю, что можно попробовать определить бесконенчое множество натуральных чисел, а потом уже с ним играться, конструируя из него любые другие бесконечные множества. Как вы и сказали, компилятор будет знать, что некоторое множество определено через бесконечное. Более общий подход подразумевает, что бесконечное множество должно обязательно задаваться какой-то функцией-формулой, определенной для всех натуральных чисел; в этом случае объект-множество будет просто содержать флажок, сигнализирующий о бесконечности, и формулу, порождающую множество.
Мне интересней теор-множественные операции на бесконечными множествами. Например, нужно нам найти пересечение двух таких множеств. Они порождаются выражениями
e1 и
e2. Мы можем сконструировать новое множество, выражение-генератор которого будет выражением
e1&&e2. А можно сконструировать новое множество с указанием (e.g., в виде флажков), что это множество является пересечением исходных. Кажется, последний подход позволил бы много чего понадергать из теории типизации (обычное C'шное
int i; ровно и означает
i in Z; :) ).
Ох не просто будет компилятору S1, он ведь должен будет догадаться, что именно имел ввиду программист. :)
Цитата:
язык можно сделать поддерживающим уникод в названиях операций
Не надо! S1 это вам не 1C. :)
Цитата:
Мне бы хотелось рассмотреть побольше примеров, чтобы понять, насколько далеко идти, чтобы не застрять с этим языком навеки.
А ну да, можно попробовать настоящий test-driven development, с предварительным написанием тестов; сначала примеры кода, потом -- компилятор. Опять же повторюсь, денег не плотют -- торопиться некуда. :)
За парсер и сложность синтаксиса не переживайте -- это самая простая часть в таком языке. :) Главное -- придумать правила анализа кода, затем придумать низкоуровневое представление (ассемблер для VM), ну и заодно написать саму VM (со сборщиком мусора!). (Пока здесь, имхо, нативный код не нужен. Выигрыш будет ничтожным из-за гипертрофированной высокоуровневости языка.)
Цитата:
Если понравится, не придумаете ли что-нибудь своё в пример?
Сначала поясните, что вы имели ввиду, когда говорили "должно быть обязательное указание множеств, из которых берутся все переменные в левой части такого конструктора"? Т.е. вы хотели бы видеть что-то вроде
R=[x in R : x>=0];?
Также замечу, что я принципиально против вот такого вот вида интерактивной repl-сессии. То есть работать над отладочным инструментарием и над слоем интроспекции может быть и стоит, но изначально требовать от среды таких вот выкрутасов с красивой печатью промежуточных результатов -- это пока слишком, имхо. Лучше предусмотреть в языке простой консольный ввод-вывод и печатать множества самостоятельно. Или проверять выполнение различных интересных условий через
_assert(), см. выше.
Не понял также, что вы имели ввиду под "кстати, можно сделать перед печатью результата поиск в таблице идентификаторов и отобразить это как "R2"". Если не ошибаюсь, смысл
{ x * y : x in R1 && y in R2 } ну никак не совпадает ни с
R2, ни с
R * { t0 : t0 <= 0 } (а может вы хотели написать не
x*y, а
[x]*[y]?). Через
~ (тильда) вы видимо обозначили симметрическую разность, а вот что такое
%1=%2 даже близко не догадываюсь. И почему вы поменяли квадратные скобки на фигурные? Мне вот квадратные на моей клавиатуре набирать проще, без шифта. :)
А так ваш код понятен. Могу только попробовать переписать его в более для меня понятный синтаксис:
Код:
res= R1 = [x in R : x>=0];
res+= R2 = [-x : x in R];
res+= R1+R2;
res+= R1*R2;
res+= R1\R2;
res+= [x*y : x in R1, y in R2];
res+= [0, ... , 8] in Z;
res+= [pair(x, y) : x in N, y in N, x+1=y];
res+= [pair(x, x+1) : x in N];
print(res);
Что должно вывестись в результате -- не представляю; как я говорил выше, для меня проще проверять ход дела assert'ами.
Вообще эти примеры хорошо изучены и проработаны в функциональных языках. Так что с list comprehension проблем быть не должно. Хотя, опять же, специфика S1 подразумевает, что такие конструкторы множеств должны обрабатываться иначе, с использованием даже не лени, а суперлени. :) То есть, если в типичном функциональном языке со списочными литералами, выражение
[x | x in list1, x>5] есть сахар над
r=[]; forall(x in list) if(x>5) r+=[x];, то в S1 мы должны таскать за собой конструктор множества пока не выяснится, что его действительно можно и нужно превратить в цепочку циклов и условий. Собственно, при интроспекции (если её вообще стоит делать, ибо лучше всего семантиу конструктора конкретного множества описывает либо весь жизненный цикл запущенной программы, либо сгенерированный байт-код; и то и другое не совсем то, что ожидается в качестве результата работы воображаемой функции печати
introspect) можно печатать именно внутреннее представление множеств, выгребая оттуда куски использованных при их построении конструкторов (например, сообщение repl-интерпретатора <infinite set> должно быть дополнено указанием способа создания множества, с указанием выражения-функции, генерирующего печатаемое множество). Нужны эксперименты. В том числе нужно поиграться с разными вариантами написанных на самом S1 функций печати множеств.
Теперь попробую ещё немного пофантазировать, и привести ещё парочку примеров S1-программулек; я тоже не буду никак себя ограничивать в синтаксисе, так что фрагменты могут быть немного противоречивыми и несамосогласованными.
Переписанный генератор простых чисел:
Код:
sieve(set)
{
s=set;
p=select s;
return [p]+sieve([x in s : x%p]);
}
primes() {return sieve([x in N : x>=2]);}
Использование сопоставления с образцом для написания игрушечного мета-
eval'а для теор-множественных выражений самого языка (ну совсем несерьезный пример :) ):
Код:
eval(expr)
{
switch(expr)
{
match a+b: return eval(a)+eval(b);
match a*b: return eval(a)*eval(b);
case []: return [];
match [a]: return [eval(a)];
match !a: return !eval(a);
default: panic();
}
}
test()
{
myset=random();
_assert(myset==eval(myset));
}
Преобразования частичного порядка в линейный:
Код:
domain(pairs) {return [x : (_, x) in pairs];}
linext(nodes, edges)
{
// Reverse the arcs.
arcs=[(x, y) : x,y in nodes, (y, x) in edges];
visited=[];
sources=nodes-domain(arcs); // Nodes with no incoming arcs.
temp=nodes;
while(temp) yield visit(select temp); // Start DFS.
visit(node) // Local function.
{
if(node in visited) return [];
visited+=[node];
temp=[x in nodes : (node, x) in arcs]; // Forward neighborhood.
while(temp) yield visit(select temp);
yield node;
}
}
Функция
linext() может быть напрямую использована в задачах планирования работ (cf. сетевые графики), для определения последовательности компиляции взаимозависимых файлов проекта (make), для разрешения зависимостей при компоновке объектного кода, &c.
Детектор гамильтоновых путей (использует
linext и воображаемую
cycles; в коде могут быть ошибки):
Код:
hamilton(dag)
{
if(cycles(dag)) return false; // Only DAGs are allowed.
sorted=[];
// Find linear extension of partial order "dag".
while(s=linext(dag.nodes, dag.arcs)) sorted+=[s];
return dag.arcs subset [(x, y) : x, y in sorted]; // Find a path.
}
Детекторы гамильтоновых путей относятся к
-hard задачам, но
hamilton() имеет не более чем
, т.е. полиномиальную сложность... Да, дайте мне миллион. Шутка. :)
Суммирование бесконечного ряда (вот это уже излишество; к тому же непонятно, как это реализовывать, ну разве что через сивольные
'ы как в cas'ах):
Код:
s=[sum([1/x : x in N])];
s+=[sum([1/x^2 : x in N])];
s+=[sum([(-1)^(x+1)/x : x in N])];
_assert(s==[INF, pi^2/6, ln(2)]);
Теор-графовая задача отыскания подмножества вершин, каждый элемент которого достижим из какого-нибудь элемента изначально заданного подмножества вершин (не знаю, есть ли для такой многоярусной окрестности подходящий термин).
Здесь видимо необходимы пояснения (вольное изложение примера из работы J.Cai et al.). Дан граф, дано подмножество
его вершин. Если мы начнем путешествия из каждого элемента
и будем помечать все посещенные вершины, то, когда этот процесс закончится, мы получим множество всех помеченных вершин
. Очевидно, что вершины из
в любом случае тоже будут помечены, т.е.
. Определим функцию
, которая для вершины
возвращает её окрестность, а именно множество вершин, в которые можно попасть из исходной ровно за один шаг. Пусть функцию
можно применять к множеству поточечно; тогда мы можем обозначить "выколотую окрестность" множества
как
, а саму окрестность как
.
Идея в том, что искомое решение
является неподвижной точкой
. Но
имеет и другие неподвижные точки, в том числе и тривиальные: пустое множество, стоки орграфа, все множество вершин. Поэтому к уравнению
надо добавить ограничения. Одно уже было описано, я имею ввиду
, ограничивающее решение снизу. Ограничение сверху дается через соображение о минимальности неподвижной точки (по включению).
Булеан вершин имеет структуру решетки. Введенная функция
монотонна в смысле
. Это позволяет применить теорему Тарского-Кнастера (где-то на форуме я уже о ней подробно писал), которая во-первых, утверждает существование минимальной неподвижной точки и, во-вторых, будучи конструктивной теоремой, дает рецепт нахождения такого решения путем итераций
.
Надо лишь суметь закодировать условие задачи в таком виде; вот например для задачи достижимости вершин это было проделано выше.
В-принципе, мы можем искать неподвижную точку "вручную":
Код:
leastfixed(f, w)
{
p=f(w);
return p==w?p:leastfixed(f,p);
}
Но может быть стоит придумать для такого кода какой-нибудь подходящий синтаксический сахар (нет, Y-комбинатор пока оставим для экспериментов :) ).
Думал, доделаю этот пример с достижимыми вершинами, но суть-то идеи и так ясна, а подходящую нотацию я так и не придумал. Необязательно обходится какой-нибудь встроенной функций-оператором
lfp, можно придумать что-нибудь посложнее/покрасивше.
Если это возможно, хотелось бы увидеть "живые" примеры парадоксов теории множеств. Но пока мозги/руки что-то не доходят, ленятся. :)
Насчет бесконечности. Из области формальных спецификаций ещё известен (т.е. лично мне-то он как раз-таки и неизвестен; и если есть какая-то информация -- буду рад услышать) язык rsl, в котором вроде бы есть поддержка мощности
.
Вопрос: Семантика
random()'а вроде-как соответствует операции
in. Нельзя ли в таком случае как-то определить эту функцию на самом S1? Здесь есть проблемы. Во первых, рассматривается принадлежность универсуму, как же его обозначить, set42 (кстати, ещё одно хорошее название для языка!)? :) Во-вторых, операция принадлежности, вроде как очень часто тянет за собой неявный квантор всеобщности, так что такой
random() может тривиально оказаться тождественной функцией. Здесь неплохо бы смотрелись обозначение универсума+
select. Но все-таки, вдруг как-то можно обойтись одной лишь операцией
in? Возможно, даже с небольшими изменениями в синтаксисе, лишь бы попробовать избавиться от "свышеданных" операторов/функций типа
select/
nextSet/
random. Какой-нибудь сахарок бы придумать для навешивания ограничения, запрещающего множеству быть не singleton'ом. Подумайте над этим, пожалуйста. В-принципе, функция
select() мне нравится, но хотелось бы иметь возможность определить её в терминах других конструкций S1. Намек-зацепка: сопоставление с образцом позволяет разбирать множества на составные части.
Вот не знаю, нужны ли в языке безымянные вызываемые функции, а если нужны то как их синтаксически оформлять, у нас же не используется pascal-like слово
function при определении функции; но и до
\x x^2 докатываться не хотелось бы. :)
Насчет
forall'а. Нужен или не нужен? Конструкция
forall(expr_over_x) body может быть сахаром над
s=[expr_over_x]; while(s) {x=select s; body}.
Хотелось бы придумать синтаксис для удобного конструирования множеств из результатов функций-генераторов (возвращающих результат по
yield).
Ещё есть беспокойство по поводу возможных конфликтов тернарного оператора
?: и бинарного
: из конструкторов множеств.
P.S.: Боюсь, что этот поток сознания вообще нечитабелен... :) Если бы мысли были более конкретными, то можно было бы их оформить в виде TODO-списка, а так...
P.P.S.: Ещё один вариант названия для S1 -- infty (есть правда как минимум один кофликтующий по названию проект, но вроде-бы пока занять это название ещё можно; да и скобки квадратные все-таки подрисуем -- к логотипу). :) Вообще, надо выписать внушительнй список названий, проверить конфликты в поисковике, ну и выбрать наиболее подходящее. Вы знаете как бренд Java придумывался? Поинтересуйтесь.