Литературное программирование

Литературное программирование

Дональд Эрвин Кнут

  • 77 лет
  • Почетный профессор многих университетов, в т.ч. СпбГУ
  • Лауреат всего чего только можно в программировании, включая Тьюринговскую премию
  • Автор "искусства программирования"
  • Создатель TeX и METAFONT
  • Автор методологии

А также:

  • Переплюнул Чака Норриса - пишет программы без ошибок
  • Про$@л сроки более чем на 20 лет
  • И ему за это ничего не было
  • Оценил разработку TeX не более чем в 6 месяцев
  • Завершил разработку через 10 лет
  • Считает, что без "литературного программирования" не справился бы с таким большим проектом

Практическое приложение

  • TeX (100 тыс строк) и METAFONT
  • Большая часть GHC (Glasgow Haskell Compiler)
  • Axiom — свободная система компьютерной алгебры общего назначения. Она состоит из среды интерпретатора, компилятора и библиотеки, описывающей строго типизированную, математически правильную иерархию типов.

Если это сработало для них - может сработать и для нас

Обещания

  • В ваших программах не будет ошибок (но это не точно :)
  • Вы осилите программные проекты БОЛЬШОГО размера
  • Это можно сочетать с другими подходами...
  • ...и вашим любимым языком программирования
  • Вам будет проще объяснить смысл жизни коллегам
  • ... и самому себе через полгода после написания кода

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

В чем суть?

Давайте изменим традиционные приоритеты в создании программ: вместо представления о нашей задаче как о создании инструкций "Что делать?" для компьютера сконцентрируемся на объяснении другим людям описаний нашего видения того, что под управлением программы должен делать компьютер.

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

Дональд Кнут

Пишем программу как научную статью

  • Цель
  • Методика исследований
  • Лабораторное оборудование
  • Описание эксперимента
  • Технология измерений
  • Результаты
  • Область применения
  • Дальнейшие переспективы исследования
  • Использованная литература

...или как захватывающий детектив

  • Экспозиция
  • Предзнаменования
  • Завязка
  • Конфликт
  • Нарастающее действие
  • Кульминация
  • Развязка

Если проект будет провален - это будет уже трагедия :)

Пишем программу как научную статью

  • Задача
  • Способ решения
  • Инфраструктура
  • Бизнес-логика
  • Требования и сроки
  • Интерфейс пользователя
  • Интеграционные тесты
  • Внедрение и развертывание
  • Зависимости и библиотеки

Пистолет.стрелять();

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

История - это не даты революций и войн!

Пример

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

Программа TANGLE

* Описание задачи

Начнем писать на HTML и будем вставлять именованные блоки кода в него вот так:

Программа TANGLE

* Описание задачи...

* Язык разметки

<html>

<head><title>Описание программы</title></head>

<body>

   <p>А это точка входа в программу:</p>

   <pre id="main.c">

    int main(int argc, char *argv[]) {...}

   </pre>

</body>

</html>

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

getchunk:

Программа TANGLE

* Описание задачи...

* Язык разметки

...

   <getchunk id="main.c">

</body>

</html>

Вот наш цикл разработки:

 

 do forever {
    edit the html file
    tangle thehtmlfile sub1.c >sub1.c
    tangle thehtmlfile sub2.c >sub2.c
    tangle thehtmlfile main.c >main.c
    gcc -o myfile sub1.c sub2.c main.c
    ./myfile
 }

Программа TANGLE

* Описание задачи...

* Язык разметки

* Компиляция и сборка

   ** Цикл разработки

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

 

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

 

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

Программа TANGLE

* Компиляция и сборка

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

 

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

Программа TANGLE

* Главная задача

Переменная буфер описывается как указатель на первый символ строки

 

   <pre id="main.c">

   /* a memory mapped buffer copy of the file */
   char *buffer;
   int bufsize;

   </pre>

 

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

Программа TANGLE

* Главная задача

Мы можем загрузить файл в один блок с помощью функции READ. Для этого нужно чтобы мы могли открыть файл, а это в свою очередь, дает нам дескриптор файла, который мы получаем от вызова OPEN. Нам нужен размер файла, так что мы должны выделить память, так что мы используем FSTAT..

 

int main(int argc, char *argv[]) {
  int fd;
  struct stat filestat;
  if ((argc != 2)) { 
    perror("Usage: tangle filename chunkname");
    exit(-1);
  }

     ...

Программа TANGLE

* Главная задача

  fd = open(argv[1], O_RDONLY);
  if (fd == -1) {
    perror("Error opening file for reading");
    exit(-2);
  }
  if (fstat(fd,&filestat) < 0) {
    perror("Error getting input file size");
    exit(-3);
  }
  bufsize = (int)filestat.st_size;
  buffer = (char *)malloc(bufsize);
  read(fd,buffer,bufsize);
  fixHTMLcode(); getchunk(argv[2]);
  close(fd);  return(0);
}

Программа TANGLE

* Главная задача

Смысл ясен?

TANGLE - THE PROGRAM

* Описание задачи

* Язык разметки

* Компиляция и сборка

   ** Makefile

* Дедлайн

* Планы по расширению функционала

   ** Поддержка markdown

   ** Создание PDF

* Интеграция с другими программами

* Зависимости от других библиотек

Что мы достигли на этом этапе?

  • Код разбит на кусочки...
  • ...и вставлен в глобальное описание программы (т.н. единый источник)
  • Есть утилиты которые превращают описание в чистый код, для отправки компилятору...
  • ...и чистое описание (например в pdf)

Что это дает?

  • Если коллеге что-то непонятно - RTFM
  • Если сам запутался - RTFM
  • Перекрестное программирование - больше не боль
  • Программу можно показать не программисту (например инженер-схемотехник разберется)

Что мы можем еще достичь?

Вставим один кусочек в другой:

(labels ((match-tree (tree f-predict &optional (if-match :return-first-match))
         (cond ((null tree) nil)
               ((atom tree) nil)
               (t
                <<cons>>))))
  <<call>>)
cons:
(if (funcall f-predict tree)
    <<match_ok>>
    <<sub_trees>>)
sub_trees:
(cons
 (funcall #'match-tree (car tree) f-predict if-match)
 (funcall #'match-tree (cdr tree) f-predict if-match))

А в другой - третий:

Сборка из кусочков

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

(defun tree-match (tree predict &optional (if-match :return-first-match))
  (let ((collect))
    (labels ((match-tree (tree f-predict &optional (if-match :return-first-match))
             (cond ((null tree) nil)
                   ((atom tree) nil)
                   (t
                    (if (funcall f-predict tree)
                        (cond ((equal if-match :return-first-match)
                               (return-from tree-match tree))
                              ((equal if-match :return-first-level-match)
                               (setf collect
                                     (append collect (list tree))))
                              ((equal if-match :return-all-match)
                               (progn
                                   (setf collect
                                         (append collect (list tree)))
                                   (cons
                                    (funcall #'match-tree (car tree) f-predict if-match)
                                    (funcall #'match-tree (cdr tree) f-predict if-match))))
                              ((equal 'function (type-of if-match))
                               (funcall if-match tree))
                              (t (error 'strategy-not-implemented)))
                        (cons
                         (funcall #'match-tree (car tree) f-predict if-match)
                         (funcall #'match-tree (cdr tree) f-predict if-match)))))))
      (match-tree tree predict if-match)
      collect)))

Пойдем дальше

Представим кусочек

#+NAME: sub_trees
#+BEGIN_SRC lisp
  (cons
   (funcall #'match-tree (car tree) f-predict if-match)
   (funcall #'match-tree (cdr tree) f-predict if-match))
#+END_SRC

В виде функции, которая возвращает его содержимое:

function sub_trees () {
  return "
  (cons
   (funcall #'match-tree (car tree) f-predict if-match)
   (funcall #'match-tree (cdr tree) f-predict if-match))
  ";
}

Вызов функций

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

#+NAME: cons
#+BEGIN_SRC lisp
  (if (funcall f-predict tree)
      <<match_ok>>
      <<sub_trees>>)
#+END_SRC

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

function sub_trees (match_ok, sub_trees) {
  return "
  (if (funcall f-predict tree)
      <<match_ok>>
      <<sub_trees>>)
  ";
}

Может!

#+NAME: cons
#+BEGIN_SRC lisp
  (if (funcall f-predict tree)
      <<match_ok>>
      <<sub_trees>>)
#+END_SRC
  • Выполнять любые преобразования параметров
  • Ходить в БД
  • Строить и выводить графики
  • Запускать сторонние утилиты
  • И даже лазить в интернет..
function sub_trees (match_ok, sub_trees) {
  return "
  (if (funcall f-predict tree)
      <<match_ok>>
      <<sub_trees>>)
  ";
}

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

Например:

#+CAPTION: Данные вакансии
#+NAME: vacancy_flds
| field name  | field type           | note               |
|-------------+----------------------+--------------------|
| id          | serial               | идентификатор      |
| name        | varchar              | название вакансии  |
| currency    | (or db-null varchar) | валюта зарплаты    |
| salary      | (or db-null integer) | размер компенсации |
| emp-id      | (or db-null integer) | работодатель       |
| descr       | varchar              | описание вакансии  |
#+results: asm_vacancy
#+begin_example
(define-entity vacancy
  ((id serial)
   (name varchar)
   (currency (or db-null varchar))
   (salary (or db-null integer))
   (emp-id (or db-null integer))
   (descr varchar)))

(make-vacancy-table)
#+NAME: asm_vacancy
#+BEGIN_SRC emacs-lisp :var flds=vacancy_flds
   (gen-entity "vacancy" flds)
#+END_SRC
#+NAME: gen_entity
#+BEGIN_SRC emacs-lisp
  (defun gen-entity (nameflds)
    (let ((result))
      (push (format "(define-entity %s" name) result)
      (push (gen-fields flds) result)
      (push ")\n" result)
      (push "\n" result)
      (push (format "(make-%s-table)\n" name) result)
      (mapconcat 'identity (reverse result) "")))
#+END_SRC
#+NAME: entityes
#+BEGIN_SRC lisp
   <<asm_vacancy>

   <<asm_employer>>

   <<asm_applicant>>

   ...
#+END_SRC

Это программирование программирования!

Что там еще есть?

Что там еще есть?

Что там еще есть?

Что там еще есть?

Что там еще есть?

Еще примеров?

<<*>>=
MODULE quick;
IMPORT IO;
<<Constants, types, and global variables>>
<<The QuickSort Procedure>>
BEGIN
  <<Get some numbers>>
  <<Sort the Numbers>>
  <<Print the numbers>>
END quick.
@ This program demonstrates the {\tt QuickSort} algorithm. It reads
a list of numbers from the standard input, sorts them, and writes the
sorted results to standard output.

<<The QuickSort Procedure>>=
PROCEDURE QuickSort(VAR a : ARRAY OF INTEGER; left, right: INTEGER);
<<Local variables>>
BEGIN
  <<Sort and divide until there's nothing left to do>>
END QuickSort;
@ Recursively sort an array {\tt a} of integers. {\tt left} and
{\tt right} denote the leftmost and rightmost elements of the array.

<<Sort and divide until there's nothing left to do>>=
IF right > left THEN
  <<Get set by guessing a cut value and initialising indexes>>
  <<Sort array with respect to cut value>>
  <<Recursively sort array left of cut value>>
  <<Recursively sort array right of cut value>>
END (* if *);
@ When we make a recursive call where the right and left indexes are
the same, then we've divided down to nothing and we're done with this
recursive thread.

<<Get set by guessing a cut value and initialising indexes>>=
cutval := a[right]; (*arbitrary start for partition*)
lo := left - 1;
hi := right;
@ Arbitrarily pick the rightmost element of the array as the cut value
for this pass.
<<Sort array with respect to cut value>>=
REPEAT
  <<Find an out of order number from left>>
  <<Find an out of order number from right>>
  <<Swap them>>
UNTIL hi <= lo;
<<Undo Extra Swap>>

Еще примеров?

<<Sort and divide until there's nothing left to do>>=
IF right > left THEN
  <<Get set by guessing a cut value and initialising indexes>>
  <<Sort array with respect to cut value>>
  <<Recursively sort array left of cut value>>
  <<Recursively sort array right of cut value>>
END (* if *);
@ When we make a recursive call where the right and left indexes are
the same, then we've divided down to nothing and we're done with this
recursive thread.

<<Get set by guessing a cut value and initialising indexes>>=
cutval := a[right]; (*arbitrary start for partition*)
lo := left - 1;
hi := right;
@ Arbitrarily pick the rightmost element of the array as the cut value
for this pass.
<<Sort array with respect to cut value>>=
REPEAT
  <<Find an out of order number from left>>
  <<Find an out of order number from right>>
  <<Swap them>>
UNTIL hi <= lo;
<<Undo Extra Swap>>

Еще примеров?

Почему это работает?

  • Мы понимаем сложную систему, понимая её простые части.
  • Программирование не следует иерархической структуре. Это не "снизу-вверх" и не "сверху-вниз".
  • Если мы выражаем программы как сеть идей, мы можем подчеркнуть их структурные свойства естественным и удовлетворительным образом.
  • Определения в программах не могут быть поставлены в любом месте. Поэтому порядок инструкций важен для работы программы
  • Но этот порядок становится проблемой для нас. "Порядок сознания" оказывается нарушен.
  • Мы должны обеспечить инструменты для изменения порядка, форматирования и реструктуризации текста.

Таким образом

LP - это макропроцессор над семантической сетью концепций

Таким образом

LP - это макропроцессор над семантической сетью концепций

А если совместить его с уже известными макропроцесорами и семантическими сетями?

А если совместить его с уже известными макропроцесорами и семантическими сетями?

А если совместить его с уже известными макропроцесорами и семантическими сетями?

  • Интерактивная компиляция

  • Горячая замена кода

  • Интроспекция на лету

Что это дает?

Инструментарий

Literate DevOps

Literate DevOps

Поддержка серверов состоит из двух фаз:

  • Взрыв мозга до начала работ на сервере
  • Управление конфигурацей с использованием maintaince automation tools
  • Puppet
  • Chief
  • Ansible
  • Salt

Literate DevOps

Задачи:

  • Администрирование пользователей
  • Управление конфигурацией приложений
  • Решение внеочередных задач

Literate DevOps

C-c C-c

Literate DevOps

Orgmode - сесии

Literate DevOps

Перегрузка org-babel-sh-command

Literate DevOps

Перегрузка org-babel-sh-command

Orgmode - сесии

  • Один ssh-коннект на все выполняемые команды
  • Можно использовать состояния в переменных
  • Перезапускает ssh-соединение
  • Состояние сбрасывается при каждом выполнении блока

Screen

  • Не позволяет создавать состояние в переменных

ssh.el

(https://github.com/ieure/ssh-el)

ob-screen extension

(org-mode Contrib collection)

Literate DevOps

DRAWER: Verbose Commands

Literate DevOps

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

Literate DevOps

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

Literate DevOps

Установка переменных и их значений

Волк, коза и капуста

Поиск в пространстве состояний

* Бизнес-логика:

Заказчик хочет переправиться на другой берег реки с козой, капустой и волком

На берегу реки находится лодка, которой должен управлять человек

Лодка одновременно может перевозить не более 2-х пассажиров (включая человека)

Если оставить волка на берегу с козой в отсутствии человека - он ее сьест

Если оставить козу на берегу с капустой в отсутствии человека - она ее сьест

Требуется выработать последовательность переправ через реку, чтобы перевезти всех

* Состояния мира:

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

(e w e w) - человек и коза на левом берегу

* Абстракция состояния:

Определим конструктор типа "состояние" и функции доступа:

(defun make-state (f w g s)
  (list f w g s))

(defun farmer-side (state)
  (nth 0 state))

(defun wolf-side (state)
  (nth 1 state))

(defun goat-side (state)
  (nth 2 state))

(defun cabbage-side (state)
  (nth 3 state))

Оставшаяся часть программы основыватся на этих функциях

* Абстракция состояния:

При каждом переходе в новое состояние функции доступа используются для разделения состояния на его компоненты. Функция opposite, которая будет вскоре определена, меняет положение объекта, пересекающего реку, а функция make-state формирует новое состояние.

* Абстракция состояния:

Например функцию farmer-takes-self можно определить так:

(defun farmer-takes-self (state)
  (make-state (opposite (farmer-side state))
              (wolf-side state)
              (goat-side state)
              (cabbage-side state)))

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

Мы будем выполнять проверку состяния на безопасность в момент переправы - перехода из одного состояния в другое с помощью формы safe, которая вскоре будет определена:

(safe '(w w w w)) ;; состяние безопасно
(safe '(e w w e)) ;; волк сьест козу, вернуть ложь
(safe '(w w e e)) ;; коза сьест капусту, вернуть ложь

* Абстракция состояния:

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

Рекурсивный алгоритм поиска с возвратами проверяет возвращаемое значение, и если оно = ложь - отбрасывает это состояние

С учетом safe четыре возможных формы перехода можно определить следующим образом:

(defun farmer-takes-self (state)
  (safe (make-state (opposite (farmer-side state))
                    (wolf-side state)
                    (goat-side state)
                    (cabbage-side state))))

(defun farmer-takes-wolf (state)
  (cond ((equal (farmer-side state) (wolf-side state))
         (safe (make-state (opposite (farmer-side state))
                           (opposite (wolf-side state))
                           (goat-side state)
                           (cabbage-side state))))
        (t nil)))

(defun farmer-takes-goat (state)
  (cond ((equal (farmer-side state) (goat-side state))
         (safe (make-state (opposite (farmer-side state))
                           (wolf-side state)
                           (opposite (goat-side state))
                           (cabbage-side state))))
        (t nil)))


(defun farmer-takes-cabbage (state)
  (cond ((equal (farmer-side state) (cabbage-side state))
         (safe (make-state (opposite (farmer-side state))
                           (wolf-side state)
                           (goat-side state)
                           (opposite (cabbage-side state)))))
        (t nil)))

* Абстракция состояния:

В функциях перехода используется функция opposite, возвращающая местоположение, противоположное текущему:

(defun opposite (side)
  (cond ((equal side 'e) 'w)
        ((equal side 'w) 'e)))

* Абстракция состояния:

Функция safe позволяет проверить два опасных условия:

(defun safe (state)
  (cond ((and (equal (goat-side state) (wolf-side state))
              (not (equal (farmer-side state) (wolf-side state))))
         nil)
        ((and (equal (goat-side state) (cabbage-side state))
              (not (equal (farmer-side state) (goat-side state))))
         nil)
        (t state)))
  • Человек находится на противоположном берегу от волка и козы
  • Человек находится на противоположном берегу от козы и капусты

* Поиск с возвратами:

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

(defun path (state goal)
  (cond ((equal state goal) 'success)
        (t (or (path (farmer-takes-self state) goal)
               (path (farmer-takes-wolf state) goal)
               (path (farmer-takes-goat state) goal)
               (path (farmer-takes-cabbage state) goal)))))

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

* Поиск с возвратами:

Еще одним вопросом, требущим решения является выявление циклов в пространстве поиска. Добавляем список посещенных состояний

(defun path (state goal been-list)
  (cond ((null state) nil)
        ((equal state goal)
         (reverse (cons state been-list)))
        ((member state been-list :test #'equal)
         (prog1 nil
           (format t "~%fail: this is old path~%")))
        ((not (member state been-list :test #'equal))
         (or (path (farmer-takes-self state) goal (cons state been-list))
             (path (farmer-takes-wolf state) goal (cons state been-list))
             (path (farmer-takes-goat state) goal (cons state been-list))
             (path (farmer-takes-cabbage state) goal (cons state been-list))))))

* Запуск:

Попробуем найти решение, с помощью нашей программы:

success!
|           |           |           |           |
| FARMER    | WOLF      | GOAT      | CABBAGE   |

| FARMER    |           | GOAT      |           |
|           | WOLF      |           | CABBAGE   |

|           |           | GOAT      |           |
| FARMER    | WOLF      |           | CABBAGE   |

| FARMER    | WOLF      | GOAT      |           |
|           |           |           | CABBAGE   |

|           | WOLF      |           |           |
| FARMER    |           | GOAT      | CABBAGE   |

| FARMER    | WOLF      |           | CABBAGE   |
|           |           | GOAT      |           |

|           | WOLF      |           | CABBAGE   |
| FARMER    |           | GOAT      |           |

| FARMER    | WOLF      | GOAT      | CABBAGE   |
|           |           |           |           |

LP + Lisp - Q|A?

практикуйте наше кун-фу!

mailto://i.am.rigidus@gmail.com

https://github.com/rigidus

http://rigidus.ru