Лицензия Creative Commons
Содержимое блога доступно по лицензии Creative Commons Атрибуция — С сохранением условий
(Attribution-ShareAlike) 3.0 Unported
, если не указано иное.

суббота, 10 января 2015 г.

Создание расширенной версии оператора case с помощью макросов Lisp

Доброго времени суток, случайные и не случайные читатели.

Сегодня хочу рассказать о модификации конструкции case в Scheme для того, чтобы она использовала переданный пользователем предикат для сравнения. Решение основано на макросах... но обо всём по-порядку.

Описание работы case

Во-первых, как работает case ? Несложно догадаться, case является одной из управляющих конструкций, которые выполняют работу по принятию решений (англ. case analysis), выбирая "путь", по которому должна пойти программа, в зависимости от переданного им выражения.

В общем виде, синтаксис case выглядит так:


case ключевое-выражение условие-1 условие-2 ...

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


(case ключевое-выражение
  ((элемент-данных ...) выражение-1 выражение-2 ...)
  ...)

Используя специальный оператор =>, можно вызвать выражение (процедуру) в условии со значением ключевого выражения:


(case ключевое-выражение
  ((элемент-данных ...) => выражение))

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


((lambda (параметр)
   ...)
 значение-ключевого-выражения)

Так же case позволяет использовать else в конце списка условий, для случаев, когда ни одно из условий не было удовлетворено:


(else выражение-1 выражение-2 ...)

Конструкция с => также допустима:


(else => выражение)

Пример использования case:


(case (+ 2 3)
  ((5)
   (display "Привет мир!")
   (newline))
  (else
   (display "С миром что-то пошло не так.")
   (newline)))

Как всё это работает? Сначала case вычисляет первый аргумент (выражение; в данном случае, (+ 2 3)), после этого сравнивает полученный результат с каждым условием. Условие состоит из списка значений для сравнения и выражения (последовательности выражений) к выполнению при совпадении результата с одним из элементов данных. Сравнение производится через предикат eqv?.

Попробуем выполнить выражение выше (-| показывает побочный эффект от вычисления выражения, в данном случае, печать строки):


(case (+ 2 3)
  ((5)
   (display "Привет мир!")
   (newline))
  (else
   (display "С миром что-то пошло не так.")
   (newline)))
-| Привет мир!

Как и ожидалось, выводится строка "Привет мир!". Но что делать, если нам нужно сравнить результат вычисления ключевого выражения со, скажем, списоком строк? Попробуем это сделать обычным case.

Использование case для перебора строк

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


(case *цвет*
  (("оранжевый")
   (display "апельсин")
   (newline))
  (("красный" "зелёный")
   (display "яблоко")
   (newline))
  (else
   (display "банан")
   (newline)))

Разумеется, компьютеры (пока) не умеют удивляться, когда им подсовывают для исполнения подобный код, однако в остальном мы уловили суть. Переменная *цвет* может быть задана до выполнения case следующим образом:

(define *цвет* "оранжевый")

Но возникает проблема -- мы всегда получаем "банан", поскольку предикат eqv? возвращает "неожиданный" результат:


(eqv? "яблоко" "яблоко")
=> #f

Что же это? Результатом булева выражения оказалась "ложь" (#f), несмотря на то, что с нашей точки зрения строки идентичны! Посмотрим, в чём дело.

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

Если же мы объявим переменную, в которую сохраним яблоко (то есть, строку "яблоко", конечно же), и объявим ещё одну переменную, присвоив ей значение первой переменной, то при сравнении этих переменных мы получим истину (#t):


(define *фрукт1* "яблоко")
(define *фрукт2* *фрукт1*)
(eqv? *фрукт1* *фрукт2*)
=> #t

Та-да! Эти переменные равны, так как ссылаются на один и тот же фрукт (одну и ту же область памяти, которая хранит строку "яблоко").

Поиск правильного инструмента сравнения

Очевидно, что eqv? не подходит в данном случае для сравнения строк, поскольку по мнению eqv? две строки различаются, если являются разными объектами, даже если они состоят из одинакового набора символов. Мы должны использовать правильный способ сравнения объектов, чтобы получить желаемый результат. В данном случае, строки должны сравниваться лексикографическим способом, тогда две строки одинаковой длины, с одинаковым набором символов, расположенных в одинаковом порядке, будут идентичны. Это может сделать предикат string=?:


(string=? "яблоко" "яблоко")
=> #t

Как раз то, что нужно. Осталось научить case использовать string=? для сравнения. Это можно сделать, добавив в язык программирования новую конструкцию -- назовём её case-pred. Сложно, разве нет? Оказывается, что не так уж и сложно, когда знаешь лисповые макросы.

Пишем код, который пишет код, который пишет код, который ...

Для реализации case-pred можно использовать другую управляющую конструкцию, cond. То есть, мы объявим наш case-pred таким образом, что после развёртки макроса он будет превращаться в корректную конструкцию cond, которая и будет выполнять всю работу.

Конструкция cond, так же, как и case, состоит из набора условий, но не привязана к конкретному ключевому выражению. В общем виде синтаксис выглядит так:

cond условие-1 условие-2 ...

где каждое из условий представляет собой:

(тест выражение-1 выражение-2 ...)

Выражения вычисляются тогда, когда тест возвращает не-ложь (не #f). Наш "фруктовый" пример кода, переписанный вручную с использованием cond:


(cond
  ((string=? *цвет* "оранжевый")
   (display "апельсин")
   (newline))
  ((or (string=? *цвет* "красный")
       (string=? *цвет* "зелёный"))
   (display "яблоко")
   (newline))
  (else
   (display "банан")
   (newline)))

Теперь осталось написать код, который будет писать код, который будет делать то, что нам нужно.

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

Макросы во всей красе

Наш первый вариант макроса case-pred использует классический лисповый макрос, объявляемый через define-macro:


(define-macro (case-pred pred key . clauses)
  `(cond
    ,@(map
       (lambda (clause)
         (let ((datum (car clause))
               (exp   (cadr clause)))
           (cond
            ((and (not (list? datum)) (not (eq? datum 'else)))
             (error "Syntax error: expected a list" datum))
            ((eq? datum 'else)
             `(else ,exp))
            ((= (length datum) 1)
             `((,pred ,key ,(car datum)) ,exp))
            (else
             `((or ,@(map (lambda (o) `(,pred ,key ,o))
                          datum)) ,exp)))))
       clauses)))

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

`(cond ,@(map ...)) как раз возвращает нужный список. Как можно прочитать в документации, оператор quasiquote, в краткой форме обозначаемый знаком грависа ("`"), позволяет создать цитированный (англ. quoted) список, части которого могут быть вычислены. Те части, которые должны быть вычислены, помечаются с помощью операторов unquote (в краткой форме -- ",") и unquote-splicing (в краткой форме -- ",@"). unquote вычисляет выражение и вставляет результат в список. unquote-splicing также вычисляет выражение, но подставляет в список элементы того списка, который был получен в результате вычисления "разкавыченного" выражения. Таким образом, ,@(map ...) возвращает список, элементы которого подставляются в другой список -- как тело cond.

Данный макрос не лишён недостатков -- к примеру, не описан оператор =>. Однако в остальном данная версия case-pred работает неплохо.

Но можем ли мы сделать эту запись короче и яснее? Ответ -- можем, используя define-syntax:


(define-syntax case-pred
  (syntax-rules (else)
    ((_ pred key ((datum ...) exp) ...)
     (cond
      ((or (pred key datum) ...) exp) ...))
    ((_ pred key ((datum ...) exp) ... (else else-exp))
     (cond
      ((or (pred key datum) ...) exp) ...
      (else else-exp)))))

В этой версии используется стиль макросов, предоставляемый GNU Guile. syntax-rules создаёт преобразователь синтаксиса, основанный на сопоставлении выражения с шаблоном, и переписывании данного выражения согласно шаблону преобразования.

К примеру, шаблон


    ((_ pred key ((datum ...) exp) ...)
     (cond
      ((or (pred key datum) ...) exp) ...))

совпадает с выражением следующего вида:


(case-pred string=? *цвет*
  (("оранжевый")
   (display "апельсин")
   (newline)))  

А шаблон


    ((_ pred key ((datum ...) exp) ... (else else-exp))
     (cond
      ((or (pred key datum) ...) exp) ...
      (else else-exp)))))

совпадает с


(case-pred string=? *цвет*
  (("оранжевый")
   (display "апельсин")
   (newline))
  (else
   (display "банан")
   (newline)))

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

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


(case-pred string=? *цвет*
  (("оранжевый")
   (display "апельсин")
   (newline))
  (("красный" "зелёный")
   (display "яблоко")
   (newline))
  (else
   (display "банан")
   (newline)))

После развёртки макроса мы получим примерно следующий код (который мы уже видели ранее):


(cond
  ((string=? *цвет* "оранжевый")
   (display "апельсин")
   (newline))
  ((or (string=? *цвет* "красный")
       (string=? *цвет* "зелёный"))
   (display "яблоко")
   (newline))
  (else
   (display "банан")
   (newline)))

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

- Артём

воскресенье, 1 декабря 2013 г.

Монтирование разделов с IMG-образов дисков, используемых QEMU

Доброго времени суток.

Заметка о том, как монтировать разделы с "сырых" образов дисков (англ. raw images), используемых, в том числе, эмулятором QEMU.

Итак, дано: Образ Debian GNU/Hurd, взятый отсюда. Цель: подмонтировать корневой раздел с образа диска к файловой системе рабочей ОС с помощью mount.

Положим, что мы уже находимся в каталоге с образом диска, а монтировать собираемся первый раздел с него в /mnt/hurd. Имя файла образа: debian-hurd-20130504.img

Первое, что нужно сделать -- посмотреть расположение разделов в образе диска (иными словами, с какого сектора диска начинается каждый из разделов). Это можно сделать как минимум двумя способами: с помощью file и с помощью fdisk. Рассмотрим оба способа.

Утилита file, как я уже как-то раз писал, умеет показывать информацию не только о простых файлах, но и о всяких чудных штуках вроде сохранённой в файл Главной Загрузочной Записи (Master Boot Record, MBR). А сейчас эта утилита поможет нам получить информацию о разделах, запечатанных внутри образа диска:

$ file debian-hurd-20130504.img | fmt
debian-hurd-20130504.img: ; partition 1: ID=0x83, starthead 32,
startsector 2048, 5785600 sectors; partition 2: ID=0x5, starthead 99,
startsector 5789694, 354306 sectors
.

Здесь утилита fmt используется, дабы уместить вывод команды file в 75 столбцов.

Посмотрим же на вывод команды. Видно, что первый раздел (partition 1) начинается с 2048-го сектора диска (startsector 2048). Это как раз то, что нам нужно.

Попробуем теперь получить эту же информацию с помощью утилиты fdisk:

$ su -
# fdisk -l debian-hurd-20130504.img
Disk debian-hurd-20130504.img: 3146 MB, 3146776576 bytes, 6146048 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk identifier: 0x00053020

                  Устр-во Загр  Начало    Конец   Блоки  Id Система
debian-hurd-20130504.img1        2048 5787647 2892800 83 Linux
debian-hurd-20130504.img2     5789694 6143999  177153  5 Расширенный
debian-hurd-20130504.img5     5789696 6143999  177152 82 Linux своп / Solaris

Видим то же самое -- начало первого раздела находится на 2048-м секторе. Разница лишь в подробности вывода и в том, что нам нужно быть суперпользователем для использования fdisk, а утилита file может быть запущена от имени обычного пользователя.

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

$ echo $((2048 * 512))
1048576

Осталось только передать полученное значение команде mount:

$ su -
# mount -o loop,offset=1048576 debian-hurd-20130504.img /mnt/hurd

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

# mount -o loop,offset=`echo $((2048 * 512))` \
debian-hurd-20130504.img /mnt/hurd

Полюбуемся результатом:

$ df -h | grep hurd
/dev/loop0  2,8G  804M  1,8G  31%  /mnt/hurd

И последнее: согласно документации, необходимо отмонтировать образ диска прежде, чем загружать его в QEMU -- иначе можно повредить данные на подмонтированных разделах образа диска.

Использованные источники:

- Артём

понедельник, 10 июня 2013 г.

Отладка разделяемой библиотеки в детективном жанре

Одна из проблем при разрабоке -- сделать так, чтобы программа собиралась и работала не только на машине у разработчика, но и на машине конечного пользователя. А ещё лучше -- чтобы всё работало без присутствия разработчика рядом.

Собственно, о чём речь

При работе над Guile-SSH столкнулся с интересной проблемой -- на Gentoo GNU/Linux (моей основной системе) библиотека работает без нареканий, а на Debian GNU/Linux 6.0.7 тестовая программа завершается с ошибкой.

Проблема была обнаружена уже на Linux Install Fest'е, перед презентацией проекта, где я должен был показать пример использования библиотеки. И вот ведь незадача -- оказалось, что эту проблему не так-то просто решить. Я потратил кучу времени, пытаясь понять -- из-за чего, собственно, происходит аварйный останов тестовой программы.

Тестовая программа -- sssh, или Scheme Secure Shell -- работает, как упрощенный вариант ssh в неинтерактивном режиме. То есть, выполняет команду на хосте и возвращает результат. Авторизация на хосте происходит по открытым ключам.

Характерные признаки Bug'а

Bug проявляет себя громким шуршанием за плинтусом и чередующиемися ошибками вида Segmentation Fault (SIGSEGV) и Illegal Hardware Instruction (SIGILL):


$ ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname
libssh version:       0.5.2
libguile-ssh version: 0.2
1. ssh_new
2. ssh_options_set
3. ssh_connect
4. ssh_is_server_known
   ok
[1]    2205 segmentation fault  ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname

Инструменты сыщика

Один из способов отладки -- это добавление отладочных сообщений (или трейсов, от англ. trace, т.е. след) в код. Это на удивление эффективный способ узнать, как работает (или не работает) программа -- в том числе, он может помочь выследить даже хитрого bug'а.

С кодом на Scheme всё достаточно просто -- можно использовать, например, display и write. Примеры:


(display "debug message\n")
=> debug message

...

(write "Hello Scheme World\n")
=> "Hello Scheme World
"

...

(define value 1024)
(display (string-append "Value:" (number->string value))
=> Value: 1024

...

(define some-list '(a b c d e))
(display some-list)
=> (a b c d e)

А вот так можно добавить вывод отладочного сообщения в код на C, чтобы он печатался из Scheme:


scm_display (scm_from_locale_string ("debug message\n"),
             scm_current_output_port ());

То же самое, но в виде удобного макроса:


#define PRINT_DEBUG(data)\
  scm_display (data, scm_current_output_port ())

...

PRINT_DEBUG(scm_from_locale_string ("debug message\n"));

Сужаем круг поиска

Вернёмся к нашему логу.


...
3. ssh_connect
4. ssh_is_server_known
   ok
[1]    2205 segmentation fault  ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname

Путём добавления дополнительных отладочных сообщений после 4-го (в логе выше) можно легко понять, что sssh аварийно завершает свою работу при вызове библиотечной функции guile_ssh_public_key_to_string (ssh:public-key->string)


...
(display "4. ssh_is_server_known\n")
...
(let ((public-key (ssh:public-key->string
                    (ssh:private-key->public-key private-key))))
  ...

которая, в свою очередь, обращается к функции ssh_string_to_char из библиотеки libssh, где и происходит ошибка:


...
str_key = publickey_to_string (data->ssh_public_key);
ret = scm_from_locale_string (ssh_string_to_char (str_key));
ssh_string_free (str_key);
...

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

Вопрос к линковщику

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

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


$ export LD_DEBUG=help
$ ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname
Valid options for the LD_DEBUG environment variable are:

  libs        display library search paths
  reloc       display relocation processing
  files       display progress for input file
  symbols     display symbol table processing
  bindings    display information about symbol binding
  versions    display version dependencies
  all         all previous options combined
  statistics  display relocation statistics
  unused      determined unused DSOs
  help        display this help message and exit

To direct the debugging output into a file instead of standard output
a filename can be specified using the LD_DEBUG_OUTPUT environment variable.

Voilà! Вместо ошибки сегментации от нашей программы мы получили замечательный вывод справки от линковщика о доступных значениях LD_DEBUG. Наиболее интересным для нас сейчас будет значение symbols. Присвоим его переменной и попробуем запустить программу снова (надеюсь, вы не забыли надеть защитные очки?):


$ export LD_DEBUG=symbols
$ ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname
...
      2487: symbol=ssh_string_to_char;  lookup in file=/usr/lib/i686/cmov/libcrypto.so.0.9.8 [0]
      2487: symbol=ssh_string_to_char;  lookup in file=/lib/i686/cmov/libpthread.so.0 [0]
      2487: /usr/local/lib/libguile-ssh.so.0: error: symbol lookup error: undefined symbol: ssh_string_to_char (fatal)
[2]    2487 segmentation fault  ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname

Итак, мы получили ОЧЕНЬ много информации на выходе (я пропустил здесь эту часть), но нам важны последние несколько строчек. Вот оно! Линковщик не может найти символ ssh_string_to_char. Если посмотреть лог выше, то видно, что он просматривает, в том числе, и libssh:


...
 2505: symbol=ssh_string_to_char;  lookup in file=/usr/lib/libssh.so.4 [0]
...

Но ведь эта библиотека как раз и должна содержать в себе определение данной функции!

Идём по следу

Посмотрим-ка на версию библитеки в Debian GNU/Linux. Но прежде выключим подробный вывод сообщений от линковщика, присвоив LD_DEBUG пустую строку:


$ export LD_DEBUG=""
$ aptitude search libssh
...
i A libssh-4            - tiny C SSH library
...
$ aptitude versions libssh-4
i A 0.4.5-3+squeeze1    oldstable                  500 
p A 0.5.3-1~bpo60+1     squeeze-backports          100 

То есть, на Debian GNU/Linux у меня сейчас стоит версия libssh 0.4.x, тогда как мне известно, что API библиотеки был изменён в версии 0.5.x.

Попробуем поставить libssh 0.5 и запустить программу снова. Я собрал версию 0.5.3 из исходников, make install установил её в /usr/local/lib. Укажем программе, где искать библиотеку через переменную LD_LIBRARY_PATH:


$ LD_LIBRARY_PATH=/usr/local/lib ./sssh.scm -i /home/avp/.ssh/lazycat localhost uname
libssh version:       0.5.2
libguile-ssh version: 0.2
1. ssh_new
2. ssh_options_set
3. ssh_connect
4. ssh_is_server_known
   ok
5. ssh_userauth_pubkey
6. ssh_channel_new
7. ssh_channel_open_session
8. ssh_channel_request_exec
Linux

Наконец-то! Заработало! Последняя строчка вывода получена по протоколу SSH и является выводом команды uname.

Заключение

Получается, что баг скрывался даже не в коде (точнее, не совсем в коде), а в динамической линковке с разделяемой библиотекой. При динамическом связывании линковщику не удавалось найти функцию ssh_string_to_char из libssh, тем не менее вызов этой функции всё же происходил. Я думаю так: процессор закладывал аргументы в стэк и прыгал на начало несуществующей функции -- из-за этого-то мы и получали чередующиеся ошибки SIGSEGV/SIGILL. Элементарно, Ватсон.

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

Одно из возможных решений проблемы -- это привязка библиотеки Guile-SSH к конкретной стабильной версии libssh. Это не самый лучший вариант, так как не во всех дистрибутивах используется новая версия библитеки (примером тому могут служить Debian GNU/Linux и старые версии Ubuntu GNU/Linux). Однако это решение позволит избежать проблем с поддержкой совместимости Guile-SSH с libssh 0.4.x.

На этом у меня всё. Надеюсь, что это расследование вам было интересно.

- Артём

суббота, 8 июня 2013 г.

Guile-SSH

Доброго времени суток, случайные и не случайные читатели этого блога.

Занимаюсь сейчас разработкой библиотеки Guile-SSH, которая призвана обеспечить возможность работы с протоколом SSH из программ, написанных на языке Scheme (с использованием интерпретатора GNU Guile).

Презентация по проекту: odp pdf (CC-BY-SA 3.0)

Guile-SSH является обёрткой над libssh и находится на начальной стадии разработки. На данный момент библиотека предоставляет API для создания простого SSH-клиента (пример клиента можно посмотреть здесь). API для создания SSH-сервера планируется.

Для сборки текущей версии библиотеки вам нужны GNU Guile 1.8 и libssh 0.5.3 или новее. Инструкции по сборке и установке можно найти здесь. Последняя версия Guile-SSH на данный момент -- 0.2, но если надумаете собирать, то лучше берите последний коммит с master'а. Код относительно стабилен (по крайней мере, стараюсь ничего не ломать в коммитах).

- Артём

воскресенье, 14 апреля 2013 г.

Макрос define-method* для использования ключевых слов совместно с GOOPS

Сегодня загрузил в репозиторий LazyCat коммит, который добавляет макрос, расширяющий стандартный набор функций GOOPS для создания методов. Макрос используется для реализации метода host-list-add-host в классе <host-list> и позволяет использовать ключевые слова (англ. keywords) для задания аргументов, передаваемых в метод. То есть, вместо создания методов с большим количеством параметров, или выдёргивания аргументов из списка по индексу, можно просто указывать каждый из аргументов по ключевому слову.

Текущая версия макроса выглядит так:

;; Needed modules
(use-modules (ice-9 optargs)
             (ice-9 syncase)
             (oop goops))

;; Macro definition
(define-syntax define-method*
  (syntax-rules ()
    ((_ (m (o <class>) (var defval) ...) body ...)
     (define-method (m (o <class>) . args)
       (let-keywords args #t ((var defval) ...)
                     body ...)))))

Пример использования макроса:

(define-class <talking-machine> ())

(define-method* (hey (obj <talking-machine>)
                     (say  "Hello, ") 
                     (name "J. Random Programmer"))
  (display (string-append say name "!\n")))

(define talking-machine (make <talking-machine>))

Результат вызова метода hey с разными аргументами:

(hey talking-machine)
=> Hello, J. Random Programmer!

(hey talking-machine #:say "Goodbye, ")
=> Goodbye, J. Random Programmer!

(hey talking-machine #:name "John Doe")
=> Hello, John Doe!

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

- Артём

суббота, 9 марта 2013 г.

Show bandwidth usage in GNU Screen status line

Написал простой скрипт для показа скорости загрузки/отдачи в статусной строке GNU Screen. Данный скрипт основан на скрипте для tmux, однако я его основательно переписал, убрав вызов sleep, из-за которого Screen блокировался при обновлении статусной строки.

- Артём

вторник, 29 января 2013 г.

Переворот строки c помощью указателей

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

Вот мой вариант ответа:


#include <stdio.h>

void reverse(char *str)
{
 char *p = str;

 while (*p && *(p + 1) && *p++)
  ;

 while ((p > str) && (*str += *p) && (*p = *str - *p) && (*str++ -= *p--))
  ;
}

int main(int argc, char **argv)
{
 char *str = argv[1];
 reverse(str);
 printf("desrever -- %s\n", str);
 return 0;
}

И, разумеется, "hello world":


$ gcc reverse.c -o reverse
$ ./reverse "hello world"
desrever -- dlrow olleh

- Артём

понедельник, 26 ноября 2012 г.

Препроцессор C

Доброго времени суток, случайные и не случайные читатели.

На прошлой неделе, изучая код ядра Linux, я наткнулся на довольно занятный заголовочный файл. В файле объявлено два макроса и одна inline-функция. Заинтересовало же меня следующее: название одного из макросов совпадает с именем функции, равно как и набор их параметров. Я подумал -- как будет работать этот код? И решил провести эксперимент, как только появится свободное время.

На следующий день я вспомнил про этот файл -- и понял, что результат компиляции очевиден и без проведения эксперимента: достаточно вспомнить, как работает компилятор gcc.

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

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

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

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

Если же всё прошло нормально, то в конце-концов мы получаем исполняемый файл.

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

Посмотрим на примере.

Итак, допустим, у нас есть заголовочный файл и файл с исходным кодом -- назовём их test.h и test.c соответственно. Вот содержимое test.h:

 
#ifndef __TEST_H__ 
#define __TEST_H__

inline int function1(int param) 
{
 return param; 
}

#define function1(param) function1(param)

#endif /* ifndef __TEST_H__ */

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

А вот содержимое test.c:

 
#include <stdio.h> 
#include "macros.h"

int main(int argc, char *argv[])
{ 
 printf("Result of calling function1: %d\n", 
  function1(100));

 return 0;
}

Результат работы препроцессора можно увидеть, передав gcc ключ -E, вот так:

$ gcc -E test.c -o test.i

Не буду приводить здесь этот файл test.i полностью (он достаточно объёмен) -- посмотрим лишь на последние 15 строк:

 
$ tail -15 test.i 


inline int function1(int param)
{
 return param;
}
# 3 "test.c" 2

int main(int argc, char *argv[])
{
 printf("Result of calling function1: %d\n",
  function1(100));

 return 0;
}

Здесь мы видим, что в main() всё так же вызывается function1(), однако этот вызов был получен в результате работы препроцессора: он нашёл макрос function1, потом встретил имя function1 в main(), и подменил его результатом обработки макроса.

Попробуем сделать следующее: поменяем макрос function1() в test.h таким образом, чтобы он разворачивался в вызов несуществующей функции function2():

 
#define function1(param) function2(param)

Теперь, после работы препроцессора мы увидим следующую картину:

 
$ tail -15 test.i        


inline int function1(int param)
{
 return param;
}
# 3 "test.c" 2

int main(int argc, char *argv[])
{
 printf("Result of calling function1: %d\n",
  function2(100));

 return 0;
}

Видно, что препроцессор совершенно не обратил внимание на то, что функция function2() нигде не объявлена -- он просто подменил макрос на результат его обработки. Об этой возмутительной оплошности нам скажет линковщик:

 
$ gcc test.c 
/tmp/ccUBRByP.o: In function `main':
test.c:(.text+0x19): undefined reference to `function2' collect2:
выполнение ld завершилось с кодом возврата 1

Из этого можно сделать интересный вывод: если у нас существует функция и макрос с одинаковым именем и набором параметров, то нам не удастся вызвать функцию напрямую -- её вызов прежде будет обработан препроцессором, который подставит в место вызова результат обработки макроса. Таким образом, функция может быть "скрыта" от компилятора, или подменена. Например, поменяв макрос следующим образом:

 
#define function1(param) 0

И скомпилировав программу, мы увидим следующее:

$ gcc test.c 
$ ./a.out 
Result of calling function1: 0

Мы получили ноль, несмотря на то, что передавали в функцию число 100, так как вызов функции был принят препроцессором за вызов макроса, и заменён на тело макроса -- то есть, на ноль.

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

воскресенье, 28 октября 2012 г.

Удаление элемента списка в Scheme

Доброго времени суток, случайные и неслучайные читатели этого блога.

Сейчас довольно много пишу на Scheme, в связи с работой над проектом LazyCat. В проекте используется Scheme для GUI и для высокоуровневой логики. Подробнее о проекте на русском языке можно узнать здесь. В данной же заметке я хочу рассказать о проблеме удаления элемента из списка.

Если вы сталкивались с Lisp'ом в жизни (например, настраивая GNU Emacs), то должны знать, что список является едва ли не основным представлением как данных, так и собственно программы, их обрабатывающей. Даже название языка -- Lisp -- произошло от "List Processing", т.е. "обработка списков". Scheme, как диалект Lisp, не представляет исключение в этом плане. И работая с Scheme, вы постоянно работаете со списками.

Так вот, в LazyCat список управляемых хостов хранится, как Lisp-список. Пример:

 
'((#f host1 host2) ("ssh-hosts" host2 host4))

В данном примере есть две группы:

  • группа #f, в которую помещяются "бездомные" хосты, которые не являются членами какой-либо группы (уж так повелось);
  • и группа "ssh-hosts".

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

(car '("ssh-hosts" host2 host4)) => "ssh-hosts"

Оставшаяся часть списка -- получаемая через cdr -- это члены группы.

(cdr '("ssh-hosts" host2 host4)) => '(host2 host4)

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

Итак, нужно удалить хост из группы. Если подумать, то можно найти предельно простое по идее, но не такое простое в реализации решение (как раз тот самый "велосипед"). Идея заключается в том, что хост можно "вырезать" из списка путём следующих действий:

  1. Нужно взять часть списка, чей car -- элемент, который необходимо удалить. Эту часть можно найти путём простого перебора элементов.
  2. После этого нужно получить cdr списка.
  3. Склеить с помощью операции append две части списка -- часть списка до текущего car и часть списка, получаемая по cdr относительно текущей части с удаляемым элементом в car.

На практике же это превратилось в довольно громоздкую конструкцию:

 

(let ((new-cdr '())) 
  (let f ((group-members (cdr group)))

    ;; Are there elements in the given list?
    (if (not (null? group-members))

        (if (eq? (car group-members) host)

             ;; We found the host which should be removed from the list.
             ;; Cut out the car of the list and concatenate two lists
             (set-cdr! group (append new-cdr (cdr group-members)))

             ;; Host is not found. Check the next group member.
             (begin 
               ;; Add the current car to the new cdr of the group
               (new-cdr (append new-cdr (car group-members))) 
               ;; Try the next group member 
               (f (cdr group-members)))))))

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

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

Благодаря использованию модуля (ice-9 common-list) вышеприведённый кусок кода был заменён на следующую строчку:

 

(set-cdr! group (remove-if (lambda (element) (eq? element host)) 
                           (cdr group)))

Функция remove-if применяет предикат (фукнцию, возвращающую #t (true) или #f (false)) переданный, как первый параметр, ко всем элементам списка (второй параметр функции). Если предикат возвращает #t -- то элемент удаляется из списка. По завершению remove-if возвращает список, из которого удалены требуемые элементы.

В представленном выше примере remove-if сравнивает два объекта -- элемент списка и хост. Если элемент списка и хост -- один и тот же объект, то он удаляется из списка. После этого происходит замена cdr группы новым списком хостов.

Пример использования remove-if взят из файла host-list.scm, являющегося частью LazyCat.

- Артём

понедельник, 7 мая 2012 г.

Получение информации о MBR и таблице разделов с помощью file(1)

Только что узнал о таком интересном умении Unix-утилиты file(1), как вывод информации из главной загрузочной записи (англ. Master Boot Record, или, по-простому, MBR).

Для примера сохраним MBR с устройства /dev/sda в файл. Для этого нам нужно скопировать первые 512 байт с жёсткого диска, то есть, один блок по 512 байт. Хочу предупредить, что утилита dd(1) славится своей способностью портить жёсткие диски при неумелом обращении. Поэтому всегда проверяйте, правильно ли вы указали источник if и место сохранения of данных. Просто нужно немного внимательности. Итак, сохраняем MBR:

$ dd if=/dev/sda of=/mnt/backup/boot.mbr bs=512 count=1

Затем получим информацию о файле:

$ file /mnt/backup/boot.mbr
/mnt/backup/boot.mbr: x86 boot sector; GRand Unified Bootloader, stage1 version 0x3, stage2 address 0x2000, stage2 segment 0x200; partition 1: ID=0x83, active, starthead 1, startsector 63, 29302497 sectors; partition 2: ID=0x83, starthead 254, startsector 29302560, 8000370 sectors; partition 3: ID=0x82, starthead 254, startsector 37302930, 2554335 sectors, code offset 0x4

В выводе команды можно увидеть тип загрузчика (GRand Unified Bootloader - GRUB) и информацию из таблицы разделов. Каждая запись отделена точкой с запятой. Например, запись

partition 1: ID=0x83, active, starthead 1, startsector 63, 29302497 sectors;

говорит, что на первом разделе (/dev/sda1) находится файловая система ext (в данном случае, ext2), так как её шестнадцатиричный код - 0x83. Так же видно, что первый раздел является активным (active) - то есть, с него осуществляется загрузка. Далее идёт размер раздела в секторах.

Вот более подробная статья на эту тему (на английском):
http://www.miljan.org/main/2007/09/05/easy-way-to-read-mbr/

четверг, 2 февраля 2012 г.

Нестандартное применение функции getopt(3)

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

Как вы наверняка знаете, getopt(3) позволяет легко разбирать (или "парсить", да простит меня русский язык) опции командной строки, переданные программе.

Вместо термина "опция" так же используются термины "флаг" или "ключ".

Вот простой пример, демонстрирующий работу getopt(3):

#include <stdio.h>
#include <unistd.h>

int
main(int argc, char* argv[])
{
  int opt;

  while ((opt = getopt(argc, argv, "ab:")) > 0)
    {
      switch (opt)
      {
      case 'a':
          puts("'a' option was found.");
          break;

      case 'b':
          printf("'b' option was found. number = %d\n", atoi(optarg));
          break;
      }
    }

  return 0;
}

Работа с программой может выглядить следующим образом:

$ ./a.out -a -b 10
'a' option was found.
'b' option was found. number = 10

В данном примере используются только короткие опции - то есть, которые начинаются с "-", например, "-h", "-n 9". Родственная функция getopt_long(3) позволяет парсить так же и длинные опции, начинающиеся с "--" (например, "--verbose" или "--option=value").

Использование getopt(3) позволяет избежать создания собственного "велосипеда" для разбора опций командной строки. А теперь представим, что нам нужно разобрать подобным образом массив строк, передаваемый в качестве параметра нашей собственной функции. Почему бы не использовать для этого столь удобный инструмент, как getopt(3)? Тем более, что функция main() очень похожа на другие функции (точнее, другие функции очень похожи неё). Разве что main() является стандартной точкой входа в программу, с которой начинается её выполнение. Но для нашего эксперимента это не столь важно. Попробуем "подделать" обычную функцию под main(), чтобы getopt(3) не обнаружила подмены. Для этого посмотрим внимательно на main().

Итак, main() принимает в качестве параметров количество аргументов командной строки argc и собственно сам список аргументов argv, который представляет собой массив указателей на строки (массивы символов). Чтобы getopt(3) работала корректно, нужно создать функцию с подобным набором параметров. Внутри функции организуем разбор "командной строки":

void
fun1(int argc, char* argv[])
{
  int opt;
  
  while ((opt = getopt(argc, argv, "ab:c:")) > 0)
    {
      switch (opt)
 {
 case 'a':
   puts("'a' was found.");
   break;

 case 'b':
   printf("'b' was found. optarg = %d\n", atoi(optarg));
   break;

 case 'c':
   printf("'c' was found. optarg = %d\n", atoi(optarg));
   break;
 }
    }
}  

Теперь возьмёмся за main():

#include <stdio.h>
#include <unistd.h>

void fun1(int argc, char* argv[]);

int
main(void)
{
  int   argc = 3;
  char* test_array[] = { "-a", "-b 10", "-c 20" };

  fun1(argc, test_array);

  return 0;
}

Скомпилируем и запустим программу:

$ gcc -o getopt-test getopt-test.c
$ ./getopt-test
'b' was found. optarg = 10
'c' was found. optarg = 20

Как можно заметить, куда-то подевался нулевой элемент массива argv - словно getopt(3) начала просмотр массива с первого элемента. Разберёмся, почему. Вспомним, что при запуске программы нулевой элемент массива argv, передаваемый в main(), содержит имя исполняемого файла программы. Поэтому функция getopt(3) попросту пропускает его, так как он не содержит опций командной строки.

Решение проблемы выглядит следующим образом:

int
main(void)
{
  int   argc = 4; /* 3 аргумента командной строки + имя программы */
  char* test_array[] = { "fun1", "-a", "-b 10", "-c 20" };

  fun1(argc, test_array);

  return 0;
}

Скомпилируем и проверим исправленную версию программы:

$ gcc -o getopt-test getopt-test.c
$ ./getopt-test
'a' was found.
'b' was found. optarg = 10
'c' was found. optarg = 20

По результатам работы программы видно, что теперь getopt(3) находит опцию "-a".

понедельник, 2 января 2012 г.

Конвейерная обработка данных в Unix

Доброго времени суток, случайные и не случайные читатели этого блога.

Сегодня я хотел бы рассмотреть пример программы, которая может работать в режиме "конвейера" (pipeline), что является обыденным делом для утилит Unix-подобных систем.

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

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

Просто? Просто.

Исходный текст программы тоже очень простой:

#include <stdio.h>
#include <stdlib.h>

int
main(void)
{
  char ch;

  while (fread(&ch, sizeof(char), 1, stdin))
    putchar(toupper(ch));
  
  exit(EXIT_SUCCESS);
}

Теперь немного технических деталей. Когда вы пишете что-то вроде

$ cat my-file.txt | sort

командная оболочка (shell) связывает стандартный поток вывода stdout команды cat(1) со стандартным потоком ввода stdin команды sort(1). Таким образом, эти команды связываются в конвейер, проходя через который данные преобразуются определённым образом. В данном примере, мы выводим (считываем) содержимое файла my-file.txt на stdout, команда sort(1) принимает эти данные на stdin, сортирует их и выводит на stdout. На этом конвейер заканчивается и обработанные данные выводятся на терминал.

Поскольку для организации конвейера используются стандартные потоки ввода-вывода stdin и stdout, и связывание программ в конвейер осуществляет командная оболочка, от программы в общем случае требуется только получить данные с stdin, обработать их и вывести в stdout для (возможно) последующей обработки. Ещё более замечательным является тот факт, что одной программе не нужно знать внутреннее устройство другой программы, чтобы работать с ней в одном конвейере.

Важно заметить, что программы, составляющие конвейер, выполняются параллельно. Если рассматривать приведённый ранее пример, то команда cat(1) и sort(1) работают одновременно. Как только cat(1) выводит данные на stdout, они сразу же могут быть считаны командой sort(1) из stdin.

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

$ cat my-file.txt; sort

Сначала будет выполнена команда cat(1), которая выведет содержимое файла my-file.txt на экран, а потом будет выполнена команда sort(1), которая будет ждать ввода данных с stdin.

Скомпилируем наконец нашу программу и посмотрим, что полезного она может сделать.

$ gcc tu.c -o tu

Протестируем работу программы. Свяжем в конвейер команду awk(1) и нашу програму:

$ awk --copyleft | ./tu

Команда awk(1) с параметром --copyleft выводит краткий вариант лицензии GPL на stdout. Далее наша программа считывает в цикле по одному символу с stdin, преобразует считанный символ в прописной и выводит этот символ на stdout.

Вывод нашей программы можно так же отправить дальше по конвейеру.

$ awk --copyleft | ./tu |\
   sed s/'1989, 1991-2010 FREE SOFTWARE FOUNDATION'/'2012 Artyom Poptsov'/

Неплохой обзор конвейерной обработки данных в Unix дан в следующих статьях:

воскресенье, 1 января 2012 г.

Новогодняя ёлка на Emacs Lisp

Доброго времени суток, случайные и не случайные читатели.

Пользуясь случаем, поздравляю вас с уже наступившим Новым Годом. Дабы немного поднять вам (и себе) новогоднее настроение, публикую исходный код программы на Emacs Lisp, которая рисует замечательную новогоднюю ACSII-ёлку.

;;;
;;; Copyright (C) 2011 Artyom Poptsov
;;;
;;; This program is free software; you can redistribute it and/or modify
;;; it under the terms of the GNU General Public License as published by
;;; the Free Software Foundation; either version 3 of the License, or
;;; (at your option) any later version.
;;;
;;; For more information, see http://www.gnu.org/licenses/
;;;

;; Result of running this program sends to other buffer
(with-output-to-temp-buffer "*tree*"

  ;; Get height of the tree from stdin
  (setf HIGHT (read))

  ;; There are symbols that I will use for drawing the tree
  (setq C_BG    ":")
  (setq C_TREE  "^")
  (setq C_TRUNK "#")

  ;; A few variables
  (setq PADDING 2) ; Padding from edges of the "canvas".
  (setf TRUNK_H (/ HIGHT 4))                              

  ;; I'm using this funtions for printing empty strings filled with BG
  (defun print-bg-for-picture ()
    "This function prints out a few strings filled with background"
    (loop for i from 1 to PADDING do
   (loop for j from 0 to (+ (* HIGHT 2) (* PADDING 3)) do
  (princ C_BG))
   (princ "\n")))

  (print-bg-for-picture)

  ;; Main loop
  (loop for i from 0 to (+ HIGHT TRUNK_H (* PADDING 2)) do
 (loop for j from 0 to PADDING do
       (princ C_BG))
 (if (< i HIGHT)
     ;; Top of the tree
     (loop for j from 0 to (* HIGHT 2) do
    (if (or (< j (- HIGHT i))
     (> j (+ HIGHT i)))
        (princ C_BG)
      (princ C_TREE)))
   ;; Trunk of the tree
   (loop for j from 0 to (* HIGHT 2) do
  (if (or (< j (- HIGHT (/ HIGHT 10)))
   (> j (+ HIGHT (/ HIGHT 10))))
      (princ C_BG)
    (princ C_TRUNK))))
 (loop for j from 0 to PADDING do
       (princ C_BG))
 
 (princ "\n"))
  (print-bg-for-picture))

Вы можете вычислить (выполнить) эту программу следующим образом: запустите текстовый редактор Emacs (предположим, что он у вас уже установлен), скопируйте исходный код в буфер *scratch*, поместите точку (курсор) после последней скобки в конце программы и нажмите C-x C-e (Ctrl+x Ctrl+e). Далее вы можете ввести размер ёлки.

В результате, если вы всё сделали правильно, у вас откроется новое окно, в котором отобразится результат работы программы:

::::::::::::::::::::::::::::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::^::::::::::::::::::::::: ::::::::::::::::::::::^^^:::::::::::::::::::::: :::::::::::::::::::::^^^^^::::::::::::::::::::: ::::::::::::::::::::^^^^^^^:::::::::::::::::::: :::::::::::::::::::^^^^^^^^^::::::::::::::::::: ::::::::::::::::::^^^^^^^^^^^:::::::::::::::::: :::::::::::::::::^^^^^^^^^^^^^::::::::::::::::: ::::::::::::::::^^^^^^^^^^^^^^^:::::::::::::::: :::::::::::::::^^^^^^^^^^^^^^^^^::::::::::::::: ::::::::::::::^^^^^^^^^^^^^^^^^^^:::::::::::::: :::::::::::::^^^^^^^^^^^^^^^^^^^^^::::::::::::: ::::::::::::^^^^^^^^^^^^^^^^^^^^^^^:::::::::::: :::::::::::^^^^^^^^^^^^^^^^^^^^^^^^^::::::::::: ::::::::::^^^^^^^^^^^^^^^^^^^^^^^^^^^:::::::::: :::::::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^::::::::: ::::::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^:::::::: :::::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^::::::: ::::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^:::::: :::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^::::: ::::^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^:::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: :::::::::::::::::::::#####::::::::::::::::::::: ::::::::::::::::::::::::::::::::::::::::::::::: :::::::::::::::::::::::::::::::::::::::::::::::

Happy New Year and happy coding!

суббота, 12 ноября 2011 г.

Цена невнимательности, или как я подложил себе интересные грабли при настройке SSH

Доброго времени суток, случайные и неслучайные читатели.

Не так давно, а именно около месяца назад, появилась у меня идея - настроить аутентификацию по публичному ключу на домашний компьютер через SSH. Это более безопастно и более удобно, чем авторизация по паролям. Я не буду здесь объяснять, как настроить аутентификацию по публичным ключам, так как в интернете есть тьма-тьмущая всяких how-to по этой теме - от самых простых, до подробнейших руководств. К примеру, есть цикл статей от Дэниела Роббинса (создателя Gentoo Linux) на IBM DeveloperWorks:

А сейчас я лишь хочу упомянуть про одни грабли, на которые я наступил при настройке SSHD. Причём грабли я подложил сам себе и набил немало шишек, пытаясь найти причину, почему через локальную сеть с ПК на ноутбук аутентификация по ключам работает, а наоборот - нет. Будто бы для настройки SSH на моём ПК под Gentoo Linux требовалось какое-то сакральное знание, которым я не обладал.

Пару дней длился поиск проблемы, были испробованы разные комбинации настроек, двадцать раз проверены права на каталог ~/.ssh и файл authorized_keys (как известно, ssh трепетно относится к выставленным правам на его файлы), в общем - была потрачена уйма времени (я занимался этим в выходные :)). Ничего не помогало - источник проблемы ускальзал от меня с удивительной ловкостью. В конце-концов я принял волевое решение оставить решение этой проблемы до лучших времён.

И вот, сегодня я решил опять попробовать найти источник проблемы. Часто так бывает, что по прошествии некоторого времени проблема решается "за пять минут". Взял свой коммуникатор под управлением Android и установленным ConnectBot, проверил, занесён ли ключ, сгенерированный на коммуникаторе, в файл ~/.ssh/authorized_keys. Всё настроено, пытаюсь подключится - ничего.

$ tail -f /var/log/messages

тоже молчит.

Ну, это мы уже проходили. Неплохо бы получить больше информации в лог. Открываю конфигурационный файл /etc/ssh/sshd_config, нахожу переменную LogLevel - установлена в INFO. Открываю man-страницу на конфиг sshd_config, нахожу описание переменной LogLevel. Значение DEBUG2 включает режим подробного вывода в лог сообщений SSHD. Выставляю LogLevel DEBUG2, перезапускаю SSHD:

$ rc-config restart sshd

Пробую подключиться на ПК с телефона. Ага, вот и подсказка: файл /home/avp/.ssh/autorized_keys не найден. Стоп, мне кажется, или в имени файла ошибка? Пропущена буква "h" в слове "authorized". Переименовываю файл:

$ cd ~/.ssh/
$ mv autorized_keys authorized_keys


Пробую подключиться - работает!..

Вот так, казалось бы, такая мелочь, как одна пропущенная буква, приводит к серьёзным последствиям. Прямо как в стихотворении Самуила Маршака:

Не было гвоздя -
Подкова
Пропала.

Не было подковы -
Лошадь
Захромала.

Лошадь захромала -
Командир
Убит.

Конница разбита -
Армия
Бежит.

Враг вступает в город,
Пленных не щадя,
Оттого, что в кузнице
Не было гвоздя.

четверг, 25 августа 2011 г.

scroller.el - простое дополнение для GNU Emacs

Решил добавить в Emacs функцию, которую не нашёл во всём многообразии keybinding'ов (может, я просто плохо искал?)

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


;;; These simple functions allows scrolling with keeping
;;; position of cursor

(defun scroll-down-keeping-cursor (&optional arg)
  "Scrolling down one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-down arg)
        (scroll-down 1))))

(defun scroll-up-keeping-cursor (&optional arg)
  "Scrolling up one line while keep cursor position."
  (interactive "p")
  (if (interactive-p)
      (progn
        (scroll-up arg)
        (scroll-up 1))))

Теперь неплохо бы привязать эти функции к клавишам:


(global-set-key (kbd "M-p") 'scroll-down-keeping-cursor)
(global-set-key (kbd "M-n") 'scroll-up-keeping-cursor)

Как и при использовании обычных комбинаций C-n и C-p, эти функции можно вызывать в интерактивном режиме, передавая количество строк, на которое следует прокрутить буфер:

C-u 10 M-n

Конечно, если прописать все функции, настройки и keybinding'и в custom.el, они загрузятся. Но IMHO пользовательские файлы для Emacs выглядят гораздо опрятнее, если их разложить по отдельным файлам/каталогам.

Например, пусть это дополнение будет в отдельном файле scroller.el и положим мы его в ~/.emacs.d/. Тогда в ~/.emacs.d/custom.el останется только загрузить этот файл:


(load-file "~/.emacs.d/scroller.el")


Или так:


(setq emacs-dotdir
      (expand-file-name ".emacs.d" "~"))
(load-file (expand-file-name "scroller.el" emacs-dotdir))

пятница, 22 апреля 2011 г.

Проект "Амёба"

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


Содержание

  1. Постановка задачи
  2. «Царство Протозоа, класс Лобозные амёбы, отряд...»
  3. Реализация методов

  4. Разделять и властвовать


Постановка задачи


В один прекрасный день на курсах мы начали изучать классы в C++.


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


Эта задача легко решается с помощью статических (static) полей класса.


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



«Царство Протозоа, класс Лобозные амёбы, отряд...»


Поскольку я хочу понять работу с классами, с конструирования класса и начнём. Из-за сходства поведения (доступных методов) этого класса с амёбой, я решил назвать мой класс в честь этих микроскопических организмов - clsAmoeba (по-русски, я думаю, это будет что-то вроде "КАмёба", т.е. "Класс амёба"). Название класса само собой стало названием этого проекта.


Предупреждаю, что мои ручные КАмёбы совершенно безобидны (они не умеют стрелять лазерными лучами и так малы, что требуют совсем немного памяти). Поэтому, когда программа заработает, вы сможете создать их сколько угодно (насколько хватит памяти вашего компьютера) без риска для жизни.


Тем не менее, стандартная КАмёба должна уметь как минимум три вещи:


  1. Делиться;
  2. Сообщать количество уже существующих КАмёб;
  3. Знать всех своих потомков.

Каждая КАмёба имеет свой IDентификационный номер, который тоже умеет сообщать (иначе как мы узнаем, с какой КАмёбой мы имеем дело?).


Я объявил класс в заголовочном файле main.h и включил этот файл с помощью директивы include в другие файлы проекта.


Листинг 1: Класс clsAmoeba.


class clsAmoeba
{
 public:
  clsAmoeba();
  clsAmoeba(const clsAmoeba& pt);
  ~clsAmoeba();

  /* This methods works with the counter */
  void incr()     { counter++;      };
  void decr()     { counter--;      };
  int  getCount() { return counter; };

  /* This method returns ID of an amoeba */
  int getID() { return id; };

  /* This method returns pointer to parent of an amoeba */
  clsAmoeba& getParent() { return *parent; };

  /* Get count of the descendants of an amoeba */
  int getDCount() { return dCount; }

  /* Get descendant of an amoeba */
  clsAmoeba& getDescendant(int i) { return *descendants[i]; }

  /* This method divide an amoeba */
  void divide();

 private:
  /* ID of the amoeba */
  int         id;
  
  /* Pointer to parent of an amoeba */
  clsAmoeba*  parent;
  
  /* Descendants of an amoeba */
  clsAmoeba** descendants;
  int         dCount;

  /* Counter of amoebas */
  static int counter;
};

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


Полями класса называются переменные этого класса.


Ключевые слова "public" и "private" являются спецификаторами области видимости всего того, что объявлено после них (до конца объявления класса или же до следующего спецификатора). Методы, как правило, представляют собой интерфейс доступа к классу. Прямого доступа к полям "извне" желательно избегать. Поля предназначены для "внутреннего использования" в классе, и доступ к ним должны иметь только особо посвящённые - а именно, методы этого класса. Поэтому обычно методы объявляются в секции "public" и доступны извне, а поля объявляются в секции "private" и скрыты от внешнего мира.


Доступ к потомкам КАмёбы обеспечен через метод getDescendant(). Этот метод принимает номер потомка (не ID, а именно номер потомка в массиве descendants), и возвращает указатель на класс-потомок. Метод getDCount() возвращает количество потомков.


Самый интересный метод в классе clsAmoeba - метод divide(). При вызове этого метода КАмёба (т.е. класс) делится две части: класс создаёт свою копию в массиве потомков.


Часть методов - например, простые методы вроде getID(), которые просто возвращают значение какого-то поля класса - объявлены прямо в теле класса. Такие классы называются "инлайновыми" (англ. Inline), т.е. в переводе на русский язык, встраиваемыми. И это действительно так - компилятор заменяет в коде вызовы этих методов на код внутри самих методов. Такие методы работают быстрее, так как при работе программы не тратится время на вызов метода. Встраиваемые методы не обязательно объявлять в теле класса - вместо этого, можно использовать ключевое слово inline перед объявлением метода.



Реализация методов


Все не-инлайновые методы КАмёбы я вынес в отдельный модуль amoeba.cpp.


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



Конструкторы и деструкторы


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


Первый конструктор производит инициализацию полей класса, увеличивает счётчик КАмёб на 1 и присваивает текущее значение счётчика в качестве ID созданной КАмёбе. Этот конструктор вызывается при создании первой КАмёбы.


Листинг 2: Конструктор 1.


clsAmoeba::clsAmoeba() :
  parent(NULL),
  descendants(NULL),
  dCount(0)
{
  incr();
  id = counter;
}

Второй конструктор называется конструктором копирования и вызывается в случае деления КАмёбы.


Листинг 3: Конструктор 2.


clsAmoeba::clsAmoeba(const clsAmoeba& pt) :
  descendants(NULL),
  dCount(0)
{
  if(&pt != this)
    {
      incr();
      id = counter;
      parent = (clsAmoeba*) &pt;
    }
}

Эти два конструктора являются по сути перегруженными методами, т.е. методами с одним именем и разными списками принимаемых параметров (есть ещё несколько критериев для различия перегруженных методов, но об этом вы можете прочитать где-нибудь самостоятельно). В первом случае, метод не принимает никаких параметров, во втором - он принимает указатель на КАмёбу Компилятор сам решает, когда какую функцию (метод) вызывать.


Деструктор вызывается при уничтожении экземпляра класса, и участвует в его уничтожении. В данном случае, он уменьшает на 1 количество "живых" КАмёб и уничтожает своих потомков (что, кстати, не совсем логично).


Листинг 4: Деструктор.


clsAmoeba::~clsAmoeba()
{
  decr();
  parent = NULL;
  /* I think here may be a memory leak
   *   because I don't call delete for the
   *   descendents of the every descendent...
   */
  delete [] descendants;
}


Метод divide()


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


Листинг 5: Создаю первый экземпляр КАмёбы (файл main.cpp).


#include <iostream>
#include "main.h"

using namespace std;

int clsAmoeba::counter = 0; // Инициализирую счётчик КАмёб

int main()
{
  clsAmoeba amoeba1;
  
  //...

  return 0;
}

Вокруг - крайне недружелюбная атмосфера. Однако, других живых созданий крупнее нашей КАмёбы мы здесь вряд ли найдём. Поэтому хваталки и кусалки для нашего экземпляра не особенно и нужны (возможно, его потомки обзаведутся этими "фичами"). Что действительно важно, так это возможность создавать себе подобных. В случае с КАмёбой этот процесс достаточно прост - он сводится к делению оригинала надвое. Для этого предназначен метод divide().


Листинг 6: Метод divide() (файл amoeba.cpp)


void clsAmoeba::divide()
{
  if(!dCount)
    {
      descendants = new clsAmoeba*;
      descendants[dCount++] = new clsAmoeba(*this);
    }
  else
    {
      clsAmoeba** tmp = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        tmp[i] = descendants[i];
      
      delete [] descendants;

      dCount++;
      descendants = new clsAmoeba* [dCount];
      for(int i = 0; i < dCount; i++)
        descendants[i] = tmp[i];      
      descendants[dCount-1] = new clsAmoeba(*this);

      delete [] tmp;
    }
}

Если КАмёба делится первый раз, то потомков 0. Следовательно, выделяю память под первого потомка и создаю его. Если потомков на момент вызова метода больше 0, то создаю временное хранилище tmp под существующих потомков, освобождаю память, выделенную под массив descendants. Увеличиваю количество потомков на 1, снова выделяю память под массив descendants, но уже для нового количества потомков. Далее копирую существующих потомков из tmp в descendants. В завершении, создаю нового потомка в конце массива и освобождаю память, выделенную под временный массив tmp.



Разделять и властвовать


Для тестирования программы я создал 24 КАмёбы в main.cpp путём деления amoeba1 в цикле, а потом - её потомков, и потомков потомков...

Листинг 7: Делим, делим, делим...


// ...
  /* I'm making heap of amoebas */
  for(int i = 0; i < 10; i++)
    amoeba1.divide();

  clsAmoeba& amoebaN = amoeba1.getDescendant(2);  
  for(int i = 0; i < 5; i++)
    amoebaN.divide();

  clsAmoeba& amoebaN2 = amoebaN.getDescendant(4);
  for(int i = 0; i < 4; i++)
    amoebaN2.divide();

  clsAmoeba& amoebaN3 = amoebaN2.getDescendant(2);
  for(int i = 0; i < 4; i++)
    amoebaN3.divide();
//...

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

вторник, 15 марта 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 2.

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

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

Постановка задачи


Последний (и самый, на мой взгляд, интересный) способ отрисовки ели заключается в следующем. Я рисую в строке количество звёздочек, равное номеру строки. Для того, чтобы ель сохраняла некоторую симметричность, я сдвигаю каждую чётную строку вправо на 1 символ.

В итоге, должно получиться следующее:


01      X
02      **
03     ***
04     ****
05    *****
06    ******
07   *******
08   ********
09  *********
10  **********
12      ##
13      ##

Реализация


Для начала, зададим цвет для звезды на верхушке ели, цвет для самой ели и цвет для ствола дерева. Несмотря на то, что программа будет работать в консоли, это не проблема - можно использовать управляющие символы. О них можно прочитать, например, здесь:
http://www.linuxjournal.com/article/8603

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

#include <stdio.h>

#define N 15

int main() {
  char c_red[N]    = "\033[49;31;5m"; // Красный
  char c_green[N]  = "\033[49;32;5m"; // Зелёный
  char c_yellow[N] = "\033[49;33;5m"; // Жёлтый
  char c_reset[N]  = "\033[0;0;0m";   // Возврат к первоначальным
                                      //   значениям
  // ...
}

Теперь я могу использовать более осмысленные названия цветов в вызове функции printf(), например:


printf("%sHello %sWorld%s\n", c_yellow, c_green, c_reset); 

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


  int base_width, half_base_width;
  int i, j;

  printf("Please enter height of the fir-tree:\n> ");
  scanf("%d", &base_width);

  half_base_width = base_width / 2;

  for(i = 1; i <= base_width; i++) {
    /* Печатаю номер строки */
    printf("%s%02d\t", c_red, i);

    /* Печатаю одну линию */
    for(j = 1; j < (half_base_width + (i / 2) + 1); j++) {
      if(((i % 2) >  0) && (j <  (half_base_width - (i / 2))) ||
         ((i % 2) == 0) && (j <= (half_base_width - (i / 2))))
        putchar(' ');
      else
        if(i == 1)
          printf("%sX", c_red);  // Большая звезда на верхушке
        else
          printf("%s*", c_green); // Ель
    }
    
    putchar('\n');
  }

Для повышения реалистичности, в завершении отрисовки ёлки нарисую ещё ствол дерева.


  for(i = base_width + 1; 
      i < (base_width + base_width / 10 * 3); i++) {
    printf("%s%02d\t", c_red, i);
    for(j = 0; j <= half_base_width + (base_width / 10) / 2; j++)
      if(j < half_base_width - (base_width / 10))
        putchar(' ');
      else
        printf("%s#", c_yellow);
    putchar('\n');
  }

Значение (base_width + base_width / 10 * 3) используется для задания высоты ствола в зависимости от высоты ели. Значение half_base_width + (base_width / 10) / 2 и half_base_width - (base_width / 10) используется для задания ширины ствола.

В итоге, ель у меня выглядит вот так:

суббота, 12 февраля 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 1.

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

Теперь пришло время нарисовать первое подобие ёлки.

Постановка задачи


Должно получиться следующее:


   _
  |      X <-------- звезда
  |      *
  |     ***
  |     ***
 10    *****
  |    ***** <------ ёлка
  |   ******* 
  |   *******
  |  *********
  |_ *********
    |----9----|

       рис.1

Всё очень просто: нужно взять за основу алгоритм отрисовки треугольника и удвоить каждую строку: 1, 1, 3, 3, 5, 5...

Высота дерева задаётся пользователем.

Дабы повысить соответствие оригиналу, я хочу поместить на верхушку ёлки "звезду". Ну, вроде тех массивных стеклянных/пластмассовых штуковин, которые часто надевают на верхушку ёлки для усиления праздничного эффекта. За звезду сойдёт английская буква "X" (см. рис.1)

Реализация


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


for(i = 0; i < height; i++) {
  // ...
}

С ним не будет особых проблем. А вот над вложенным циклом нужно подумать. Каким образом реализовать удвоение каждой строки? Посмотрим, как будет расти количество звёздочек в строке, в зависимости от номера строки. Как видно из куска кода выше, переменная i изменяется в цикле от 0 до height - 1 (о чём говорит знак "меньше").


  i | кол. звёздочек в строке
  --|------------------------
  0 | 1        X
  1 | 1        *
  2 | 3       ***
  3 | 3       ***
  4 | 5      *****
  5 | 5      *****
  6 | 7     *******
  7 | 7     *******
  8 | 9    *********
  9 | 9    *********
  ...
        рис.2

Из рисунка выше видно, что количество звёздочек увеличивается на 2 на каждой чётной строке. Это даёт подсказку, как можно реализовать алгоритм.

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


for(i = 1; i <= h; i++)
  for(j = 1; j <= (h + i); j++) {
    if(j <= (h - i + 1))
      putchar(' ');
    else
      putchar('*');
    putchar('\n');
  }

Начальное значение счётчиков i, j равно 1, чтобы обеспечить отступ в один дополнительный символ перед каждой строкой.

Нарисуем 3 строку треугольника (i = 3). Пусть высота h равна 5.


h + i = 8;
h - i + 1 = 3;

 |<------------- 8 ------------->|
  _______________________________
 |   |   |   | * | * | * | * | * |
  -------------------------------
             |<----- j > 3 ----->|

             рис.3

Я думаю, из рисунка 3 можно понять работу этого алгоритма.

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

hw = h / 2 - 1          (1)


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

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

Я просто разделю i на 2 во всех проверках условий во вложенном цикле. Вот так:

// ...
int hw;
// ...
hw = height / 2 - 1;

for(i = 0; i < height; i++)
  /* Печатаю номер строки в обратном порядке (снизу вверх) */
  printf("%02d\t", height-i);

  for(j = 0; j <= (hw + i / 2); j++) { // <-- здесь
    if(j < (hw - i / 2)) // <-- и здесь
      putchar(' ');
    else
      if(i == 0) // или можно написать: if(!i)
        /* Рисую большую звезду на верхушке ёлки */
        putchar('X');
      else
        putchar('*');

    putchar('\n');
  }
// ...

Если строка нечётная, то hw будет увеличиваться в проверке условия завершения цикла for() и уменьшаться в условии if() - на остаток от деления, т.е. на 1. В результате, количество звёздочек в нечётной строке увеличиться на 2. Если же номер строки - чётный, то никаких изменений не происходит, просто копируется предыдущая строка.

Мне кажется, это потрясающе просто и понятно.

суббота, 29 января 2011 г.

Рисуем новогоднюю ёлку с помощью Си: алгоритм номер 0.

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

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

С чего всё началось


А началось всё со следующей задачи.

По условию задачи пользователь вводит число строк, а программа должна нарисовать равнобедренный треугольник с помощью звёздочек - "*". Треугольник должен выглядеть так:


    *
   ***
  *****

Для примера, положим, что я ввёл высоту, равную 5 строкам.


 _
|      *
|     ***
5    *****
|   *******
|_ *********
  |----9----|

Тогда, как видно на примере выше, ширина основания треугольника равна 9 звёздочкам. Если я возьму другую высоту - например, 10 строк - то получу треугольник шириной 19 звёздочек. Отсюда выводим формулу:

w = h / 2 - 1          (1)


где w - ширина, h - высота.

Рисую равнобедренный треугольник


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


 -Что есть-  -Что должно получиться-
1 *              1     *
2 ***            2    ***
3 *****      =>  3   *****
4 *******        4  *******
5 *********      5 *********
 \             /
  номер строки 


Чтобы это осуществить, надо знать номер центрального "столбца".


строки
  1     *
  2    ***
  3   *****
  4  *******
  5 *********
    123456789  <-- столбцы
        ^
   центральный
     столбец

Очевидно, в треугольнике высотой 5 строк, центральным будет 5-й столбец. И тут оказывается, что для нахождения центрального столбца нам нужно знать только высоту h треугольника, введённую пользователем - ведь, если верить формуле 1, то номер центрального столбца всегда будет совпадать с высотой. Это позволяет упростить программу.

Вот, кстати, код на Си:


#include <stdio.h>

int main() {
  int height;
  int i, j;

  printf("Please enter height:\n> ");
  scanf("%d", &height);

  for(i = 1; i <= height; i++) {
    for(j = 1; j <= (height + i); j++) {
      if(j <= (height - i + 1))
        putchar(' '); // Печатаем пробел
      else
        putchar('*'); // Печатаем звёздочку
    }
    putchar('\n'); // Переходим на новую строку
  }

  return 0;    
}

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

Первый цикл работает, пока i меньше или равно половины ширины треугольника. А поскольку, как уже было сказано выше, высота треугольника совпадает с номером центрального столбца, т.е. его центра - то я могу использовать значение введённой пользователем высоты height. Что я и делаю.

Второй цикл работает, пока j меньше или равно height плюс значение i (т.е. номер текущей строки).

Далее я проверяю, если j меньше или равно height минус i плюс 1. Единица здесь задаёт смещение всего треугольника от левой границы экрана. Если условие выполняется, то я печатаю пробел, иначе - звёздочку.

Эта программа решает поставленную задачу - рисует треугольник из звёздочек высотой N строк. Но...

Что не так с этим алгоритмом?..


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

Как бы нарисовать что-то, похожее на ёлку - да ещё так, чтобы эта ёлка высотой N, будучи вписанной в квадрат размером NxN, в основании была так же равной N?

Я подошёл к решению этой проблемы со всей серьёзностью, и нарисовал следующее:


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

Алгоритм отрисовки треугольника я не случайно назвал "алгоритмом номер 0" - следующие два алгоритма отрисовки ёлки реализуются на основе идей, реализованных в программе, решающей задачу с треугольником.

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

fig. 3 показывает алгоритм отрисовки ёлки. Это как раз то, что нужно. Как видно из рисунка, ширина ёлки при таком способе заполнения строк равна высоте. Причём, здесь можно заметить интересную особенность данного алгоритма - она показана на рисунке справа от fig. 3.

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

Реализацию второго алгоритма я рассмотрю в следующем посте.

четверг, 20 января 2011 г.

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

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


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


Отлично, теперь пора перейти к реализации.


Считываю строку в массив


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



#include <stdio.h>

#define N 256

/* Прототип будущей функции */
int findSequences(const char *arr, char **arrPtr);

int main() {
  char charray[N];
  char *sequence[N];
  int count = 0;
  int i;

  printf("Please enter string:\n> ");
  fgets(charray, N, stdin);

  count = findSequences(charray, sequence);

  // ...

  return 0;
}

/* Полное описание функции */
int findSequences(const char *arr, char **arrPtr) {
  // невидимый текст ;)
}

Кое-что о функциях и их прототипах


Прототип функции не является обязательным элементом программы. Он необходим, если нужно обратиться к функции до её полного объявления, и для проверки передаваемых функции параметров. Например, я мог бы написать полное объявление функции findSequences() до функции main() - в таком случае, мне не потребовался бы её прототип. Но у данного способа объявления функций есть два недостатка:

  • Во-первых, если функций много, то главная функция main(), с которой начинается выполнеие любой программы на Си, оказывается где-то далеко в конце файла исходника. Читать такой код стороннему человеку, да и самому тоже, неудобно.
  • Во-вторых,иногда нужно сделать так, чтобы функция f1 вызывала функцию f2, а дать полное описание функции f2 до её использования в функции f1 не представляется возможным. А если из функции f1 нужно вызвать f2, а из f2 - f1 (т.е. необходима косвенная рекурсия)? Тогда без прототипов никак не обойтись.


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


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


Всё это, конечно, хорошо, но... что насчёт функции findSequences()?


В поисках последовательностей...


Посмотрим ещё раз на прототип функции findSequences():



int findSequences(const char *arr, char **arrPtr);


Ясно, что она возвращает значение типа integer. А в качестве параметров принимает ссылку на начало строки (т.е. на начало символьного массива charray, в котором хранится строка), и указатель на массив указателей. Причём, ключевое слово const перед типом параметра говорит, что этот параметр ни за что, ни при каких условиях не должен (и не будет) изменяться. Что мне и нужно - я хочу, чтобы функция только искала и считала последовательности в строке, а не портила её.


Пришло время заглянуть внутрь функции.



int findSequences(const char *arr, char **arrPtr) {
  char *p = arr;
  int count = 0;

  arrPtr[0] = p;
  while(*p && ('\n' != *p))
    if(*p != *(p+1)) 
      arrPtr[++count] = ++p;
    else
      p++;

  return count;
}

В начале, я создаю указатель на тип char и присваиваю ему адрес первого элемента массива arr. Соответственно, первый элемент массива указателей arrPtr должен содержать ссылку на первый элемент arr (т.к. начало массива - это начало последовательности).


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


Цикл while() работает, пока символ по адресу p не равен 0 (т.е. не достигнут конец строки) и не равен '\n' (т.е. символу перевода строки). Звёздочка '*' перед указателем p говорит о том, что я работаю не самим указателем, а с данными, на которые он указывает. В цикле я делаю проверку на определённое событие - если символ *p не равен символу *(p+1), т.е. символу в следующей ячейке массива, то я увеличиваю счётчик найденных последовательностей на 1, передвигаю указатель вправо, чтобы он указывал на следующую ячейку массива, и сохраняю его адрес в массиве указателей. Поскольку знак инкремента '++' стоит до переменной, которую он увеличивает - то действие инкремента, т.е. увеличения значения переменной на единицу, происходит до того, как её значение будет использовано в выражении.


По завершению цикла, я возвращаю значение count, т.е. количество найденных последовательностей в строке.


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