Добро пожаловать!

Я Дож, программирование — моё хобби. По мере того, как я осваиваю что-то новое, стараюсь об этом написать пост.
На текущий момент в блоге затронуты следующие темы: vim, free pascal, lisp, forth, m4
Занимаюсь разработкой своего языка под названием DEmbro, подбробней: wiki и svn
Для постов, не связанных с программированием, у меня есть отдельное жж.

декабря 18, 2010

Кодировки в vim'е (в том числе koi8-r для windows)

Часто приходится сталкиваться с файлами в разных кодировках.

Если Вы открыли файл в виме и увидели вместо русских букв закорючки,
то скорее всего файл отображается в неверной кодировке. Чтобы вручную
выставить кодировку, следует набрать
  :e ++enc=название_кодировки
Вот некоторые из возможных кодировок для кириллицы:

  1. cp688 — основная DOS-кодировка
  2. cp1251 — основная Windows-кодировка
  3. utf-8 — UTF8-кодировка
  4. koi8-r — основная Linux-кодировка
Более полный список см. «:help encoding-names».

Если вы используете windows, то «koi8-r» изначально не поддерживается. Чтобы
исправить это, следует скачать файл «iconv.dll» и кинуть в папку с exe'шником
вима (я качал отсюда).

Предположим другую ситуацию: вам нужно сохранить файл, перекодировав его в
другую кодировку. Для этого можно воспользоваться опцией «fileencoding»:
  :set fileencoding=название_кодировки
или же
  :w ++enc=название_кодировки имя_выходного_файла

Часто приходится работать с разными файлами, и хочется, чтобы
вим пытался при запуске как-то выбрать наиболее подходящую кодировку
из нескольких. Достигается это прописыванием опции «fileencodings» в файле
«.vimrc». К примеру, у меня прописано так:
  set fileencodings=utf-8,cp1251
Это означает, что вим попытается отобразить файл как utf-8, а если не получится,
то отобразит в cp1251.

Узнать список поддерживаемых кодировок можно вызовом
help encoding-names


Читать дальше......

декабря 06, 2010

Подключение библиотек

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

Вообще, хочу всё вынести в RTL, и оставить изначально всего одну команлу:
«builtin». Она будет подключать в программу встроенные в язык команды,
например, вот так:
builtin +
builtin -
builtin create

По умолчанию будет загружаться стандартный «system.de» файл, который будет
подключать все встроенные команды. Но их много, поэтому будет возможность написать
свой system.de, содержащий в себе необходимый для поставленной задачи
комплект. Это позволит мне не скупиться на встроенных «быстрых» (на машинных кодах)
командах, а с другой стороны будет возможность убрать всё ненужное.


Читать дальше......

ноября 26, 2010

Hex-константы и хедеры

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

Во-первых, что лучше — обозначать hex-константу префиксом или постфиксом? Мне привычней префикс, но это лишь вопрос привычки, зато парсинг немного проще.

Логичней всего использовать букву h (принято в asm'ах, но у них это не префикс, а постфикс):
hFFF1

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

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

А Delphi-стиль (префикс $ перед цифрами)
$FFF1
хоть и не очень распространён и не логичен, выглядит наиболее удобным — особый символ, не сливающийся с цифрами.

Пока что я избрал первый вариант, посмотрим приживётся или нет.

Пишу тут заголовок для WinApi windows.de на DEmbro. Из-за многих недоработок языка выглядит непричёсанным, но всё по-взрослому: с переобозначением типов, объявлением констант, объявлением прототипов функций с автоматической их загрузкой в период выполнения (т.е. не при подключении библиотеки, а при вызове функции windows-load этой библиотеки). Когда требуется добавить новую функцию в хедер, достаточно прописать её прототип один раз, не нужно копипастить что-то в стиле
typedef ... (*SomeFunc_t)(....);
...
extern SomeFunc_t SomeFunc;
...
SomeFunc = (SomeFunc_t)GetProcAddress(libuser32, "SomeFunc");

Одна моя мечта сбылась *.* Йииехуууууу!


Читать дальше......

ноября 23, 2010

Разные файлы — разные настройки

По мере того, как я плотнее программирую на нескольких языках, всё больше становится необходимо задавать под разные типы файлов совершенно разные настройки. Например, сворачивание текста (folding) в исходниках для Free Pascal я оформляю конструкцией REGION (для совместимости с IDE Delphi), а в DEmbro обычным вимовским образом.

Штатный вариант это сделать: поправить файлы filetype-настроек (синтаксиса, компилятора или другие подобные) — но почему-то не хочется в них разбираться, хочется всё просто описать в «.vimrc».

А в «.vimrc» это сделать так просто нельзя: опция filetype будет вычислена уже после выполнения «.vimrc», т.е. в «.vimrc» будет не доступна. А нам нужно внутри «.vimrc» сделать в случае одного filetype одни настройки, а в случае другого filetype — другие.

Один из трюков заключается в следующем: описываем настройки в виде функции:

function DEmbroMarker()
  set fdm=marker
  set commentstring=//%s
  set foldmarker={{{,}}}
endfunction


А потом, в самом конце, прописываем условное действие при запуске всех файлов:

au BufRead,BufNewFile *         if &ft == 'dembro' | call DEmbroMarker() | endif


Аналогично для другого типа файла (в моём случае, для pascal'я).


Читать дальше......

MessageBox

Только что моя прога на DEmbro смогла вывести на экран виндовый message box. Доволен как слон.

В целом, темпы очень маленькие, много ленюсь.


Читать дальше......

ноября 15, 2010

Name typization

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

Динамическая типизация — это когда переменная может принимать значение любого
типа. Как только с переменной нужно проделать какое-то действие (сложение, например),
интерпретатор узнаёт тип переменной, и в зависимости от этого выбирает операцию.
Это очень удобно, не нужно заботиться о явном указании типа. Но работает медленней.

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

[Upd] Всё, что идёт ниже, про упрощение статической типизации.

Я осознал, что бывают такие переменные, у которых всегда один и тот же тип.
Например, у меня переменные I и J всегда имеют тип Integer, переменные
Name и FileName тип TString, P тип Pointer и т.д. Было бы весьма удобно иметь
возможность указать привязанные к именам типы, чтобы потом их явно не
указывать. Как-то так:


typization I, J, N, M, K: Integer;
typization Name, FileName, Dir: TString;

procedure LoadFilesInDir(Dir);
var
  Files: TArrayOfString;
  I;
begin
  GetFilesInDir(Dir, Files);
  for I := 0 to High(Files) do
    LoadFile(Files[I]);
end;


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

К примеру, в математическом модуле, все переменные с именами M, H, Mat, Matrix будут
являться матрицами, V, A, B, Vec, Vector — векторами, Q, R, Quat и Quaternion —
кватернионами и т.д. Вот было бы хорошо, если бы можно было делать так


typization M, H, Mat, Matrix: TMat4f;
typization V, A, B, Vec, Vector: TVec3f;
typization Q, R, Quat, Quaternion: TQuat;

function Add3f(A, B): TVec3f;
function Dot3f(A, B): TVec3f;
function Cross3f(A, B): TVec3f;
function Len3f(V): Single;

function Apply3f(M, V): TVec3f; overload;
function Apply3f(Q, V): TVec3f; overload;


Возможно, что иногда даже можно бы было не объявлять переменную, компилятор сам
бы создал локальную с таким именем. Например, если в функции используется
переменная I, то скорее всего она локальная и имеет тип Integer (потому что этот
тип указан в typization секции).


procedure LoadFilesInDir(Dir);
var
  Files: TArrayOfString;
begin
  GetFilesInDir(Dir, Files)
  for I := 0 to High(Files) do
    LoadFile(Files[I]);
end;


Читать дальше......

октября 19, 2010

DForth → DEmbro

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

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

Во-вторых, открыл отдельный wiki- и svn-сервер под проект.

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

Сейчас ведётся активная разработка подключения динамических библиотек (dll в windows'е).


Читать дальше......

августа 30, 2010

Автоматизация в обычной жизни: исправление субтитров

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

00:01:40.12,00:01:41.34
Dear God

00:01:41.44,00:01:44.15
Cut it off! His arm - cut it off!

00:01:58.92,00:02:00.23
Is he dead?

00:02:00.32,00:02:03.07
- Are you drunk, Mr Price?[br]- I am, I am.

00:02:03.16,00:02:06.35
- Is he dead, the Colonel?[br]- Not dead yet. He has hope.

00:02:06.44,00:02:09.43
Hey! You two. Get him back. Back!

00:02:11.32,00:02:14.91
We're in! Do you hear? They cheer!

00:02:15.00,00:02:16.95
Cuidad Rodrigo is taken

00:02:17.04,00:02:19.63
Oh. He looks dead to me.
...

А чтобы субтитры прочитались в моём плеере (GOMPlayer), он должен был выглядеть так:

0
00:01:40.12 --> 00:01:41.34
Dear God

1
00:01:41.44 --> 00:01:44.15
Cut it off! His arm - cut it off!

2
00:01:58.92 --> 00:02:00.23
Is he dead?

3
00:02:00.32 --> 00:02:03.07
- Are you drunk, Mr Price?[br]- I am, I am.

4
00:02:03.16 --> 00:02:06.35
- Is he dead, the Colonel?[br]- Not dead yet. He has hope.

5
00:02:06.44 --> 00:02:09.43
Hey! You two. Get him back. Back!

6
00:02:11.32 --> 00:02:14.91
We're in! Do you hear? They cheer!

7
00:02:15.00 --> 00:02:16.95
Cuidad Rodrigo is taken

8
00:02:17.04 --> 00:02:19.63
Oh. He looks dead to me.
...

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

Чтобы заменить запятые на стрелочки, можно воспользоваться макросами в виме. В первой строке выполняем
    qw0f,cl --> <ESC>jjjq
Это создаст макрос на букве w, меняющий в текущей строке первую запятую на стрелочку и смещающий курсор на три строки вниз. Осталось теперь несколько раз набрать
    1000@w
и все запятые будут заменены на стрелочки.

Теперь перейдём к нумерации. Тут мы воспользуемся M4, а именно пригодится макрос incr, который предназначен для увеличения своего параметра на единицу. Например, incr(`17') выдаст 18.

Будем хранить в переменной count кол-во занумерованных строк:
    define(`count',`0')
а макрос current будет подставлять очередной номер субтитра и увеличивать count на 1:
    define(`current',`define(`count',incr(count))count')
Эти два определения нужно расположить в самом начале файла.

Теперь вставляем current перед описанием каждого субтитра — используем тот же приём, что и при замене запятых, только создание макроса будет выглядеть так (предполагается, что курсор находится на пустой строке):
    qwocurrent<ESC>jjjq

Осталось скормить это в m4:
    m4 subtitles.srt4 > subtitles.srt

Всё, теперь субтитры грузятся и работают.

Осталась совсем маленькая деталь — в субтитрах можно обнаружить команду [br]. Она предназначена для разбиения субтитров на несколько строк, однако, GOMPlayer это игнорирует и отображает её как текст. Чтобы заменить все вхождения [br] на разбиение строк, достаточно выполнить команду
    :%s/\[br\]/\r/g


Читать дальше......

августа 28, 2010

DForth: продвижения 3

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

Сразу после запуска доступно 525 команд — вышло много комбинаторных взрывов с типизированными командами. В этом деле M4 очень помогает избежать копипаста.


Читать дальше......

августа 16, 2010

DForth: продвижения 2

Появились функции (т.е. объявление своих команд через двоеточие, в том числе immediate), работа с двумя видами строк.

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

Читаю потихоньку стандарт forth-94.


Читать дальше......

августа 08, 2010

DForth: продвижения

Написал генерацию шитого кода, его исполнение (первоначально была написана чистая интерпретация). Написал команды branch, ?branch, >mark и >resolve, позволяющие реализовывать условные и безусловные переходы. Когда добавлю возможность объявлять свои команды (т.е. функции) — средств будет достаточно, чтобы на самом DForth'е реализовать оператор if.

Научился создавать пустой исполняемый файл (который ничего не делает) для Win32. Убил на это три дня :)

Прочитал стандарт forth-83. Весьма примитивно, я ожидал чего-то бо́льшего. Но всё равно интересно.


Читать дальше......

августа 05, 2010

Азы M4

Всё описанное должно работать в GNU версии макропрепроцессора m4, в операционной системе Windows XP.

Скачать можно отсюда, в первую очередь требуются Binaries и Dependencies. После размещения m4.exe и regex2.dll в одну папку можно начать пользоваться.

Запуск m4.exe без параметров вызывает прогу в REPL-режиме. Чтобы выйти из m4, нужно набрать m4exit. В этом REPL режиме удобно отлаживать свои наработки.

Чтобы обработать файл, достаточно передать его имя в качестве параметра:
    m4 u1.pas4

При этом m4 выдаст результат в стандартный вывод. Стандартный вывод можно перенаправить в файл:
    m4 u1.pas4 > u1.pas

Если передать несколько файлов, то они будут последовательно выполнены так, как будто это единый файл.
    m4 u1.pas4 u2.pas4 > u.pas

Это было то, что касается запуска m4, теперь приступим к собственно использованию. m4 ищет в поступающем тексте макросы и исполняет их. Например, макрос __file__ заменяется на название текущего файла, а __line__ на номер текущей строки.

В m4 есть такое понятие, как цитата, по умолчанию цитаты должны быть заключены в кавычки (обратите внимание, что они разные)
  `это цитата'

К примеру, если у вас в тексте встречается __file__, которое не должно быть заменено на имя файла, то его нужно оформить в виде цитаты
  `__file__'
m4 при этом отбросит кавычки и останется только __file__.

При помощи макроса changequote можно сменить символы, отвечающие за кавычки:
  changequote({,})
После этого цитаты можно оформлять в виде:
  {__file__}

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

Вернём стандартные цитатные символы
  changequote(`,')

Собственные макросы объявляются при помощи макроса define. Первый переданный ему параметр задаёт имя макроса, а второй тело.
  define(`foo', `bar')

Макрос foo будет просто заменяться на bar. $1, $2, $3 и т.д. в теле макроса заменяются на переданные параметры. Например:
  define(`assign', `$2 := $1')
  assign(a+b, c)

выведет «c := a+b»

Изученного уже достаточно чтобы написать макрос assert для проверки истинности утверждения:
  changequote(~,")
  define(~assert", ~if not ($1) then begin Writeln('Assert error $1 in __file__:__line__') end")


Пример использования:
  assert(Obj <> nil);


Читать дальше......

августа 04, 2010

M4

Я вот пишу свою прогу для препроцессинга сурсов (dpp), а тут вдруг обнаружил программку M4:
http://lib.custis.ru/M4

Она предназначена для препроцессинга исходников на основе макросов. Т.е. именно то, чего мне так не хватает. Круто! Теперь буду её изучать.


Читать дальше......

августа 03, 2010

DForth

Недавно начал читать про язык Forth, мне понравились некоторые идеи, но как-то влом его досконально изучать. Поэтому я решил реализовать свой форт, под названием DForth. Пишу его на языке free pascal.

За сегодня написал основные команды работы со стеком для всех видов целых чисел, арифметические операции для них же, вывод их в консоль, функции сравнения и булевы операции. Это всё вылилось в 210 команд! Не хило так... И это я ещё не приступил ни к чему содержательному, совсем.

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

Начал изучать как создаются exe-файлы в win32, благо это есть в исходниках fpc :) Поэтому скоро мой DForth станет одновременно компилятором, интерпретатором и скриптовым движком.


Читать дальше......

июля 31, 2010

Обработка ошибок внешних библиотек

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


function CreateWindow(Width, Height : Integer; Fullscreen : Boolean; PixelDepth : Integer) : Boolean;
  // ...
begin
  // ...
  if (RegisterClass(wndClass) = 0) then  // Attemp to register the window class
  begin
    MessageBox(0, 'Failed to register the window class!', 'Error', MB_OK or MB_ICONERROR);
    Exit
  end;

  // ...
  h_Wnd := CreateWindowEx(dwExStyle,      // Extended window styles
                          'OpenGL',       // Class name
                          WND_TITLE,      // Window title (caption)
                          dwStyle,        // Window styles
                          0, 0,           // Window position
                          Width, Height,  // Size of window
                          0,              // No parent window
                          0,              // No menu
                          h_Instance,     // Instance
                          nil);           // Pass nothing to WM_CREATE
  if h_Wnd = 0 then
  begin
    MessageBox(0, 'Unable to create window!', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  // Try to get a device context
  h_DC := GetDC(h_Wnd);
  if (h_DC = 0) then
  begin
    MessageBox(0, 'Unable to get a device context!', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  // ...

  PixelFormat := ChoosePixelFormat(h_DC, @pfd);
  if (PixelFormat = 0) then
  begin
    MessageBox(0, 'Unable to find a suitable pixel format', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  if (not SetPixelFormat(h_DC, PixelFormat, @pfd)) then
  begin
    MessageBox(0, 'Unable to set the pixel format', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  // Create a OpenGL rendering context
  h_RC := wglCreateContext(h_DC);
  if (h_RC = 0) then
  begin
    MessageBox(0, 'Unable to create an OpenGL rendering context', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  if not wglMakeCurrent(h_DC, h_RC) then
  begin
    MessageBox(0, 'Unable to activate OpenGL rendering context', 'Error', MB_OK or MB_ICONERROR);
    Exit;
  end;

  // ...
  Result := True;
end;


Можно заметить, что тут очень много копипастится код

MessageBox(0, 'Unable to «что не получилось»!', 'Error', MB_OK or MB_ICONERROR);
Exit;

Кроме копипаста этих двух строк, есть и второй копипаст: вместо «что не получилось» нужно везде прописывать название функции, в которой это не получилось. Как это можно бы было улучшить? Вынести в отдельную функцию нельзя, потому что Exit не умеет прыгать сразу на две функции вверх, а чтобы message box отобразил строку, нужно эту строку вбить вручную (не важно где, главное что эта строка должна быть вбита своими руками).

Конечно, в языке есть исключения, но WinApi не бросает никаких исключений специально для Free Pascal в случае штатных ошибок.

Как же быть? А никак — только копипаст. Те же беды с контролем ошибок в WinSock, DirectX и далее по списку.

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


(defmacro block-check-winapi (&body body)
  `(block NIL
      ,@(loop for f in body collect 
            `(if (eql ,f 0) 
                 (progn 
                     (MessageBox 0 
                                 (format NIL "Unable to ~a" ',f)
                                 "Error" 
                                 (or MB_OK MB_ICONERROR)) 
                     (return NIL))))
      T))


Как всегда, для наглядности привожу пример того, во что эта конструкция разворачивается:


(block-check-winapi 
  (RegisterClass wndClass) 
  (CreateWindowEx dwExStyle 
                  "OpenGL" 
                  WND_TITLE 
                  dwStyle 
                  0 0 
                  Width 
                  Height 
                  0 
                  0 
                  h_Instance 
                  NIL))

(BLOCK NIL
  (IF (EQL (REGISTERCLASS WNDCLASS) 0)
      (PROGN
       (MESSAGEBOX 0 (FORMAT NIL "Unable to ~a" '(REGISTERCLASS WNDCLASS))
        "Error" (OR MB_OK MB_ICONERROR))
       (RETURN NIL)))
  (IF (EQL
       (CREATEWINDOWEX DWEXSTYLE "OpenGL" WND_TITLE DWSTYLE 0 0 WIDTH HEIGHT 0
        0 H_INSTANCE NIL)
       0)
      (PROGN
       (MESSAGEBOX 0
        (FORMAT NIL "Unable to ~a"
                '(CREATEWINDOWEX DWEXSTYLE "OpenGL" WND_TITLE DWSTYLE 0 0 WIDTH
                  HEIGHT 0 0 H_INSTANCE NIL))
        "Error" (OR MB_OK MB_ICONERROR))
       (RETURN NIL)))
  T)


Читать дальше......

июля 28, 2010

Загрузка, ждите

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

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

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

На Free Pascal я могу назвать три решения.
1) В лоб — после каждой строчки кода загрузки вызываем функцию перерисовки. Это можно сделать так:


for I := 0 to LinesCount - 1 do begin
  case I of 
    0: <Line0>;
    1: <Line1>;
    2: <Line2>;
    ...
  end;
  RedrawLoading(I, LinesCount);
end;


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

2) Использовать нити (threads, так же известные как «потоки») — в одной нити грузим, в другой обновляем экран. Решение мне не нравится тем, что ИМХО с нитями лучше лишний раз не связываться.

3) Написать класс, предназначенный для выполнения одного шага загрузки, имеющий одну абстрактную виртуальную функцию Execute(). Унаследовать от него разные типы шагов: копирование файла, загрузка библиотеки, загрузки картинки, проверка чего-нибудь и т.д. После этого перед загрузкой занести в нужной последовательности такие классы в массив, и в цикле после каждого Execute вызывать RedrawLoading(I, Length(Steps)). Очень громоздкое решение — городить его ради ерунды как-то не очень хочется.


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

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


(defmacro watch ((watch-func) &body body)
  (let ((op body))
    `(progn 
       ,@(loop for i upto (- (* (list-length body) 2) 1) collect
               (if (evenp i)
                   `(,watch-func ,(/ i 2) ,(list-length body))
                   (let ((result `(,@(car op)))) (setf op (cdr op)) result)))
       (,watch-func ,(list-length body) ,(list-length body)))))


Функция watch-func как бы наблюдает за ходом выполнения кода body, поэтому макрос я назвал watch. Несмотря на то, что я попытался написать красивый код, получилось спагетти. Но поставленную задачу оно выполняет, вот лог нескольких тестов:


(watch (L) A B C D E F)

(PROGN (0 6) A (1 6) B (2 6) C (3 6) D (4 6) E (5 6) F (6 6))


(watch (L) A)

(PROGN (0 1) A (1 1))


(watch (L))

(PROGN (0 0))


По хорошему, нужно еще обезопасить L от многократных вычислений, это можно сделать конструкцией once-only.

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


  #(watch (RenderLoading)
    (FGui := TGui.Create(FGraph, FWindow);)
    (FFontMan := TFontMan.Create(Graph);)
    (FScene := TCucuScene.Create(Graph);)

    (FStyle := TStyle.Create;)
    (FStyle.SetParam('font.primary', TStyleParamFont.Create(TFont.Create(FFontMan, 'data\font\c256.bit', -6)));)
    (FStyle.SetParam('color.primary', TStyleParamColor.Create(Vec4f(0.5, 0.5, 0, 1)));)
    (FStyle.SetParam('color.background', TStyleParamColor.Create(Vec4f(0.3, 0.0, 0.3, 1)));)  
    (FGui.Root.Style := FStyle;)  
    (FConsole := TGuiConsole.Create(FGui);)

    (FStart := GetTimer;)
  )


Читать дальше......

июля 24, 2010

vim 7.3 beta

Недавно вышла бета будущего vim 7.3.


Одно из интересных нововведений — вечное UNDO. Теперь вся история редактирования файла хранится не только в рамках текущей сессии, но и при перезапусках. Например, можно будет увидеть состояние файла годичной давности. Конечно, при использовании svn это не так актуально, но не всё ведь хранится в свн. А бэкапы делать лень.

По умолчанию фича отключена, чтобы ею воспользоваться, достаточно добавить в vimrc файл строку:

set undofile


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

set relativenumber


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

set colorcolumn=…

Где вместо … нужно указать через запятую номера столбцов, которые нужно подсветить. Я у себя выставил «set colorcolumn=81» чтобы видеть какие строки не влезли в 80 символов.

Вот как у меня выглядят последние две фичи:


Читать дальше......

dpp: персморт концепции

Изначально я предполагал, что моя утилитка dpp для препроцессинга сурсов на паскале будет строчно-ориентирована. Т.е. ключевые строки программы (объявление класса, функции, слова «interface» и «implementation») должны были бы занимать одну строку, а dpp может только дописывать в конец некоторым строкам какой-то код.

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

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

А именно, код для dpp должен находится внутри блока #(…). Например, уже реализована конструкция

#(return A)

Которую dpp заменяет на

begin Result := A; Exit end

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

#(return A1 A2 … AN)

развернётся в

begin A1; A2; … A(N-1); Result := AN; Exit end

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

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


Читать дальше......

июля 22, 2010

Собственная линейная алгебра на лиспе 2

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

Пусть, например, есть такие две функции

(defun get-vector-one () (print "get-vector-one") (vec3f 1.0 0.0 0.0))
(defun get-vector-two () (print "get-vector-two") (vec3f 0.0 2.0 0.0))

Тогда вызов

(add3f (get-vector-one) (get-vector-two)) 


выдаст:

"get-vector-one"
"get-vector-two"
"get-vector-one"
"get-vector-two"
"get-vector-one"
"get-vector-two"
#(1.0 2.0 0.0)

Получается, что обе функции будут вызваны трижды. Чтобы понять почему это так, можно посмотреть во что развернется макрос add3f в этом вызове:

>(macroexpand '(add3f (get-vector-one) (get-vector-two)))

(MAKE-ARRAY 3 :ELEMENT-TYPE 'SINGLE-FLOAT :INITIAL-CONTENTS
    (LIST (+ (VEC3F-X (GET-VECTOR-ONE)) (VEC3F-X (GET-VECTOR-TWO)))
          (+ (VEC3F-Y (GET-VECTOR-ONE)) (VEC3F-Y (GET-VECTOR-TWO)))
          (+ (VEC3F-Z (GET-VECTOR-ONE)) (VEC3F-Z (GET-VECTOR-TWO)))))

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

Решение проблемы заключается в том, чтобы ввести вспомогательные переменные при помощи конструкции (let (...) ...) и вычислить значения параметров один раз в эти вспомогательные переменные. Фактически, хочется чтобы при раскрытии «(add3f (get-vector-one) (get-vector-two))» получалось:

(LET ((TEMP1 (GET-VECTOR-ONE)) (TEMP2 (GET-VECTOR-TWO)))
  (VEC3F (+ (VEC3F-X TEMP1) (VEC3F-X TEMP2))
         (+ (VEC3F-Y TEMP1) (VEC3F-Y TEMP2))
         (+ (VEC3F-Z TEMP1) (VEC3F-Z TEMP2))))

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

(defmacro add3f (&rest vecs) "Сложение векторов"
  (let ((gensyms (loop for v in vecs collect (gensym))))
       `(let (,@(loop for g in gensyms for v in vecs collect `(,g ,v)))
           (vec3f (+ ,@(map 'list (lambda(v) `(vec3f-x ,v)) gensyms)) 
                  (+ ,@(map 'list (lambda(v) `(vec3f-y ,v)) gensyms)) 
                  (+ ,@(map 'list (lambda(v) `(vec3f-z ,v)) gensyms))))))

Абсолютно тоже самое нужно проделать и для разности. Для остальных векторных макросов с фиксированным кол-вом параметров все гораздо проще, можно воспользоваться известным макросом once-only. Изначально в Common Lisp его вроде как нет, но его легко можно написать самому, или взять отсюда:

(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

Суть его проще всего понять на примере. Вот так вот выглядит исправленная версия mul3f:

(defmacro mul3f (v s) "Покомпонентное умножение вектора v на скаляр s"
  (once-only (v s)
    `(vec3f (* (vec3f-x ,v) ,s) 
            (* (vec3f-y ,v) ,s) 
            (* (vec3f-z ,v) ,s))))

once-only обеспечивает одноразовое вычисление переданных ей параметров.

Наконец, под катом привожу полный код текущей версии векторной математики:



(defmacro once-only ((&rest names) &body body)
  (let ((gensyms (loop for n in names collect (gensym))))
    `(let (,@(loop for g in gensyms collect `(,g (gensym))))
      `(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
        ,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
           ,@body)))))

(defmacro vec3f (x y z) "Создание вектора по координатам"
  `(make-array 3 :element-type 'single-float :initial-contents (list ,x ,y ,z)))

;;; Привычное обращение к координатам вектора
(defmacro vec3f-x (v)   `(elt ,v 0))
(defmacro vec3f-y (v)   `(elt ,v 1))
(defmacro vec3f-z (v)   `(elt ,v 2))

(defmacro mul3f (v s) "Покомпонентное умножение вектора v на скаляр s"
  (once-only (v s)
    `(vec3f (* (vec3f-x ,v) ,s) 
            (* (vec3f-y ,v) ,s) 
            (* (vec3f-z ,v) ,s))))

(defmacro div3f (v s) "Покомпонентное деление вектора v на скаляр s"
  (once-only (v s)
    `(vec3f (/ (vec3f-x ,v) ,s) 
            (/ (vec3f-y ,v) ,s) 
            (/ (vec3f-z ,v) ,s))))

(defmacro dot3f (a b) "Скалярное произведение векторов"
  (once-only (a b)
    `(+ (* (vec3f-x ,a) (vec3f-x ,b)) 
        (* (vec3f-y ,a) (vec3f-y ,b))
        (* (vec3f-z ,a) (vec3f-z ,b)))))

(defmacro qlen3f (v)  "Квадрат длины вектора"  (once-only (v) `(dot3f ,v ,v)))
(defmacro len3f  (v)  "Длина вектора"          `(sqrt (qlen3f ,v)))
(defmacro norm3f (v)  "Нормирование вектора" 
  (once-only (v) `(div3f ,v (len3f ,v))))

(defmacro add3f (&rest vecs) "Сложение векторов"
  (let ((gensyms (loop for v in vecs collect (gensym))))
      `(let (,@(loop for g in gensyms for v in vecs collect `(,g ,v)))
           (vec3f (+ ,@(map 'list (lambda(v) `(vec3f-x ,v)) gensyms)) 
                  (+ ,@(map 'list (lambda(v) `(vec3f-y ,v)) gensyms)) 
                  (+ ,@(map 'list (lambda(v) `(vec3f-z ,v)) gensyms))))))

(defmacro sub3f (&rest vecs) "Вычитание векторов"
  (let ((gensyms (loop for v in vecs collect (gensym))))
      `(let (,@(loop for g in gensyms for v in vecs collect `(,g ,v)))
           (vec3f (- ,@(map 'list (lambda(v) `(vec3f-x ,v)) gensyms)) 
                  (- ,@(map 'list (lambda(v) `(vec3f-y ,v)) gensyms)) 
                  (- ,@(map 'list (lambda(v) `(vec3f-z ,v)) gensyms))))))

(defmacro cross3f (a b) "Векторное произведение векторов"
  (once-only (a b)
    `(vec3f (- (* (vec3f-y ,a) (vec3f-z ,b)) (* (vec3f-z ,a) (vec3f-y ,b)))
            (- (* (vec3f-z ,a) (vec3f-x ,b)) (* (vec3f-x ,a) (vec3f-z ,b)))
            (- (* (vec3f-x ,a) (vec3f-y ,b)) (* (vec3f-y ,a) (vec3f-x ,b))))))

(defmacro triple3f (a b c) "Смешанное произведение векторов"
  `(dot3f (cross3f ,a ,b) ,c))


Читать дальше......

июля 21, 2010

Собственная линейная алгебра на лиспе 1

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

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

Будем писать векторные операции для трехмерного вектора с SINGLE-FLOAT в качестве типа каждой компоненты. Массивы в лиспе создаются функцией MAKE-ARRAY. Таким образом, чтобы создать новый вектор с координатами (50 60 70) нужно написать


(make-array 3 :element-type 'single-float :initial-contents (list 50.0 60.0 70.0))


Код прост и понятен, но слишком длинный, да и запомнить такое не так просто. Хотелось бы писать что-то вроде (vec3f 50.0 60.0 70.0), и чтоб компилятор вместо этого выражения сам бы подставлял make-array с нужными параметрами. (На самом деле, в лиспе принято функции и макросы создания данных называть начиная с «make-», то есть в моём случае должно было бы быть «(make-vec3f 50.0 60.0 70.0)», но из соображений лаконичности я оставлю vec3f.) Напрашивается такой вот макрос


(defmacro vec3f (x y z) "Создание вектора по координатам"
  `(make-array 3 :element-type 'single-float :initial-contents (list ,x ,y ,z)))


Теперь уже можно писать (vec3f 50.0 60.0 70.0). Для работы с вектором крайне полезно уметь узнавать его координаты. В лиспе для доступа к элементам массива используется функция ELT, т.е. получение x-координаты вектора v можно осуществить вызовом (elt v 0), y-координаты — (elt v 1), z-координаты — (elt v 2). Подобные записи не всегда являются наглядными, многие люди (и я в том числе) привыкли к «x», «y» и «z», поэтому напрашивается еще тройка макросов:


;;; Привычное обращение к координатам вектора

(defmacro vec3f-x (v)   `(elt ,v 0))
(defmacro vec3f-y (v)   `(elt ,v 1))
(defmacro vec3f-z (v)   `(elt ,v 2))


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

При помощи написанного можно сходу написать пачку полезных операторов:


(defmacro mul3f (v s) "Покомпонентное умножение вектора v на скаляр s"
  `(vec3f (* (vec3f-x ,v) ,s) 
          (* (vec3f-y ,v) ,s) 
          (* (vec3f-z ,v) ,s)))

(defmacro div3f (v s) "Покомпонентное деление вектора v на скаляр s"
  `(vec3f (/ (vec3f-x ,v) ,s) 
          (/ (vec3f-y ,v) ,s) 
          (/ (vec3f-z ,v) ,s)))

(defmacro dot3f (a b) "Скалярное произведение векторов"
  `(+ (* (vec3f-x ,a) (vec3f-x ,b)) 
      (* (vec3f-y ,a) (vec3f-y ,b))
      (* (vec3f-z ,a) (vec3f-z ,b))))

(defmacro qlen3f (v)  "Квадрат длины вектора"  `(dot3f ,v ,v))
(defmacro len3f  (v)  "Длина вектора"          `(sqrt (qlen3f ,v)))
(defmacro norm3f (v)  "Нормирование вектора"  
  `(div3f ,v (len3f ,v)))


Теперь вызов (len3f (vec3f 30.0 40.0 0.0)) выдаст «50.0». Осталось самое сложное в этом посте: сложение и вычитание векторов.

Сложность заключается в том, что мы хотим написать макрос не для сложения двух векторов, а для сложения сколь угодного кол-ва векторов. Например, (add3f a b c d) должно сложить четыре вектора.

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

Во-вторых, для вычисления x-координаты результата нужно собрать все x-координаты каждого вектора в список и сложить их всех. Например, если нам на вход дали список векторов (a b c d), то для получения x-координаты результата мы должны просуммировать элементы списка ((vec3f-x a) (vec3f-x b) (vec3f-x c) (vec3f-x d)).

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


(map 'list (lambda(v) (vec3f-x v)) (list (vec3f 1.0 2.0 3.0) (vec3f 4.0 5.0 6.0) (vec3f 7.0 8.0 9.0)))


вернёт список (1.0 4.0 7.0) — будет применена функция под lambda к каждому элементу под list.

Теперь вернёмся к нашим сложениям. В макросе нужно получить список выражений вида «((vec3f-x v1) (vec3f-x v2) ...)» и подставить этот список под знак суммирования в «(+ ...)». Подстановка списка в выражение осуществляется конструкцией «,@». Приходим к такому вот:


(defmacro add3f (&rest vecs) "Сложение векторов"
  `(vec3f (+ ,@(map 'list (lambda(v) `(vec3f-x ,v)) vecs)) 
          (+ ,@(map 'list (lambda(v) `(vec3f-y ,v)) vecs)) 
          (+ ,@(map 'list (lambda(v) `(vec3f-z ,v)) vecs))))


Аналогично для разности:


(defmacro sub3f (&rest vecs) "Вычитание векторов"
  `(vec3f (- ,@(map 'list (lambda(v) `(vec3f-x ,v)) vecs)) 
          (- ,@(map 'list (lambda(v) `(vec3f-y ,v)) vecs)) 
          (- ,@(map 'list (lambda(v) `(vec3f-z ,v)) vecs))))


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


(macroexpand '(add3f a b c d e f))


получим:


(MAKE-ARRAY 3 :ELEMENT-TYPE 'SINGLE-FLOAT :INITIAL-CONTENTS
    (LIST (+ (VEC3F-X A) (VEC3F-X B) (VEC3F-X C) (VEC3F-X D) 
             (VEC3F-X E) (VEC3F-X F))
          (+ (VEC3F-Y A) (VEC3F-Y B) (VEC3F-Y C) (VEC3F-Y D) 
             (VEC3F-Y E) (VEC3F-Y F))
          (+ (VEC3F-Z A) (VEC3F-Z B) (VEC3F-Z C) (VEC3F-Z D) 
             (VEC3F-Z E) (VEC3F-Z F))))


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

Реализовать функцию cross3f, вычисляющую векторное произведение, оставлю на домашнее задание. На этом пока всё.


Читать дальше......

Постоянные читатели

Архив блога

Обо мне

Моя фотография
Мой e-mail: vitek_03(at)mail(dot)ru