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)

«Доктор, что мне делать: мои друзья или тупые, или сумасшедшие. Я им несколько раз продемонстрировал, что зажигалки неэффективны: сначала я вскипятил воду зажигалкой, а затем кипятильником. Кипятильником получилось гораздо быстрее. Но они всё равно продолжают прикуривать от зажигалки».

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

Как сущность — из них состоят все списки. Как функция — чтобы составлять новый список добавляя элемент к существующему. Например, (Учу плохому — рекурсия без TCO)

(defun range (num)
  (case num
    ((1) (list 1))
    (t (cons num (range (1- num))))))
> (range 10)

(10 9 8 7 6 5 4 3 2 1)

Или ещё, чтобы конструировать alist’ы, которые технически могут быть и списками списков, но традиционно их делают списками cons’ов.

(setf v '((:one . 1) (:two . 2) (:three . 3)))
(push (cons :four 4) v)
Gentooshnik ★★★★★
()

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

К случайному нет. К нужному тебе никто не мешает иметь отдельную ссылку. В т.ч. на последний элемент. Кстати что такое «конец» для дерева?

JS на световые годы опережает Lisp со своим JSON

Какая связь вообще? Сons это абстракция элемента памяти, json сериализованный объект. В ЛИСПе внезапно можно обрабатывать json.

Она медленная и неудобная

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

(defun main (&optional (n 99999))
  (declare (optimize (speed 3)))
  (let* ((list (loop for i from 0 to n collect i))
        (sum (loop for i in list sum i)))
    (format t "~d~%" sum)))
no-such-file ★★★★★
()
Последнее исправление: no-such-file (всего исправлений: 2)

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

neumond
()

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

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

а без ребер ты из одних атомов ничего не построишь.

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

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

Для добавления ... к нужному тебе никто не мешает иметь отдельную ссылку.

Но только если таких элементов пара штучек, иначе опять тормоза. Нельзя же сделать (setf fast_index '(link link)), и обращаться по индексу.

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

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

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

Какая связь вообще? Сons это абстракция элемента памяти, json сериализованный объект. В ЛИСПе внезапно можно обрабатывать json.

Если обрабатывтаь их на cons, то это будет ужасно медленно по сравнению с JSON. Cons это строительный блок лиспа, в JS строительным блоком я бы назвал JSON, потому что там нету более общего блока.

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

Так cons сами лишние при таком подходе, есть же замыкания, вот пример из Wikipedia, реализация cons через функции, на CL и других лиспах тоже можно повторить.

(define (cons x y)
  (lambda (m) (m x y)))

(define (car z)
  (z (lambda (p q) p)))

(define (cdr z)
  (z (lambda (p q) q)))

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

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

Так почему же cons из языка-ассемблера IPL 1956 года считается важным?

Кто это сказал? Нет, не считается. Списки (в виде тех же std::list, std::slist в плюсах, std::collections::LinkedList в rust) в современном программировании используются чуть менее чем никогда. Как, на самом деле, и другие ссылочные структуры, как-то бинарные деревья - в качестве kv контейнера почти всегда лучше HashMap, да и в качестве сортированного часто лучше взять вектор и от/до сортировать его когда нужно. Bнутри хэшмап также цепочки коллизий на списках повсеместно заменили на открытую адресацию. Всё потому что ссылочные структуры неэффективны по памяти и производительности, во многом благодаря ужасающей cache-unfriendliness.

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

Странно если бы по _L_isp не распространились бы списки. Но в свете вышесказанного язык основанный на ссылочных структурах сегодня неактуален.

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

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

да лана. ты считаешь, что все работает как в си++.

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

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

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

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

говорю сразу, что на лиспе я не пишу, просто знаю как его реализуют.

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

Вот тебе пары в JS, выбирай эта {head, tail} или может такую [head, tail]? Какой смысл в паре как отдельной структуре, когда есть динамический массив или словарь который ее прекрасно представляет?

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

и других размеров выделяемой памяти нет

Есть хоть одна реализация которая реализует строки на cons?

сборщик мусора(с перемещением) переносит занятые 64-бит блоки в начало памяти, таким образом обеспечивая высокий уровень кеш хитов.

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

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

Вот тебе пары в JS, выбирай эта {head, tail} или может такую [head, tail]?

на уровне байтов это что? в лиспе это просто массив из двух указателей. ну или запись из двух полей - указателей: car и cdr.

аналог в си это

struct {
  Atom* car;
  Atom* cdr;
}

ну попробуй записать проще.

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

ну попробуй записать проще

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

struct { int count; Atom **item; }
Что проще?

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

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

В Lisp за 70 лет не осилили сделать такую оптимизацию для cons, и выкинуть vector?

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

Есть хоть одна реализация которая реализует строки на cons?

cons - это функция, что создает cons-ячейку. не путай там.

есть конечно. просто строка разбивается на блоки по 64 бита и из них строится список.

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

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

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

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

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

Все что реализуется на парах, можно реализовать на массивах. Так как массив из двух элементов это тоже самое что и пара.

на массивах ты не сделаешь нормального представления динамически меняющихся данных

Да ладно, делают сложнейшие вещи на массиве char[], посмотреть можно в /dev/mem.

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

Что проще?

А теперь покажи, как на твоём списке будет

(defun filter (p list)
  (cond
    ((null list) list)
    ((p (car list))
     (let ((tail (filter p (cdr list)))
        (if (eq tail (cdr list)) 
          ; если хвост после отбора не изменился, возвращаем сам список
          list 
          (cons (car list) tail)))
    (t (filter p (cdr list)))))
monk ★★★★★
()
Ответ на: комментарий от monk

Для меня это китайская грамота. Напиши просто текстом, что именно тебе нужно объяснить.

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

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

В Lisp за 70 лет не осилили сделать такую оптимизацию для cons, и выкинуть vector?

Я повторю вопрос, а где вообще сейчас юзают лисп? Вот у меня на нем емаксовый конфиг написан и вроде сам емакс (но это неточно). А из живого/актуального? CAS maxima еще вроде бы.

Если ЯП скорее мертв чем жив последние дцать лет, то о каких оптимизациях может идти речь? Сохранить бы в рабочем состоянии то что есть…

Даже главный лиспер всея лора Лавсан с лиспа на шарпы мигрировал.

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

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

Так и есть. Страуструп еще писал: даже если вам нужна вставка, попробуйте сначала использовать вектор, на малом количестве элементов он может быть быстрее.

Я повторю вопрос, а где вообще сейчас юзают лисп?

Лисперы постоянно упоминают какие то два-три проекта, первый это переводчик какой то, второй это спутник NASA запущенный 25 лет назад, а третий возможно Emacs, уже не помню.

Если ЯП скорее мертв чем жив последние дцать лет, то о каких оптимизациях может идти речь?

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

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

Имя? Где так сделано? SBCL? CLISP?

я меня плохая память на имена. зато хорошая на красивые решения. тебе показали решение. по решению возражения есть?

еще раз. лисп машина может быть реализована просто и эффективно и по использованию памяти и по кеш хитам.

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

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

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

я меня плохая память на имена. зато хорошая на красивые решения. тебе показали решение. по решению возражения есть?

Такой реализации нету (используемой, разве что студент написал), потому что она была бы невыносимо медленной. Это вообще традиция реализовывать отдельно массивы в lisp, как я уже писал, они были даже в перволиспе. Строки тоже идут отдельно.

еще раз. лисп машина может быть реализована просто и эффективно и по использованию памяти и по кеш хитам.

Нет, из за того что ты не можешь обращаться к cons внутри списка или графа за разумное время, никакой речи про кеши итд вести нельзя, сначала нужно что бы они не сливали В 7_406_000 РАЗ. Никакое уплотнение не спасет лисп. Нужна оптимизация на представление cons-списков в виде массива, и alist в виде хеш-таблицы, и много чего еще. Тогда у него будет возможность потягаться с реальными языками программирования, такими как Python, JS.

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

В моем посте, даже наивная реализация с realloc на каждые 8 байтов обгоняет Lisp В 7_406_000 РАЗ. Заканчивай с этими сказками.

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

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

Все что реализуется на парах, можно реализовать на массивах. Так как массив из двух элементов это тоже самое что и пара.

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

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

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

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

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

Ну если он разбирается во внутренностях, то ответит lovesan, еще вроде во внутренностях копался den73.

Лучше всего я описал вопрос тут: Lisp: Где применимы cons? (комментарий)

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

массиве, если его сжать или разжать - указатели слетят.

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

Это вообще не списочный тип данных - а набор однотипных элементов

Если он хранит ссылки на Atom, то там какие угодно элементы могут быть.

массив плохо пригоден для удаления/вставки элементов, а только для добавления в конец

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

без дырок

Дырок и в списке нету. Не может быть у тебя в середине (nil . nil), только (nil . cdr), что выражается в терминах Atom** массива как NULL по N индексу.

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

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

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

Узкие места от сценариев использования зависят.

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

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

нарисуй на массивах своих два списка с разным началом, но одним хвостом.

на конс ячейках это тривиально(собсно для того они и сделаны).

или один список есть хвост другого.

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

Вот тебе удочка, а дальше ты сам:

function cons(car, cdr) { return [car, cdr] }
function car(cons) { return cons[0] }
function cdr(cons) { return cons[1] }

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

На обезьяннем это будет что-то типа:

function filter($p, $list) {
    if (empty($list)) {
        return $list;
    }

    $head = array_shift($list);
    $tail = filter($p, $list);

    if ($p($head)) {
        array_unshift($tail, $head);
    }

    return $tail;
}

используется вот так:

function is_even($n) {
    return $n % 2 == 0;
}

$list = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
$filtered_list = filter('is_even', $list);
print_r($filtered_list);
/**
Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
)
**/
Obezyan
()
Ответ на: комментарий от MOPKOBKA

фигня какая-то.

у тебя список - это массив указателей. (без всяких там конс ячеек)

с конс ячейками ты можешь построить линейный список B и приделать ему хвост - уже имеющийся список A.

а потом построить список С и приделать ему хвост - тоже список А. естессно это не копирование A, и просто указатель на конс-ячейку - голову A.

итак у тебя два списка - B и C, со своими началами, но концом - A.

как ты это будешь делать на массивах-то? без конс-ячеек?

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

и да, настоящий обезъян может просто

function is_even($n) {
    return $n % 2 == 0;
}

$list = array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

print_r(array_filter($list, "is_even"));
/**
Array
(
    [0] => 2
    [1] => 4
    [2] => 6
    [3] => 8
    [4] => 10
)
**/
Obezyan
()
Последнее исправление: Obezyan (всего исправлений: 2)
Ответ на: комментарий от alysnix

разбьясняю идею

на конс-ячейках строятся такие структуры

a -> b -> c -> d
1 -> 2 /

это список a b c d и список 1 2 c d. и они связаны семантически - у них одинаковый хвост. если его дополнить (…c d e f), это будет дополнение для обоих списков.

и будут списки

a b c d e f
1 2 c d e f
``

сделай это на массивах, без костылей.
alysnix ★★★
()
Последнее исправление: alysnix (всего исправлений: 1)
Ответ на: комментарий от MOPKOBKA

Функция filter. Два аргумента: функция p и список.

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

На Си примерно так:

list* filter(bool (*p(int elem)), list *list)
{
  if(list = NULL) return NULL;
  if(p(list.car)) 
  {
    list *tail = filter(p, list->cdr);
    if(tail == list->cdr) return list;
    return cons(list->car, tail);
  }
  return filter(p, list->cdr);
}
monk ★★★★★
()
Ответ на: комментарий от MOPKOBKA

Оптимизация хорошо, там, где семантика совпадает.

Например:

list *find(list *l, elem p)
{
  if(l == NULL) return NULL;
  if(p(l->car)) return l;
  return find(l->cdr, p);
}

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

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

В моем посте, даже наивная реализация с realloc на каждые 8 байтов обгоняет Lisp В 7_406_000 РАЗ. Заканчивай с этими сказками.

Потому что она делает совершенно другое. append = скопировать переданные в списки в новый список из этих элементов. Добавь malloc + memcpy как аналог append и сравнивай.

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

Я так на питоне первый парсер сделал жутко медленным. По опыту лиспа и си предполагал, что взятие подстроки s[i:] должно быть быстрым (это ведь всего лишь указатель на хвост строки!). Ан нет, оказалось, что эта операция копируют весь хвост строки. И вычислительная сложность парсера внезапно стала из линейной квадратичной.

monk ★★★★★
()