LINUX.ORG.RU

Lisp: Где применимы cons?

 cons,


1

6

Для начала небольшой «бенчмарк», С без всех оптимизаций в 7406000 раз быстрее.

(defun main ()
  (declare (optimize (speed 3)))
  (let ((n 99999) (l '()) (sum 0))
    (loop for i from 0 to n
	  do (setq l (append l (list i))))
    (setq sum 0)
    (loop for i from 0 to n
	  do (setq sum (+ sum (nth i l))))
    (format t "~d~%" sum)))
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
	long n = 99999, *l = NULL, count = 0, sum = 0;
	for (long i = 0; i <= n; i++) {
		l = realloc(l, (count + 1) * sizeof(long)); 
		l[count++] = i;
	}
	for (long i = n; i >= 0; i--) sum += l[i];
	printf("%ld\n", sum);
}

Для чего же нужны cons? В качестве универсального строительного блока, я считаю это одна из самых худших структур. Все ее преимущества заканчиваются на быстром добавлении в начало. Добавление в конец уже нежелательно, разрез в произвольном месте тоже, так как нету даже быстрого доступа к случайному элементу. Она медленная и неудобная, можно придумать кучу более быстрых и удобных структур. Даже JS на световые годы опережает Lisp со своим JSON, и его частое использование лишь подтверждает его удобство.

Так почему же cons из языка-ассемблера IPL 1956 года считается важным? Да, это неплохая структура для AST, если ваша машина имеет 16 кб памяти, но она распространилась по языку слишком широко.

★★★★★

Последнее исправление: MOPKOBKA (всего исправлений: 3)
Ответ на: комментарий от qulinxao3

и из бинарных получаем произвольно-арные

Так в нём не все операции бинарные. Что делать с dup и rot? Вот в вышеприведённом примере со скобками что будет, если кто-то напишет

( + ( rot 1 2 3 ) 5 )

?

monk ★★★★★
()
Ответ на: комментарий от qulinxao3

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

Есть Factor как форт с типизацией. Но там уже о простоте реализации речь не идёт.

monk ★★★★★
()
Ответ на: комментарий от monk

имхо форт это мульти(2 и более) стек - остальное частности реализации :)

т.е явный доступ для прикладного прога и разделение стека аргов и стека извратов

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

по dup rot и как удобных(иза практической частотсности) частных случаев npeek и комбинации npeek ndrop - это частности реализации Мура и комитета - у самого НебоЖителя как до так и после более продуктивные форты

первый форт был ваще на строках :)

qulinxao3 ★☆
()
Ответ на: комментарий от monk

Без отношения к вашей дискуссии о cons: у меня глаз дернулся от примера. Где столько раз нужно добавлять элементы в начало списка? Обычно это довольно редкие единичные случаи, пример как мне кажется уж слишком синтетический. Просто потому что при такой ситуации массив разворачивают в конце и это работает в ~400К раз быстрее:

$l = array();
for ($n = 0; $n <= 99999; $n++) {
    $l[] = $n;
}
$l = array_reverse($l);
print(array_sum($l));
//4999950000

Result for PHP v8.2.20 Execution time: 0.005472s Mem: 2439KB

Obezyan
()
Ответ на: комментарий от Obezyan

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

Он менее синтетический, чем тот, который в заголовке темы.

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от Obezyan

Где столько раз нужно добавлять элементы в начало списка?

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

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

monk ★★★★★
()
Ответ на: комментарий от monk

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

Obezyan
()
Ответ на: комментарий от Obezyan

Просто потому что при такой ситуации массив разворачивают в конце и это работает в ~400К раз быстрее.

Чистые (иммутабельные) списки разворачивают, но в CL и это не всегда обязательно, можно делать как в Си/Паскале/..., сохраняя указатель на последнюю cons-ячейку и мутируя её хвост:

(defun main ()
  (declare (optimize (speed 3)))
  (let* ((n    99999)
         (l   '(0))
         (last  l ))

    (loop :for i :from 1 :to n :do
      (let ((tail (list i)))
        (nconc last tail)
        (setq  last tail)))
    
    (let ((sum (loop :for x :in l :summing x)))
      (format t "~a~%" sum))))
CL-USER 4 > (time (main))
Timing the evaluation of (MAIN)
4999950000

User time    =        0.000
System time  =        0.000
Elapsed time =        0.002
Allocation   = 1617424 bytes
0 Page faults
GC time      =        0.000
NIL

CL-USER 5 > 
korvin_ ★★★★★
()
Ответ на: комментарий от monk

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

Ты недавно жаловался на duff device, и всякие ручные оптимизации, по моему это тоже что и разворот цикла.

Странно что такой, по идее высокоуровневый язык, обязывает программиста задумываться о том что доступ к регистру car быстрее чем append. А в том же PHP массивы трансформируют свою внутреннюю структуру незаметно для программиста, и наверное большинство его использующих вообще не задумываются куда им добавлять элемент, в начало или конец. И ничего не тормозит.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от monk

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

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от monk

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

Сделать его «безопасным», в том смысле о котором ты говоришь легко. Добавить сохранение и проверку количества входа/выхода, добавить указание типа либо в указатель на стеке, либо в сами структуры.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от anonymous

Про многое уже читал. Вот у Lisp и MUMPS одна проблема, они выбрали одну структуру, и даже не хотят ее превратить в чистую абстракцию. В MUMPS это оправдано сферой применения еще, не очень приятно если в середине процесса часть дерева начнет превращаться в массив, а вот в Lisp, это выглядит странно. Как будто максимум это закостыливание вектора сбоку.

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

- Массив || Отсортировнное множество (atom, i8, i32, i64/f64)

- Хеш-карта

- Разреженный ... ?тензор? (матрица, 3д матрица, 4д матрица, ...) с быстрой операцией transpose (перестановкой row[], col[]), что бы быстрым было и получение значений по ключу, и ключей по значению, как и в PHP операции над значениями будут сохранять вышестоящие значения/ключи, такое сохранение «контекста» полезно, странно что мало где есть

Для облегчения задачи внешне все cons ячейки будут представленны как неизменяемые, для исключения проблемы с отслеживанием изменения середины длинного списка. Ну и базовые методы будут поддерживать ветки для всех структур: map, filter, reduce ...

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от MOPKOBKA

а вот в Lisp, это выглядит странно. Как будто максимум это закостыливание вектора сбоку.

c помощью cons строятся графы в общем смысле, а список - лишь частный случай графа.

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

вот бинарное дерево а не список -

cons ( cons (a,b), cons (c,d) ) 
alysnix ★★★
()
Ответ на: комментарий от alysnix

Однородный массив это у тебя в С++, хотя и там можно делать забавные вещи Как создать массив в С++

На JavaScript массивы тоже способны на такие структуры, вот рабочий пример:

const g = [['a', 2], ['c', 2], ['d', ['e', 'f']]];

В относительно популярном Racket Lisp как я прочел, зациклить ячейку вообще нельзя классическим способом, они там иммутабельные, поэтому ты не можешь установить cdr на первый cons, нету функции установки, и так же соединение двух голов к одному хвосту неотличимо от соединения массивов из за иммутабельности. Но при этом тормоза списка в наличии. Им бы нормально массивы на срезах с перевернутыми индексами подошли.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от monk

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

Вполне возможно что тебе случайный доступ не нужен, я вот привык использовать три поля в for и индексы по максимуму. Однако во всех современных языках есть словари, они имеют огромную популярность. Хорошо ли работает alist?

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

На JavaScript массивы тоже способны на такие структуры, вот рабочий пример:

причем тут жаваскрипт. там такие «массивы» просто внутри как списки или вроде того. «массивы» это просто название, а квадратные скобочки - синтаксический сахар

В относительно популярном Racket Lisp как я прочел, зациклить ячейку вообще нельзя классическим способом, они там иммутабельные

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

alysnix ★★★
()
Ответ на: комментарий от MOPKOBKA

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

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

alysnix ★★★
()
Ответ на: комментарий от alysnix

причем тут жаваскрипт. там такие «массивы» просто внутри как списки или вроде того. «массивы» это просто название, а квадратные скобочки - синтаксический сахар

Нету там списков, я тебе уже показывал как они выглядят.

struct Array {
 int count;
 struct Atom **item;
}

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

Там можно строить графы, наверное это просто делается функциональным способом (скорее всего есть заумный инструмент для них). Вообще есть много способов строить графы, некоторые выбирают линейную алгебру https://graphblas.org/, такой подход по всей видимости популярен и в Rust где есть сложности с графами на структурах.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

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

плюсы лисповых списков. вставка/удаление в текущее место - O(1) (у массива - O(n)).

то есть списки выигрывают в ставке/удалении (и потому должны быть мутабельными, иначе - зачем оно?), но проигрывают в индексации.

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

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

молодежь выбирает лисп, короче!

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 3)
Ответ на: комментарий от alysnix

массивы в жаваскрипте - это те же списки, но указатели засунуты в массив

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

плюсы лисповых списков. вставка/удаление в текущее место - O(1) (у массива - O(n)).

Однако для такой вставки надо переделать половину стандартных функций, уже выше обсуждали. Давай лучше проверим, как JS выполняет вставку в начало массива, и сравним с кодом на лиспе.

const n = 99999;
const l = [];
for (let i = 0; i <= n; i++)
	l.unshift(i); // Вставка в начало
let sum = 0;
for (let i = 0; i <= n; i++)
	sum += l[i];
console.log(sum);
Упс! Код выполнился 40 раз быстрее чем Lisp.
<?php
$n = 99999;
$l = [];
for ($i = 0; $i <= $n; $i++)
	// По индексу 0 значение 99999, по индексу 99999 значение 0
	$l[$n - $i] = $i; 
$sum = 0;
for ($i = 0; $i <= $n; $i++)
	$sum += $l[$i];
echo $sum, "\n";
На PHP код выполнился вообще всего лишь в 10 раз медленнее чем в С.

и потому (списки) должны быть мутабельными, иначе - зачем оно?

Как раз в функциональщине их любят, вот был у тебя список, как добавить новый элемент без изменения? Создаешь новый, с концом указывающим на старый.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от monk

Я кстати понял почему ты пример переписал, если оставить мой, то он все равно выигравает у твоего в два раза, который написан идеально для cons.

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

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 2)
Ответ на: комментарий от monk

А как менять глубоко вложенные структуры в ФП? В С, JS, Python можно написать:

planet.state.town.house.name = "123";
А в Racket?

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

Для меня слишком сложна мысль о том что списки это массивы, а массивы это списки.

берешь список размером в N элементов, и непрерывную область памяти размером N указателей.

аккуратно, пинцетом, друг за другом копируешь поле «next» элементов списка в i-элемент массива, и инкрементируешь i.

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

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

короче все это и крякает и ходит примерно одинаково и примерно как утка.

alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

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

Привычка к Си.

Однако во всех современных языках есть словари, они имеют огромную популярность. Хорошо ли работает alist?

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

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA
monk@veles:~/tmp$ cat > test.js
const n = 99999;
const l = [];
for (let i = 0; i <= n; i++)
        l.unshift(i); // Вставка в начало
let sum = 0;
for (let i = 0; i <= n; i++)
        sum += l[i];
console.log(sum);
monk@veles:~/tmp$ nodejs test.js
4999950000
monk@veles:~/tmp$ time nodejs test.js
4999950000

real    0m2,606s
user    0m2,578s
sys     0m0,036s

Напомню, что на лиспе было 0,004 секунды.

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Количество значений всё равно не определить.

2 3 4 f g

Как узнать, 2 ушло аргументом к f или к g?

(g (f 4 3) 2)
(g (f 4 3 2))
(g (f 4) 3 2)

А в лиспе однозначно видно, какой вариант пытается выполнится.

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Сделать его «безопасным», в том смысле о котором ты говоришь легко. Добавить сохранение и проверку количества входа/выхода, добавить указание типа либо в указатель на стеке, либо в сами структуры.

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

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

У меня ровно столько же:

monk@veles:~/tmp$ cat > test.c
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
        long n = 99999, *l = NULL, count = 0, sum = 0;
        for (long i = 0; i <= n; i++) {
                l = realloc(l, (count + 1) * sizeof(long));
                l[count++] = i;
        }
        for (long i = n; i >= 0; i--) sum += l[i];
        printf("%ld\n", sum);
}
monk@veles:~/tmp$ gcc -O3 test.c
monk@veles:~/tmp$ time ./a.out
4999950000

real    0m0,004s
user    0m0,000s
sys     0m0,004s

gcc version 14.2.0 (Debian 14.2.0-12)

SBCL 2.5.0.debian

monk ★★★★★
()
Ответ на: комментарий от alysnix

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

Там есть изменяемые списки mlist и есть возможность зацикливать неизменяемые такой конструкцией:

(shared ([a (cons 1 b)]
         [b (cons 2 a)])
  a)

#0='(1 2 . #0#)
monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

так же соединение двух голов к одному хвосту неотличимо от соединения массивов из за иммутабельности

Зато работает на порядок быстрее.

Но при этом тормоза списка в наличии.

b = [1] + a
c = [2] + a

на списках на порядок быстрее, чем на массивах.

Им бы нормально массивы на срезах с перевернутыми индексами подошли.

Вышеприведённый пример на них всё равно будет адски тормозить.

monk ★★★★★
()
Ответ на: комментарий от Obezyan

С обеих сторон идет попытка навязать свою среду обитания так сказать

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

no-such-file ★★★★★
()
Ответ на: комментарий от monk

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

Если и ключи маленькие, то речь скорее о сотнях.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от monk

Количество значений всё равно не определить.

Они определенны в сигнатуре слова. Определяется кол-во входов и выходов.

MOPKOBKA ★★★★★
() автор топика
Последнее исправление: MOPKOBKA (всего исправлений: 1)
Ответ на: комментарий от monk

так же соединение двух голов к одному хвосту неотличимо от соединения массивов из за иммутабельности

Зато работает на порядок быстрее.

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

b = [1] + a

на списках на порядок быстрее, чем на массивах.

Выше описал.

Вышеприведённый пример на них всё равно будет адски тормозить.

Не будет.

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от MOPKOBKA

Они определенны в сигнатуре слова. Определяется кол-во входов и выходов.

a b ?dup +

Что попало в +? a и b или два раза b?

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

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

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от MOPKOBKA

Чем проще? Если нужен синтаксис со скобками, проще сразу писать лисп. Сила форта именно в том, что каждое слово само определяет свой контекст. Это позволяет писать очень лаконичные программы. Но требует от читателя этой программы как минимум знания всех слов.

Если в лиспе написано (+ (f 5) (g 5)), то можно не знать, ничего про f и g, но понять структуру программы. В форте 5 f 5 g + может подразумевать что угодно.

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

monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

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

Можешь ( a -- a a | 0 )

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

Если предлагаешь запретить сигнатуры с переменным количеством результатов, то if в этом языке тоже не cуществует?

monk ★★★★★
()
Ответ на: комментарий от monk

Если нужен синтаксис со скобками, проще сразу писать лисп.

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

( + 4 5 6 7 )

В форте 5 f 5 g + может подразумевать что угодно ... требует от читателя этой программы как минимум знания всех слов.

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

MOPKOBKA ★★★★★
() автор топика
Ответ на: комментарий от monk

Можешь ( a — a a | 0 )

Ненужная сложность.

Если предлагаешь запретить сигнатуры с переменным количеством результатов, то if в этом языке тоже не cуществует?

Почему? if проверяет аргумент (и даже возможно его не удаляет в некоторых версиях Forth), после чего выбирает куда сделать прыжок. Где тут противоречие?

MOPKOBKA ★★★★★
() автор топика