Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 06.11.07 14:18
Оценка: 93 (8)
Предыстория и рассказ, откуда взялась задача Problem K — здесь: О дизайне и ФП
Решаем задачку Problem K — код выложу позже.

Итак, как же будет выглядеть дизайн решения задачи Problem K (http://thesz.livejournal.com/280784.html) в случае Эрланга. Для начала разберемся, какие возможности в плане декомпозиции нам дает Эрланг.

В Эрланге, собственно говоря, есть два основных инструмента для структурирования системы. Первое — модули, второе — процессы. Процесс с точки зрения дизайна является прямым аналогом ООП-шного _объекта_. Он полностью удовлетворяет букве и духу определения объекта Алана Кея. Он имеет состояние (на стеке в бесконечной рекурсии), имеет идентити (pid()), посылает и отправляет сообщения другим процессам, а также сам решает, как обработать полученное сообщение. Кроме того, если сравнить процессы Эрланга с объектами Smalltalk 72, мы получим _полнейшую_ идентичность. Системы не похожи — они просто одинаковы. В случае Эрланга мы, правда, имеем добавочные свойства в виде распределенности, изоляции, и отказоустойчивости. Но это в контексте нашей темы — неважно.

Итак, процессы в Эрланге могут применяться (и применяются) в тех же контекстах и с теми же целями, как объекты в ООП. Кроме того, процессы в Эрланге могут применяться и по другому — как эквивалент техники декомпозиции программы на ленивых списках (потоках) в чистых языках. Таким образом, процессы в Эрланге обеспечивают большую гибкость при декомпозиции задачи, и являются редкой техникой, сочетающей свойства как "потокового" dataflow дизайна, традиционого для "чистых" языков, так и ОО дизайна. В курсе SICP авторы отмечали, что задача "подружить" два этих "ортогональных" подхода — это сложная и актуальная теоретическая проблема. Так вот, процессы Эрланга (а точнее — actors model) — это ее решение, это более общий механизм.

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

Так вот, модуль — это очень простая штука. Это файл, единица раздельной компиляции. Он имеет имя. Он может экспортировать определенные в нем функции. Они вызываются вот так: module_name:function_name( arg1, ..., argN ). И все.

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

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

-module( operation ).
-export( [ tokens/0, create/1, execute/3 ] ).

%% tokens() -> [ char() ].
%% binary operators' token list...
tokens() -> "+-*/".

%% create( char() ) -> bin_op()
%% create binary operator...
create( $+ ) -> fun add/2;
create( $- ) -> fun sub/2;
create( $* ) -> fun mult/2;
create( $/ ) -> fun divd/2.

%% execute( bin_op(), term(), term() ) -> term() | { error, atom() }.
%% Execute binary operation...
execute( Op, A, B ) when is_integer( A ), is_integer( B ) -> Op( A, B );
execute( _, _, _ ) -> { error, '#evaluation' }.

%% Binary operations implementation... 
add( X, Y )  -> X + Y.
mult( X, Y ) -> X * Y.
sub( X, Y ) -> X - Y.

divd( _, 0 ) -> { error, '#division by zero' };
divd( X, Y ) -> X / Y.


Вот так. tokens возвращает список токенов для бинарных операций, create создает из токена операцию, execute ее выполняет. Здорово. Почему мы выбрали для имени модуля существутельное? Давайте посмотрим, как быдут выглядеть вызовы:

Op = operation:create( $+ ).
operation:execute( Op, 1, 2 ).

Просто, красиво, и понятно. И главное — когда мы будем пользоваться этим модулем, нас совершенно не будет волновать, как именно реализована операция. Она у нас АТД. Поэтому, когда мы будем в последствии добавлять операции над строками (часть условия задачи), мы ограничимся модификацией только этого простого модуля. Более того — мы эту модификацию можем кому-нибудь поручить, и он быстро с ней справится.

Теперь я опишу дизайн всего решения. Я его оцениваю примерно на 4-ку, то есть, в принципе — good enough.

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

Она использует cell — это клетка, которая умеет заполняться из текста и преобразовываться в текст, а также умеет рассчитывать свое значение, возможно, изменяя при этом sheet.

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

operation, который реализует вычисление операций.

Еще будет модуль main, который у нас будет "грязный", и он знает, как сохранять и читать таблицу работая со стандартным вводом/выводом. Он взаимодействует только с sheet.

-module( operation ).
-export( [ tokens/0, create/1, execute/3 ] ).
%% Бинарная операция. Можно ее создать, можно выполнить.

tokens() -> [ char() ].
%% binary operators' token list...

create( char() ) -> bin_op()
%% create binary operator...

execute( bin_op(), term(), term() ) -> term() | { error, atom() }.
%% Execute binary operation...

-module( expression ).
-export( [ create/1, evaluate/3 ] ).
%% Выражение. Можно его создать, можно вычислить. Вычисление выражения изменяет таблицу.
%% uses: operation, sheet, cell

create( string() ) -> expression().
%% create expression from string...
%% uses: operation
    
evaluate( Ref, Expression, Sheet ) -> { Value, UpdatedSheet }.
%% evaluate expression...
%% uses: operation, sheet, cell

-module( cell ).
-export( [    create/1, to_string/1, evaluate/2, %% внешний интерфейс 
             eval_and_get/2 ] ). %% Интерфейс для expression
%% Ячейка таблицы
%% uses: expression, sheet

create( string() ) -> cell()
%% create cell from text representation
%% uses: expression

to_string( value() ) -> string().
%% get text representation for the given cell value. Can't handle expressions.

evaluate( Ref, sheet() ) -> sheet().
%% Evaluate cell for the given reference.

eval_and_get( Ref, sheet() ) -> { Value, sheet() }.
%% Evaluate cell and get its value...
%% uses: sheet, expression

-module( sheet ). %% главный модуль - АТД для спредшыта. Его по-хорошему надо на два побить.
-export( [    new/0, set_cell/3, get_cell/2, %% Это интерфейс для нашего вычислительного движка.
        update_row/3, get_row/3, evaluate/1 ] ). %% Это внешний интерфейс для всего спредшыта

new() -> sheet()
%% construct new sheet...

set_cell( Ref, cell(), sheet() ) -> sheet().
%% Ref = { Row, Col }
%% Row = Col = integer()

get_cell( Ref, sheet() ) -> Value.

evaluate( sheet() ) -> sheet().
%% evaluate all expressions...

update_row( Row, [ string() ], sheet() ) -> sheet()
%% update row with a given list of cells

get_row( Row, Cols, sheet() ) -> [ string() ]


Итого, имеем следующие связи модулей (ниже посмотрите детали определения модулей, будет видно, почему так).

main -> sheet <-> cell <-> expression -> operation
          ^-------------------+


Потом нарисую красивую картинку.

Похоже на ОО декомпозицию, правда? Однако — что есть критерий разделения на модули? Ну не инкапсулированное состояние же, в конце концов, как в ООП, нет у нас никакого состояния, программа "чиста". Критерий — вот что. За границами модуля мы не обязаны знать ничего про его внутренние структуры данных. Вот что является критерием.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.