avva: (Default)
[personal profile] avva
(интересно будет только программистам)

Продолжение наброска.

У наброска появилась страница, на которой можно смотреть на весь код, как на текущее состояние, так и
на прошлые припадки (теги fit-1, fit-2 итд.).

-----<<<<<< предыдущая часть, там контекст и начало работы---

План работы (возможно, изменится): к концу этого припадка я хочу иметь возможность сказать (+ 3 5) и (car '(1 2)) и (set! y 13) и (+ y 22). Часть кода, который это воплотит, будет впоследствии переписана заново для поддержки лексической видимости и хвостовой рекурсии, которой сейчас еще не будет. Тот код, который предназначен на выброс, я напишу как можно проще и быстрее.

----------------------tutorial----------------------
Идентификаторы, т.е. имена переменных и функций, в Схеме называются символы. Символы указывают на значения в результате процесса, который называется binding и подробнее будет описан позже. Пока что тривиальный пример: (define y 4) определяет новую переменную y и присваивает ей значение 4. Если бы в Схеме, как в некоторых функциональных языках, были неизменяемые значения (immutable values), то больше ничего и не надо было говорить, но это не так. В Схеме на самом деле переменная связана не с значением, а с "ячейкой" (location), в которой это значение сидит, и можно его изменить. Если я выполню (set! y 5), то теперь значение y будет 5. Если напишу (set! y "214"), то y станет строкой, и так далее.

Результат выполнения символа - его значение. Поэтому, например, (+ y 10) дает результат 14, если y было со значением 4. Как я объяснил в прошлый раз: выполняются все значения в списке, первое из них принимается за функцию, которую надо вызвать, остальные - за ее аргументы, функция вызывается и то, что она возвращает, есть результат выполнения списка. В данном примере 10 выполняется в 10, y выполняется в 4, символ + выполняется в встроенную функцию сложения, она получает 10 и 4 и возвращает 14.

В прошлый раз я сказал, что аргументы всегда выполняются до вызова функции. Но это была ложь, или точнее недосказание, потому что когда выполняется список типа (foo bar1 bar2 bar3), символ foo необязательно выполняется в функцию. Ведь если бы это было так, как бы могло работать (set! y 5) выше? еще до вызова "функции" set! y заменилось бы на свое значение 4, и set! получила бы 4 и 5, и не смогла бы изменить y. То, что происходит на самом деле это вот что: set! это не функция, а пример 'специальной формы', встроенной в язык. Когда Схема распознает, что первое значение в списке - символ, указывающий на специальную форму, а не просто функцию, то она не выполняет остальные аргументы y и 4, а передает их как есть, без выполнения, и форма каким-то специальным образом что-то с ними делает. Например, set! дает переменной y новое значение, а (define y 4), показанное выше - тоже специальная форма - определяет новую переменную.

Еще один пример 'специальной формы' - "if": если я напишу (if x1 x2 x3), то это значит: "если x1 выполняется в истинное значение, то выполни x2 и верни его значение, иначе выполни x3 и верни его значение". Т.е. если x1 верно, я вообще хочу не выполнять x3, но если бы if была обычной функцией, это было бы невозможно.

Схема позволяет программисту создать свои новые варианты 'специальных форм', кроме встроенных в язык, таких как set!, define и if. Такие новые специальные формы называются 'макросы', о них позже.

Кроме того, что список является основным способом вызвать функцию в Схеме, он также является главной структурой данных в Схеме. Если у нас есть (foo 1 2 3), то выполнение этого списка вызовет функцию foo. А что если я просто хочу список (1 2 3) из трех чисел, чтобы работать с ним, как с данными? Ну например передать его функции car, чтобы она вернула первый элемент: 1. Если я напишу (car (1 2 3)), то будет ошибка: Схема попытается выполнить аргумент функции car, т.е. список (1 2 3), но 1 - не символ, не идентификатор функции. Как мне сказать Схеме, что я не хочу выполнять этот список, а просто хочу его передать?

Для этого придумали специальную форму quote: если я напишу (quote (1 2 3)), то результатом выполнения этого списка будет (1 2 3). Ошибки не будет, потому что quote - не обычная функция, а специальная форма, и не обязана выполнить свой аргумент (1 2 3), а может делать с ним что хочет. Она его просто возвращает. Так что мне надо написать:
(car (quote (1 2 3))), и это даст нужный результат: 1. Но так каждый раз писать очень неудобно, и поэтому придумали сокращение: вместо (quote (1 2 3)) можно написать '(1 2 3). Символ апострофа заменяет вызов quote; по сути дела он абсолютно идентичен этому вызову, и просто является синтаксическим сокращением для программиста. Но это действительно удобно: (car '(1 2 3)) написать легко. Так что каждый раз, как вы видите ' перед каким-то значением в Схеме, это означает: "процитируй" это значение, т.е. дай в качестве результата именно его, не пытаясь его выполнить.
Чаще всего это будет стоять перед списками. Еще один пример, когда удобно - перед символом. Значение foo при попытке выполнения пытается посмотреть, что записано в переменной foo, и вернуть это; а 'foo просто возвращает символ foo, как значение (переменной такой может и не быть вообще).

----------------------braindump---------------------------
[не связано напрямую с кодом, к-й я сейчас буду писать, но эти размышления вызваны им]

На данный момент я не очень понимаю, нужен ли мне еще один layer of indirection между переменными и значениями. Что-то я запутался тут. Значит так. У меня будет один layer of indirection - собственно bindings. Это понятно. Пусть у меня есть переменная y, и предположим для простоты, что она в top-level environment, ничем не скрыта итд. Тогда есть binding y->i, где i некий индекс, и в cells[i] лежит значение. Так я думал до сих пор, что будет. Используя язык стандарта R5RS, i это "location", а cells[i] "value".

Теперь предположим, что в cells[i] лежит 4, а я делаю (set! y "123"). Я не могу, конечно, взять и записать "123" в cells[i], где было до этого 4, потому что "123" занимает больше клеток, и они уже заняты другими значениями. Я предполагал до сих пор, что я сделаю следующее: создам новое значение "123" по какому-то адресу j, и изменю binding y->i на y->j. Строго говоря, это меняет значение "location". Но мне казалось, что это не мешает ничему. Теперь я как-то не уверен: что если какие-то eq? equal? итд. ожидают чего-то другого? В принципе я могу сделать по-другому. Я могу сделать так, чтобы y->k, где k это клетка нового типа T_PTR, указывающая на i - т.е. еще один layer of indirection. Теперь, если я делаю (set! y "123"), я меняю k->i на k->j, а binding y->k остается без изменений - "location" сохраняется. Но правильно ли будет так сделать?

У меня это как-то переплелось с путаницей насчет того, как собственно передаются аргументы функциям - по значению или по ссылке (by value or by reference). Ведь если по значению, то получается, что вызов (some-string-function string) всегда создаст новую копию string, чтобы ее передать, а если строки длинные, выходит, что работа с ними очень неэффективная. И зачем тогда нужна стандартная функция (string-set! str ind char), которая меняет строку str по индексу ind на знак char? Если я сделаю string-set! внутри функции foo, получившей str в виде аргумента, изменится ли то, что передал вызвавший foo, в его собственной строке, к-ю он передал для str?

Меня на самом деле смущает то, что я не могу найти ответ на эти вопросы в спецификации R5RS. Мне как-то очень нравилась ее краткость, а теперь что-то разонравилась. Или она специально оставляет это несказанным? OK, нашел одно упоминание в первой главе: "Arguments to Scheme procedures are always passed by value", но как-то это очень нелогично выглядит в свете существования мутирующих string-set! и set-car!. OK, устанавливаю plt-scheme, чтобы запустить его REPL и попробовать. Понадеюсь на то, что он хорошо воплощает R5RS.

Ага. Работа с plt-r5rs многое прояснила. Передача аргументов действительно по ссылке, а не по значению, совершенно точно в случае строк и пар (для остального это наверное не очень важно). Если определить функцию foo которая делает string-set! своему аргументу, и вызвать эту функцию (foo y), то значение y меняется. То же самое, если foo делает set-car! своему аргументу. А вот если foo делает set! своему аргументу, то на исходное значение это никак не влияет. То же самое происходит, если вместо функции просто сделать (define z y), и менять z: если с помощью set!, то y не меняется, если с помощью set-car!, string-set!, то меняется. Черт побери, почему в спеке нельзя было нормально это объяснить? Ведь я три раза его просмотрел именно в поисках этой информации.

OK, кажется, я распутал себя. Surprise surprise! мой первоначальный подход был верен. Пусть (set! y "123") меняет binding y->i на y->j, это правильно; если другая переменная z тоже указывала на cells[i], в результате (define z y) или передачи y функции с аргументом z, то значение z не изменится, и это правильно. С другой стороны, string-set! и set-car! не меняют y->i, а идут внутрь значения cells[i] и шуруют в нем; если z тоже указывала на cells[i], она получает изменения. И это тоже правильно, если верить plt-scheme - в стандарте я не могу это найти. Функции при этом могут все свои аргументы получать by reference в том смысле, что они получают индекс клетки i, и вставляют его в свои bindings. Дополнительный layer of indirection, кажется, не нужен вообще. Если я где-то неправ и вы это понимаете, поправьте меня.

-------------------code--------------------------
Меж тем пока что я хочу по-простому одну глобальную таблицу bindings, от символа к значению (к индексу значения). Потом я все это переделаю для поддержки lexical scope и closures, поэтому мне нужно что-то невыносимо тривиально и простое, чтоб не жалко было выкинуть. Возьму-ка я hash_map<string, uint32_t> из C++; вообще-то я хочу весь проект написать на C, но это временно.
[file symbols.cc] extern "C" uint32_t get_symbol(const char *name, int len); extern "C" void set_symbol(const char *name, int len, uint32_t val);
hash_map, говорите? Нет, прощай, дорогой hash_map, вперед, к светлым новым стандартам C++0x!
#include <tr1/unordered_map> tr1::unordered_map<string, uint32_t> table;
Я передаю этим функциям имя символа в виде указателя+длины, потому что так оно у меня хранится в памяти, без заканчивающего нуля. Надо как-то создать C++-ную строку из этого. К сожалению, у string нет подходящего конструктора, зато есть оператор присваивания. Вот вспомогательная функция и set_symbol:
string sym_name(const char* name, int len) { string str; str.assign(name, len); return str; } void set_symbol(const char *name, int len, uint32_t val) { table[sym_name(name, len)] = val; }
Гм, а как, интересно, get_symbol() сообщит мне, что не нашла символ? Есть два простых варианта: либо специальное значение 0, либо пусть вернет boolean, и дополнительный аргумент-указатель, куда посадить само значение. Однако у меня индекс 0 не специальный, он используется в cells[]! Требование простоты кода важнее: пусть get_symbol() возвращает 0, а первую клетку мы для такого случая зарезервируем; может, еще где пригодится:
uint64_t cells[MAX_CELLS]; /* start from 1, as index 0 is reserved for errors or invalid values */ uint32_t next_cell = 1;
OK, теперь пора разобраться с именами символов, которые мы будем передавать get_symbol(). В прошлый раз я их запихал по одному знаку в клетку, надо сжать. Ой, внезапно до меня доходит очевидное: ведь если строка/символ сидят по адресу cells[i], то их байты можно просто писать начиная с cells[i+1] как обычные байты, не надо никакого сложного кодирования (я во время прошлого припадка представлял себе отчего-то, что надо будет в цикле сдвигать на 0, 8, 16, 24, ... бит и вставлять в нужное место очередной клетки). Ха, функция pack_string() сильно упрощается. Остается решить вот что: в самой первой клетке строки/символа cells[i] есть неиспользованные верхние 32 бита, втискивать ли туда тоже начало строки? Кажется, мы благополучно приземлились на дилемму little endian/big endian: если архитектура little endian, то эти верхние 32 бита будут сразу перед следующей клеткой, и все просто; если big endian, то разрыв. Варианты: а) предположить little-endian и задокументировать это, все равно "почти все такие"; б) в случае big-endian хранить все флаги, тип итп. в /верхних/ 32 битах, а не нижних, создать набор макросов, к-й это скрывает; такое веселое вполне извращение! но в) все это слишком сложно, пожертвуем этими 32 битами и начнем со следующей клетки. Выбираю в).
#define SYMBOL_NAME(i) (char *)(cells+i+1) #define SYMBOL_LEN(i) ((cells[i] & 0xFFFFFFFF) >> 16) /* helper func to store a string into cells */ uint32_t pack_string(char *str, char *end, int type) { uint32_t len = (end-str+7)/8; CHECK_CELLS(len); uint32_t index = next_cell; uint64_t value = type | (uint64_t)len << 16; cells[next_cell++] = value; strncpy(SYMBOL_NAME(index), str, end-str); return index; }
Стоп, проблема: мне-то надо знать длину строки в байтах, а это сохраняет только в клетках! a) Варианты: a) можно в битах 31-15 вместо len хранить end-str, т.е. длину в байтах; это будет отличать строку/символ от других типов, и во время сборки мусора надо будет об этом не забыть. Еще это снижает максимальную длину строки до 64k, в то время как я рассчитывал на 512k. б) можно отдельно в битах 47-32 записать длину в байтах, благо я похерил планы использовать верхние 32 бита для чего-то стоящего в этих типах. Выбираю б). Соответственно меняю
#define SYMBOL_LEN(i) (cells[i] >> 32) ... uint64_t value = type | (uint64_t)len << 16 | (uint64_t)(end-str) << 32;
Кроме того, меняю соответствующий код в dump_value(). Запуск и проверка обнаруживают, что я забыл сделать "next_cell+=len" в функции выше; хорошо, что я в REPLе вставил распечатку текущего кол-ва клеток в подсказке командной строки.

OK, что дальше. Кажется, пришла пора писать функцию выполнения eval()! Мой первый eval() будет очень примитивный (улавливается закономерность?), потом я его переделаю. Списки он будет выполнять, вызывая себя рекурсивно; это очень просто написать, но не поддерживает хвостовую рекурсию, обязательную в Схеме. Что это такое и как ее поддержать, обсудим позже. Специальные формы define и set! будут специальными случаями в коде eval(), а вот встроенные функции типа + и car я хочу сделать уже общим способом.

eval() принимает индекс значения для выполнения и возвращает индекс результата. Вот начало функции, показывающее, что делать для примитивных значений, для списка, и специальный случай "define" внутри списка.
uint32_t eval(uint32_t index) { uint32_t sym, val, car, cdr; switch(TYPE(index)) { case T_EMPTY: case T_INT32: case T_BOOL: case T_STR: return index; case T_SYM: val = get_symbol(SYMBOL_NAME(index), SYMBOL_LEN(index)); if (val == 0) die("undefined symbol"); return val; case T_PAIR: car = CAR(index); cdr = CDR(index); if (TYPE(car) != T_SYM) die("car of eval'd pair is not a symbol"); if (TYPE(cdr) != T_PAIR) die("cdr of eval'd pair is not a pair"); if (strncmp(SYMBOL_NAME(car), "define", 6) == 0) { /* the only allowed syntax here is (define symbol value) */ sym = CAR(cdr); if (TYPE(sym) != T_SYM) die ("define followed by non-symbol"); if (TYPE(CDR(cdr)) != T_PAIR) die ("define symbol . smth"); val = CAR(CDR(cdr)); if (TYPE(CDR(CDR(cdr))) != T_EMPTY) die ("define symbol smth ???"); set_symbol(SYMBOL_NAME(sym), SYMBOL_LEN(sym), val); return val; /* TODO: actually undefined */ } break;
Случай set! почти идентичен этому, только проверяет, что символ уже существует; собственно, сделаю для них один код. Форма "quote" тоже очень простая - проверяет, что ей передали ровно одно значение, и возвращает его.

Пока хватит специальных форм, займемся функциями. Для функций мы заведем новый вид значения T_FUNC. Что он должен хранить - отдельный интересный вопрос. Если это функция, определенная программистом - то список параметров и собственно тело функции, т.е. два значения (тело функции - просто очередное значение). Кроме того, для поддержки замыканий (closures) надо будет еще добавить... но как именно это устроить, я еще не решил окончательно. Ну а если функция встроенная? Это отдельный тип T_BLTIN или тот же T_FUNC но с дополнительным флажком, который говорит "я встроенная функция"? Мне кажется, лучше с флажком, бит у нас еще много.
#define T_FUNC 7 /* function, a.k.a. closure */ #define BLTIN_MASK 16
Договоримся, что встроенные функции используют одну дополнительную клетку, в которой по-простому лежит указатель на C-функцию. Эта функция принимает и возвращает uint32_t, индексы значений, используя 0 для того, чтобы вернуть ошибку.

Встроенные функции регистрируют себя с помощью register_builtin():
typedef uint32_t (*builtin_t)(uint32_t); void register_builtin(char *name, builtin_t func) { CHECK_CELLS(2); uint64_t value = T_FUNC | BLTIN_MASK; uint32_t index = next_cell; cells[next_cell++] = value; cells[next_cell++] = (uint64_t)func; set_symbol(name, strlen(name), index); }
Для начала нам хватит car, cdr, и функция регистрации всех встроенных, которую вызовем из main(). Плюс кусочек кода в dump_value(), печатающий лаконичное *func* при встрече с функцией.

Встроенные функции вскоре переедут в свой собственный файл, подозреваю.
uint32_t car(uint32_t index) { if (TYPE(index) != T_PAIR) return 0; return CAR(index); } uint32_t cdr(uint32_t index) { if (TYPE(index) != T_PAIR) return 0; return CDR(index); } void register_builtins(void) { register_builtin("car", car); register_builtin("cdr", cdr); }
Возвращаемся в eval(). Предположим, мы посмотрели, на что ссылается символ, и увидели, что это функция, и даже встроенная, потому что другие мы запускать не умеем. Теперь нам надо выполнить все-все-все аргументы, которые нам даны в виде списка, и построить новый список - их значений. Как это сделать? Обойдемся без рекурсии и постулируем какой-то разумный предел - скажем, максимум 256 аргументов в одном вызове звучит нормально? Пусть одна вспомогательная функция пройдется по всем аргументам, выполнит их, и заполнит временный массив (индексов) значений, а потом с помощью другой вспомогательной функции make_pair() мы сделаем из него список.
/* special forms end here. Now check if it's a function. */ val = get_symbol(SYMBOL_NAME(car), SYMBOL_LEN(car)); if (val == 0) die("undefined symbol as func name"); if (TYPE(val) != T_FUNC) die("first element in list not a function"); if (cells[val] & BLTIN_MASK == 0) die("can only call builtins for now"); uint32_t args[MAX_ARGS]; uint32_t num_args; if (!eval_args(cdr, args, &num_args)) return 0; uint32_t list = make_list(args, num_args); builtin_t func = (builtin_t)cells[val+1]; /* well, there you go */ return func(list);
Опускаю код eval_args() и make_list() тут, см. исходники.

Проверка этого кода показывает, что в car() и cdr() выше есть баг, хоть они и кажутся очень простыми. Дело в том, что они получают напр. не (1 2 3), если вызвать (car (quote (1 2 3)), а ((1 2 3)), потому что они получают список всех своих аргументов: в данном случае аргумент один, но список все равно есть. Вот например код исправленной car:
uint32_t car(uint32_t index) { /* check that we have just one argument (the list is of size 1) */ if (TYPE(index) != T_PAIR || TYPE(CDR(index)) != T_EMPTY) return 0; /* pass to this argument - first CAR - and return its first element */ return CAR(CAR(index)); }
Заодно добавляю встроенную функцию +, которая работает только с 32-битными числами. Она умеет суммировать сколько угодно аргументов. Ее отлаживание обнаруживает баг в коде прошлого припадка, читающем специальные идентификаторы +,-,... . Исправляю.

Последнее, что хочется успеть сегодня - синтаксис '(1 2 3) вместо (quote (1 2 3)). Это просто. В случае, когда мы видим апостроф, мы просто читаем следующее значение, и руками составляем его в список вместе с символом quote. Вот код нового куска в read_value() (кроме этого, был небольшой рефакторинг make_list(), чтобы он сам () в конце добавлял для простоты):
if (*str == '\'') { str++; uint32_t indices[2]; int res = read_value(&str, &indices[1], 0); if (!res) return 0; char *quote = "quote"; indices[0] = pack_string(quote, quote+5, T_SYM); *pindex = make_list(indices, 2); *pstr = str; return 1; }


Проверка всего, что понаписано!
$ ./sketch 7 cells> (+ 3 5) 8 24 cells> (+ 3 10 -14) -1 46 cells> (car (quote (1 2 3))) 1 73 cells> (car '("foo" "bar")) "foo" 99 cells> (cdr '(1 2 3)) (2 3) 126 cells> (define foo 14) 14 138 cells> (+ foo -14) 0 156 cells> (set! foo 20) 20 168 cells> (+ foo -14) 6 186 cells> (set! foo "foofoofoo") "foofoofoo" 200 cells> (+ foo -14) eval failed. 217 cells>

На сегодня все. Проект вырос до 420 строк кода.
------------------------------конец второго припадка----------------------------------

Код проекта (главная линия, меняется)

код второго припадка (зафиксирован)
From: [identity profile] sko4.livejournal.com
я не_программист, но хочу верить , что смогла бы разобраться , что бы учится пользоваться....
могу спросить: позволительно ли сохранить собранный вами "лекбез"=кратко-толковой объяснение как на примере выше Вами приведенном -- можно перенести "конец второго припадка" в свой ЖЖ , конечно указав источник?
( я когда имею на бумаге что-то подобно написанное, из незнакомой мне сферы, ---а сейчас переписать ТЩАТЕЛЬНО не_представляется возможным) короче: НЕ УДАЛЯЙТЕ, пожалуйста, эту ..??... ,
From: [identity profile] avva.livejournal.com
Простите, не очень понял, что не удалять, но в любом случае удалять ничего не собираюсь. Перенести к себе тоже можно.
From: [identity profile] sko4.livejournal.com
спасибо , вот теперь мне наконец спокойно(тревога что то взятое на заметку потерять)- до связи
*уточняйте это явно в вопросе - запомните, пожалуйста, Вы пообещали не_удалять НИЧЕГО, пусть случай и любой:)
From: [identity profile] mkal.livejournal.com
По-моему вы с ботом общаетесь

Date: 2010-07-15 07:37 am (UTC)
From: [identity profile] amosk.livejournal.com
Надо как-то создать C++-ную строку из этого. К сожалению, у string нет подходящего конструктора, зато есть оператор присваивания

На самом деле такой конструктор есть (принимающий два итератора - begin и end):

const char* str = "12345";
string s(str, str + 2);
assert(s == "12");

Но это впрочем не важно, учитывая что код на выброс :)

Date: 2010-07-15 07:58 am (UTC)
From: [identity profile] avva.livejournal.com
Действительно. Спасибо в любом случае :)

Date: 2010-07-15 07:45 am (UTC)
From: [identity profile] gianthare.livejournal.com
eval для PAIR должен разрешать пустой список на месте CDR: (foo)

Date: 2010-07-16 11:51 pm (UTC)
From: [identity profile] avva.livejournal.com
И это исправил. На самом деле у меня был код в ридере, к-й читал (foo) как foo. Почему мне пришло в голову, что это правильно делать, теперь уже не могу понять, удалил его.

Date: 2010-07-15 07:51 am (UTC)
From: [identity profile] gianthare.livejournal.com
> Если это функция, определенная программистом - то список параметров и собственно тело функции, т.е. два значения
Можно одно значение - просто список с lambda на первом месте, собственно
(define ( ) )
равносильно
(define
(lambda () ))
как нам говорит стандарт
[А так как это Лисп-1 то определение функции и есть значение символа функции]

Date: 2010-07-15 07:57 am (UTC)
From: [identity profile] avva.livejournal.com
Да, но тогда чтобы что-то сделать с функцией, нужно будет всегда первым делом пойти внутрь этого списка, пропустить lambda, вытащить отдельно список аргументов и тело. Выходит крохотный трейдофф: лучше так, как ты говоришь, и хранить всего одно дополнительное значение для каждой функции, или лучше хранить два, и вызовы будут чуть-чуть быстрее? Поскольку функций "мало", лишней памяти на это не жалко. Скорее всего на performance это заметного эффекта не произведет, конечно...

Date: 2010-07-15 08:02 am (UTC)
From: [identity profile] gianthare.livejournal.com
на самом деле не принципиально, но посмотри следующий коммент

Date: 2010-07-15 07:59 am (UTC)
From: [identity profile] gianthare.livejournal.com
Ты разрешаешь только символ в кач-ве "имени" функции - это не по схемовски.
Там может быть
а) лямбда-выражени, т.е. анонимная функкция
б) вообще что угодно ((if (= 1 0) + -) 5 6)
короче, первый элемент списка тоже надо предварительно вычислить.

Date: 2010-07-15 08:07 am (UTC)
From: [identity profile] avva.livejournal.com
Да, большое спасибо, ты прав, причем это даже не тот случай, что "знаю, специально, позже исправлю". Это случай "помнил об этом, держал в уме, и в какой-то момент забыл". Видимо, потому, что я начал с специальных форм, а в этом случае как раз нельзя выполнять символ "define" или "quote", в моем подходе сейчас (в будущем они конечно будут символами с нормальными значениями). Исправлю сегодня позже.

Date: 2010-07-15 08:24 am (UTC)
From: [identity profile] gianthare.livejournal.com
> в будущем они конечно будут символами с нормальными значениями
И пометкой spec-form? В корне верно. Интересно, в Схеме можно сделать
(define my-define define) ?

Date: 2010-07-16 11:48 pm (UTC)
From: [identity profile] avva.livejournal.com
OK, исправил это. if у меня пока еще нет, но это показывает, что работает:

9 cells> (define a (list cdr 1))
(*func* 1)
37 cells> ((car a) '(3 4 5))
(4 5)

Date: 2010-07-15 08:06 am (UTC)
From: [identity profile] gianthare.livejournal.com
Я правильно понимаю, что у тебя парсер для каждого символа выделяет новую память, даже если такой символ уже существует (т.е., говоря по-лисповски ты им не делаешь intern)?

Date: 2010-07-15 08:10 am (UTC)
From: [identity profile] avva.livejournal.com
Да, но это временно. Когда окончательно решу, как представлять environments, и перепишу символьную таблицу под это, имена символов не будут больше храниться много раз.

Date: 2010-07-15 08:12 am (UTC)
From: [identity profile] jsn.livejournal.com
Не совсем понял, про что этот кусок с "call-by-value" vs "call-by-reference". Тут же всё совершенно как в перле: parameters are always passed by value, and values of variables are references. Т.е.: если бы там был by reference, то "set!" мог бы быть обычной функцией, а не special form.

Date: 2010-07-15 08:24 am (UTC)
From: [identity profile] avva.livejournal.com
В свою очередь не понимаю ваш комментарий. Я как раз пытаюсь сказать, что строки и пары передаются by reference, а не by value.

(string-set! foo 2 #\c)

string-set! это функция, не специальная форма. Получает ли она отдельную копию foo? Нет, после этого вызова меняется сама foo. Если вы считаете, что дело в том, что foo переменная, то вот это не должно выполняться, а оно выполняется:

(string-set! (string-append "12" "34") 2 #\c)

(просто результат уходит в никуда). Или я не понял вас?

Date: 2010-07-15 08:46 am (UTC)
From: [identity profile] jsn.livejournal.com
Похоже, что не понимаете. Value of "foo" is a reference to the string. This value is passed into "set-string!". Когда Вы меняете referenced object, разница не видна. Но когда Вы меняете собственно parameter variable, разница очевидна. Например...

(define *z* "aaa")

(define (bar x) (set! x 11))

(bar *z*)

(display *z*)

...при передаче by reference напечатал бы 11, а при передаче by value печатает "aaa". By reference бывает в C++, например, кажется, когда какие-то параметры задекларированы с амперсандом.

Date: 2010-07-15 10:43 am (UTC)
From: [identity profile] avva.livejournal.com
А, OK, теперь понял. Мы говорим об одном и том же, и я ожидал того результата от куска кода в вашем комментарии, о котором вы говорите. Вы используете более корректную терминологию, которую я успел забыть.
http://en.wikipedia.org/wiki/Evaluation_strategy

Т.е. схема, как Джава: все by value, но value есть handle/reference.

(правда, перл все-таки именно by reference! просто обычно @_ копируют в локальные переменные, и напрямую не меняют; выходит essentially by value).

Date: 2010-07-15 10:49 am (UTC)
From: [identity profile] jsn.livejournal.com
(правда, перл все-таки именно by reference! просто обычно @_ копируют в локальные переменные, и напрямую не меняют; выходит essentially by value).

вроде как нет, в perl всё совершенно так же by value:

jsn ~ # perl -le '$a = 1; sub f { $_[0] = 2 } ; print $a'
1

Date: 2010-07-15 10:54 am (UTC)
From: [identity profile] avva.livejournal.com
Вы забыли вызвать f($a) :-)

Date: 2010-07-15 01:39 pm (UTC)
From: [identity profile] jsn.livejournal.com
Ну, это детали -- эффект-то от этого не меняется :) [как старому perlgolfer-у мне вообще хотелось написать print 1]

Date: 2010-07-15 09:23 am (UTC)
From: [identity profile] cmm.livejournal.com
Я как раз пытаюсь сказать, что строки и пары передаются by reference, а не by value.

с точки зрения семантики все параметры передаются by value, просто эти самые values имеют reference semantics (excuse my Klatchian).  это наблюдаемо в случае mutable объектов (пар, строк, векторов), и никак не наблюдаемо в случае прочих (чисел, символов).

при реализации могли бы понадобиться заморочки с by reference/by value если бы спецификация требовала возможность мутировать объекты, разумнее всего реализуемые как immediate (небольшие целые числа, например).  к счастью, Схема ничего подобного не требует и посему заморачиваться тут совершенно незачем.

Date: 2010-07-15 09:39 am (UTC)
From: [identity profile] cmm.livejournal.com
или ещё проще: все объекты являются указателями, и все они передаются по значению.  распространённой оптимизацией является помещение неизменябельных небольших данных прямо в тело их указателей.

Date: 2010-07-15 10:22 am (UTC)
From: [identity profile] avva.livejournal.com
Это то, как я делаю, да. Без этой оптимизации пока что, хотя не невозможно, в принципе ("указатель" у меня 32-битный индекс, мог бы вместо него жонглировать 64-битным значением с 32 битами для immediate value; но не хочу пока).

Date: 2010-07-15 10:23 am (UTC)
From: [identity profile] avva.livejournal.com
Ага, спасибо, я примерно до того же понимания дошел, только с неправильными C-шными словами, правильные успел забыть ;). Но вот блядь, почему бы это нормальным языком не написать в самом стандарте?

Date: 2010-07-15 10:51 am (UTC)
From: [identity profile] jsn.livejournal.com
Да не знаю, мне как-то сразу всё было понятно из стандарта, после перла et ol. В принципе, это очень во многих языках так в современных, поэтому чего заостряться-то.

Date: 2010-07-15 01:41 pm (UTC)
From: [identity profile] jsn.livejournal.com
ух, меняется, оказывается :)

Date: 2010-07-15 01:46 pm (UTC)

Date: 2010-07-15 09:05 am (UTC)
From: [identity profile] gianthare.livejournal.com
У тебя при вызове функции каждый раз создается Схемовский список, к-рый остается жить после вызова?
Т.е. если в цикле вызывать функцию, скажем, print, то память кончится (и вызовется gc)?

Date: 2010-07-15 09:19 am (UTC)
From: [identity profile] avva.livejournal.com
Если вызвать мильон раз, то да. Я старался писать код так, чтобы вызов GC мог случиться во время выполнения чего угодно и ничему не помешать. Сейчас это не совсем так (в районе функции eval_args()), но легко исправимо.

Т.е. теоретически макрос CHECK_CELLS, который сейчас умирает, если места нет, может вызвать GC, переупаковать всю память, подвинуть next_cell, вернуться и выполнение продолжится. Это "дефолтная" идея того, как будет работать GC, у меня в голове. Я пока что на ней не зафиксировался, и стараюсь оставить место другим идеям.

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

Date: 2010-07-15 11:05 am (UTC)
From: [identity profile] gianthare.livejournal.com
Кстати, когда ты переупаковываешь память тебе придется менять "указатели" в неосвобожденных об'ектах? Или нет?

Date: 2010-07-15 11:21 am (UTC)
From: [identity profile] avva.livejournal.com
Да. Я еще не знаю, как буду это делать, есть разные варианты (напр. in-place или в новую память). Но так или иначе надо будет найти все "живые" объекты рекурсивно, переупаковать их, и поправить все указатели в них друг на друга.

Date: 2010-07-15 11:25 am (UTC)
From: [identity profile] gianthare.livejournal.com
Я прямо предвкушаю. ;-)
Вообще, просто отлично - мне и делать ничего не надо и есть над чем подумать

Date: 2010-08-01 11:58 am (UTC)
From: [identity profile] recontemplator.livejournal.com
О! Спасибо, что подсказали точную формулировку для объяснения, почему этот жанр (how it's made) мне так нравится.

Date: 2010-07-15 10:29 am (UTC)
From: [identity profile] al-zatv.livejournal.com
спасибо, интересно читать. плавное изучение схемы происходит:)

Date: 2010-07-15 12:23 pm (UTC)
From: [identity profile] ilya-dogolazky.livejournal.com
Я попробовал покомпилировать на 32-битной машинке, исправил два бага и истребил уйму варнингов, что делать с последним не знаю --- см. тамошний коммент. Если хотите, забирайте --- http://github.com/ilyad/sketch/commits/master/

Date: 2010-07-15 06:04 pm (UTC)
From: [identity profile] avva.livejournal.com
Спасибо! После этих исправлений, оно нормально работает на 32-битной машине или как?

Возьму почти все, кроме изменений в symbols.h/symbols.cc, это все равно скоро уходит.

Date: 2010-07-15 07:16 pm (UTC)
From: [identity profile] ilya-dogolazky.livejournal.com
А фиг его знает, я не в курсе что такое "нормально", так как до вчерашнего дня про схему ничего не знал :) Но различия с лосём некоторые имеются, например лось не любит пустой список -- там мол нечего вызывать. В общем sch.sh вам в помощь...

Date: 2010-07-16 10:02 pm (UTC)
From: [identity profile] avva.livejournal.com
Прошелся по всем коммитам, спасибо еще раз. Кстати, на 64-битных системах int остается 32-битным, так что разница между int и uint32_t только в знаке (к одному из ваших коммитов, я изменил это чуть по-другому).
From: [identity profile] nbuwe.livejournal.com
В контексте схемы лучше не надо говорить о "значении символа". В схеме просто нет такого понятия. Можно было бы списать это на небрежность словоупотребления, не будь других лиспов, где такое понятие есть.

В схеме, по сравнению с тем же common lisp, у символа отрезали все, кроме двух свойств, без которых уже совсем никак: имя и идентичность. В том же CL у символа есть и значение (value), и функциональное значение (function - т.к. CL это lisp2), и property list...

Конечно, в каком-то смысле можно говорить, что связи (binding) в схемовском top-level environment выполняют ту же (ну или похожую) роль, что и слот value у CL'евских символов, но лучше не надо, все и так путаются :)

http://groups.google.com/group/fido7.ru.lisp/msg/969cf3cc6479994d
http://groups.google.com/group/fido7.ru.lisp/msg/167c39e6ce42ff06
From: [identity profile] avva.livejournal.com
Спасибо. Я не хочу создавать терминологическую путаницу, но и какой другой термин использовать, не знаю. Как назвать, для данного символа, то, куда указывает его текущий binding (необязательно из top-level environment)? Это то, что я назвал значением.
From: [identity profile] nbuwe.livejournal.com
А чем плохо банальное "значение переменной"?

В связке программа==данные символ нужен собственно только ради своего имени-идентификатора, а идентификаторы это или переменные или макросы (syntactic keyword).

И про "текущий binding символа" тоже поосторожнее :), стандарт говорит о binding'е только применительно к идентификаторам, т.е. переменным и ключевым словам. То, что в связке программа==данные мы используем символ ради своего имени-как-идентификатора, чтобы представить идентификатор, не делает сам символ связанным с чем-то - ну нет в схеме такого понятия и, соотвественно, функций аналогичных CL boundp &co.

В том же CL:

[1]> (let ((x 0)) (boundp 'x))
NIL

*переменная* "x" внутри формы let вполне себе определена - для нее существует лексическая связь, но *символ* "x", как видим, никакой связи не имеет.
From: [identity profile] avva.livejournal.com
Наверное, мне не очень понятно, зачем нужно понятие "переменная", если есть понятие 'текущий binding символа'. Ваше возражение против термина 'текущий binding символа' я, наверное, понял, но не уверен.

Попробую подробно описать ситуацию так, как я ее понимаю (в Схеме). Итак, у нас есть символы. В CL у каждого символа есть автоматически всякие "загашники", которые ему достаются бесплатно, просто за счет того, что он символ: value slot, function slot, property list. В Схеме у символа никаких "загашников" нет, ничего ему за то, что он символ, не полагается, кроме собственно его идентичности как символа.

Если символ используется в качестве идентификатора, то он попадает в environment и получает binding к определенному месту в памяти, куда можно записать любой схемовский объект. "Попасть в environment", необязательно top-level, и "получить binding" - синонимы. Концептуально говоря, в любой момент времени есть стэк активных environments, начинающийся с top-level (поэтому представим его себе растущим сверху вниз), и самый нижный environment, в котором есть binding для данного символа, дает ему его "текущий binding". Если определенный символ вообще нигде не находится в активном на данный момент стеке, то у него нет никакого binding.

Если я вас правильно понял, вы возражаете против "текущий binding символа", потому что это звучит так, как будто это некая имманентная property самого символа, которую можно регулировать "руками"; в то время как это всего лишь артефакт текущего состояния стэка environments. Меня же не очень смущает этот термин, потому что для меня он является практически синонимом ментальной фразы "что мы получим, если пройдемся поиском по стэку environments, включая возможность, что не найдем ничего". Но по сути дела, в том, как это устроено, разногласий нет. Я правильно вас понял? Спасибо еще раз за разъяснения до сих пор.
From: [identity profile] nbuwe.livejournal.com
Наверное, мне не очень понятно, зачем нужно понятие "переменная", если есть понятие 'текущий binding символа'.

Ок, тогда в моём примере из CL:

[1]> (let ((x 0)) (boundp 'x))
NIL

как в вашей терминологии увязать между собой, что у переменной "x" есть значение, но символ 'x не имеет связи?

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

Заметьте, что стандарт схемы говорит именно о variable binding. Стандарт CL тоже крайне аккуратен в выборе формулировок (там все несколько усложняется динамическим окружением и штуками типа symbol-macrolet). И это писали парни, которые могут похваляться шрамами, полученными на тех граблях.

по сути дела, в том, как это устроено, разногласий нет

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

К сожалению по переписке это всё довольно трудно внятно сформулировать, уж простите.

February 2026

S M T W T F S
1 2 3 4 5 67
8 9 10111213 14
15 16 17 18192021
22232425262728

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags
Page generated Feb. 24th, 2026 05:50 am
Powered by Dreamwidth Studios