Решение 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
          ^-------------------+


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

Похоже на ОО декомпозицию, правда? Однако — что есть критерий разделения на модули? Ну не инкапсулированное состояние же, в конце концов, как в ООП, нет у нас никакого состояния, программа "чиста". Критерий — вот что. За границами модуля мы не обязаны знать ничего про его внутренние структуры данных. Вот что является критерием.
Re: Решение Problem K на Эрланг: дизайн
От: Трурль  
Дата: 07.11.07 10:56
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Надо побить наше решение на модули, таким образом, чтобы связность между ними была настолько слаба, насколько это возможно.


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


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


G>Может я чего-то не учел, но связи можно сделать и попроще.


main -> sheet -> cell -> expression -> operation
Re[2]: Код expression, sheet, cell: задачка
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 12:20
Оценка:
Здравствуйте, Трурль, Вы писали:

G>>Может я чего-то не учел, но связи можно сделать и попроще.


Т>
Т>main -> sheet -> cell -> expression -> operation
Т>


Так не получится. Вся сложность в алгоритме expression. Он вычисляет клетки рекурсивно. Когда он встречает в аргументе reference, ему надо вычислить значение в клетке, а там также может быть expression. Отсюда протягивается обратная ссылка на cell. Придется мне привести код, чтобы стало понятно.

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

-module( expression ).
-export( [ create/1, evaluate/3 ] ).

%% create( string() ) -> expression().
%% create expression from text...
create( Expr ) ->
    Pos = string:cspan( Expr, operation:tokens() ),
    case lists:split( Pos, Expr ) of
        { Val, [ Op | Rest ] } ->
            { create_arg( Val ), operation:create( Op ), create( Rest ) };
        { Val, [] } -> create_arg( Val )
    end.

%% create_arg( string() ) -> Ref | integer() | Error
%% Ref = { integer(), integer() }
%% Error = { error, atom() }
create_arg( [ Row, Col ] ) when Row >= $a, Row =< $z, Col >= $0, Col =< $9 ->
    { ref, { Row - $a + 1, Col - $0 } };
create_arg( Number ) -> cell:create_number( Number ).
    
%% evaluate( Ref, Expression, Sheet ) -> { Value, UpdatedSheet }.
%% evaluate expression...
evaluate( _, updating, Sheet ) -> { { error, '#cycle' }, Sheet };
evaluate( Ref, Expr, Sheet ) ->
    Sheet_1 = sheet:set_cell( Ref, updating, Sheet ),
    { Value, Sheet_2 } = evaluate( Expr, Sheet_1 ),
    { Value, sheet:set_cell( Ref, Value, Sheet_2 ) }.

%% evaluate( Expression, Sheet ) -> { Value, Sheet }
%% Evaluate expression. Forces evaluation of all referenced cells...
evaluate( { A, Op, B }, Sheet ) ->
    { ResA, Sheet_1 } = evaluate( A, Sheet ),
    { ResB, Sheet_2 } = evaluate( B, Sheet_1 ),
    { operation:execute( Op, ResA, ResB ), Sheet_2 };
evaluate( { ref, Ref }, Sheet ) -> cell:eval_and_get( Ref, Sheet );
evaluate( Val, Sheet ) -> { Val, Sheet }.


-module( cell ).
-export( [    create/1, create_number/1, to_string/1,
            evaluate/2, eval_and_get/2 ] ).

%% create( string() ) -> cell()
%% create cell from text representation
create( "" ) -> "";
create( [ $' | Text ] ) -> Text;
create( [ $= | Expr ] ) -> { expr, expression:create( Expr ) };
create( Number ) -> create_number( Number ).

%% create_number( string() ) -> integer() | { error, '#nan' }.
%% create number from string...
create_number( Number ) -> 
    case string:to_integer( Number ) of
        { Value, [] } -> Value;
        _ -> { error, '#nan' }
    end.

%% to_string( value() ) -> string().
%% get text representation for the given cell value
to_string( Int ) when is_integer( Int ) -> integer_to_list( Int );
to_string( { error, Msg } ) -> atom_to_list( Msg );
to_string( Text ) -> Text.

%% evaluate( Ref, sheet() ) -> sheet().
%% Evaluate cell value for the given reference
evaluate( Ref, Sheet ) -> 
    { _, Sheet_1 } = eval_and_get( Ref, Sheet ),
    Sheet_1.

%% eval_and_get( Ref, sheet() ) -> { Value, sheet() }.
%% Evaluate cell and get its value...
eval_and_get( Ref, Sheet ) ->
    case sheet:get_cell( Ref, Sheet ) of
        { expr, Expr } -> expression:evaluate( Ref, Expr, Sheet );
        Value -> { Value, Sheet }
    end.


-module( sheet ).
-export( [    new/0, set_cell/3, get_cell/2, 
            update_row/3, get_row/3, create_cell/2, cell_as_string/2, evaluate/1 ] ).

%% new() -> sheet()
%% construct new sheet...
new() -> gb_trees:empty().

%% set_cell( Ref, cell(), sheet() ) -> sheet().
%% Ref = { Row, Col }
%% Row = Col = integer()
set_cell( _, "", Sheet ) -> Sheet; %%% do NOT store empty values...
set_cell( Ref, Value, Sheet ) -> gb_trees:enter( Ref, Value, Sheet ).

%% get_cell( Ref, sheet() ) -> Value.
get_cell( Ref, Sheet ) -> 
    case gb_trees:lookup( Ref, Sheet ) of
        { value, Value } -> Value;
        _ -> ""
    end.

%% evaluate( sheet() ) -> sheet().
%% evaluate all expressions...
evaluate( Sheet ) -> lists:foldl( fun cell:evaluate/2, Sheet, gb_trees:keys( Sheet ) ).

%% update_row( Row, [ string() ], sheet() ) -> sheet()
%% update row 
update_row( Row, Cells, Sheet ) ->
    Refs = [ { Row, Col } || Col <- lists:seq( 1, lists:length( Cells ) ) ],
    lists:foldl( fun create_cell/2, Sheet, lists:zip( Refs, Cells ) ).

%% get_row( Row, Cols, sheet() ) -> [ string() ]
get_row( Row, Cols, Sheet ) ->
    [ cell_as_string( { Row, Col }, Sheet ) || Col <- lists:seq( 1, Cols ) ].

%% get_cell_text( Ref, sheet() ) -> string().
cell_as_string( Ref, Sheet ) -> cell:to_string( get_cell( Ref, Sheet ) ).

%% create_cell( { Ref, string() }, sheet() ) -> sheet().
create_cell( { Ref, Text }, Sheet ) -> set_cell( Ref, cell:create( Text ), Sheet ).
Re[2]: Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 12:24
Оценка:
Здравствуйте, Трурль, Вы писали:

Вообще — обрати внимание, насколько прост дизайн в sequentional erlang. Единственное, над чем надо думать — группировка функций по модулям. Это удивительно, настолько простого и эффективного подхода к дизайну я нигде еще не встречал. Крупно налажать в дизайне практически негде, рефакторинг элементарен.
Re[3]: Решение Problem K на Эрланг: дизайн
От: Курилка Россия http://kirya.narod.ru/
Дата: 07.11.07 12:32
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Здравствуйте, Трурль, Вы писали:


G>Вообще — обрати внимание, насколько прост дизайн в sequentional erlang. Единственное, над чем надо думать — группировка функций по модулям. Это удивительно, настолько простого и эффективного подхода к дизайну я нигде еще не встречал. Крупно налажать в дизайне практически негде, рефакторинг элементарен.


А какие виды реально значимого рефакторинга для Эрланга можешь назвать?
Re[3]: дизайн-паттерны, Эрланг
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 12:37
Оценка: 8 (1)
А вот дизайн-паттерны, как это ни смешно, есть.

1) Абстрактный тип данных.
В модуле описывается функция конструктор абстрактного типа данных, а также функции-манипуляторы с ним. Пользователь модуля не знает внутреннее устройство типа данных. Пример — gb_trees из стандартной библиотеки. Собственно, у этого паттерна нулевой "синтаксический оверхэд", его можно наблюдать в моем коде.

2) "Динамический" абстрактный тип данных.
Если надо написать generic алгоритм, который оперирует на разных реализациях одного АТД, делается так. Состояние АТД — тупл, первый элемент которого — имя модуля, в котором определен АТД. Тогда:

type( ADT ) -> element( 0, ADT ).

dynamic_generic_function( ADT, Arg1, ..., ArgN ) -> type( ADT ):dynamically_dispatched_call( ADT, Arg1, ..., ArgN ).

3) behaviours. Примеры и документация есть в OTP.

Еще один способ динамической диспетчеризации более легковесный, он основан на использовании функций и приведен в моем модуле operation. На design pattern это, право слово, не тянет.
Re[4]: Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 12:46
Оценка: 16 (2)
Здравствуйте, Курилка, Вы писали:

К>Здравствуйте, Gaperton, Вы писали:


G>>Здравствуйте, Трурль, Вы писали:


G>>Вообще — обрати внимание, насколько прост дизайн в sequentional erlang. Единственное, над чем надо думать — группировка функций по модулям. Это удивительно, настолько простого и эффективного подхода к дизайну я нигде еще не встречал. Крупно налажать в дизайне практически негде, рефакторинг элементарен.


К>А какие виды реально значимого рефакторинга для Эрланга можешь назвать?


1) Передвинуть функцию из одного модуля в другой. Это тривиально, и может быть автоматизировано.
2) По другому переразбить кусочек кода на функции, с одновременным изменением местоположения функций по модулям. Собственно, я занимался этим в перерывах между работой несколько дней, выполнял глубокую перегруппировку раза 4. Это основное.
3) Накатить design pattern из моего соседнего поста на монолитный код. Одна проблема — паттерны элементарны, и все на самом деле сведется к пункту 2.

Это если говорить о sequentional erlang. А вот когда мы задействуем процессы, что в более крупной системе обязательно произойдет, вот тут-то самое интересное и начнется. Вот это будет уже больше похоже на геморройный дизайн, к которому мы привыкли. Но не попробуешь — не узнаешь.
Re[5]: Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 12:54
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Здравствуйте, Курилка, Вы писали:


К>>Здравствуйте, Gaperton, Вы писали:


G>>>Здравствуйте, Трурль, Вы писали:


G>>>Вообще — обрати внимание, насколько прост дизайн в sequentional erlang. Единственное, над чем надо думать — группировка функций по модулям. Это удивительно, настолько простого и эффективного подхода к дизайну я нигде еще не встречал. Крупно налажать в дизайне практически негде, рефакторинг элементарен.


К>>А какие виды реально значимого рефакторинга для Эрланга можешь назвать?


G>1) Передвинуть функцию из одного модуля в другой. Это тривиально, и может быть автоматизировано.

G>2) По другому переразбить кусочек кода на функции, с одновременным изменением местоположения функций по модулям. Собственно, я занимался этим в перерывах между работой несколько дней, выполнял глубокую перегруппировку раза 4. Это основное.
G>3) Накатить design pattern из моего соседнего поста на монолитный код. Одна проблема — паттерны элементарны, и все на самом деле сведется к пункту 2.

G>Это если говорить о sequentional erlang. А вот когда мы задействуем процессы, что в более крупной системе обязательно произойдет, вот тут-то самое интересное и начнется. Вот это будет уже больше похоже на геморройный дизайн, к которому мы привыкли. Но не попробуешь — не узнаешь.


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

А вот с процессами мы таки получим обратно изменяемое состояние и референсы. Тогда и начнется самое интересное.
Re[4]: дизайн-паттерны, Эрланг
От: Курилка Россия http://kirya.narod.ru/
Дата: 07.11.07 13:14
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>3) behaviours. Примеры и документация есть в OTP.


Единственное что не очень понравилось — композицию в смысле наследования (скажем банкомат, который ещё и сервер) из них делать не очень удобно, хотя это не сильно надо, не ООП ведь. Наверное лучше это делегированием решать
Re[5]: дизайн-паттерны, Эрланг
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 13:18
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Здравствуйте, Gaperton, Вы писали:


G>>3) behaviours. Примеры и документация есть в OTP.


К>Единственное что не очень понравилось — композицию в смысле наследования (скажем банкомат, который ещё и сервер) из них делать не очень удобно, хотя это не сильно надо, не ООП ведь. Наверное лучше это делегированием решать


Ну, можно делать композицию и не в смысле наследования. Ты законченный пример приведи, в котором ты считаешь тебе наследование поможет. А мы посмотрим, как это сделать идио... матическим образом
Re[6]: дизайн-паттерны, Эрланг
От: Курилка Россия http://kirya.narod.ru/
Дата: 07.11.07 13:25
Оценка: 13 (1)
Здравствуйте, Gaperton, Вы писали:

G>Здравствуйте, Курилка, Вы писали:


К>>Здравствуйте, Gaperton, Вы писали:


G>>>3) behaviours. Примеры и документация есть в OTP.


К>>Единственное что не очень понравилось — композицию в смысле наследования (скажем банкомат, который ещё и сервер) из них делать не очень удобно, хотя это не сильно надо, не ООП ведь. Наверное лучше это делегированием решать


G>Ну, можно делать композицию и не в смысле наследования. Ты законченный пример приведи, в котором ты считаешь тебе наследование поможет. А мы посмотрим, как это сделать идио... матическим образом


Да была задача аля http://www.trapexit.org/Cascading_Behaviours только у меня не fsm, а server был, и ничего, вполне себе работало.
Re[6]: Решение Problem K на Эрланг: дизайн
От: Cyberax Марс  
Дата: 07.11.07 13:34
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Вообще — общий вывод такой: функциональная чистота радикально упрощает дизайн — фактически, она устраняет его как геморройную и ответственную фазу, к которой мы привыкли в ОО. Конкретнее — спасает отсутствие указателей, референсов, и изменяемого состояния. Из-за этого рефакторинг обходится дешевле на порядок — его просто не замечаешь. И дизайн элементарен — это уже не настолько трудоемкая и ответственная фаза. Думать нужно только о связности и группировке функций. Халява полная.

Иногда, тем не менее, оно сильно мешает. Сейчас пишу на Эрланге модуль к ejabberd — в паре мест существенно помогла бы возможность иметь нормальные мутабельные переменные. Определенные типы рефакторингов (например, изменение форматов туплов) тоже не очень приятно делаются.

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

G>А вот с процессами мы таки получим обратно изменяемое состояние и референсы. Тогда и начнется самое интересное.

Процесс вместо объекта в каждом случае, ИМХО, слишком — даже для Эрланга. Объекты в ОО — это самодостаточные легковесные сущности, а процессы в Эрланге все же находятся под контролем планировщика и требуют глобальной регистрации.
Sapienti sat!
Re[7]: Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 13:53
Оценка:
Здравствуйте, Cyberax, Вы писали:

C>Здравствуйте, Gaperton, Вы писали:


G>>Вообще — общий вывод такой: функциональная чистота радикально упрощает дизайн — фактически, она устраняет его как геморройную и ответственную фазу, к которой мы привыкли в ОО. Конкретнее — спасает отсутствие указателей, референсов, и изменяемого состояния. Из-за этого рефакторинг обходится дешевле на порядок — его просто не замечаешь. И дизайн элементарен — это уже не настолько трудоемкая и ответственная фаза. Думать нужно только о связности и группировке функций. Халява полная.

C>Иногда, тем не менее, оно сильно мешает. Сейчас пишу на Эрланге модуль к ejabberd — в паре мест существенно помогла бы возможность иметь нормальные мутабельные переменные.

В чем проблема? Используй словарь процесса. Если, конечно, ты не боишься .

C> Определенные типы рефакторингов (например, изменение форматов туплов) тоже не очень приятно делаются.

1) Делается это просто — достаточно найти все вхождения функции, возвращающей тупл, и сделать замену. Ничего особо неприятного — не сложнее, чем поменять местами параметры функции.
2) У тебя туплы сложного формата между модулями ходят? Это нехорошо, батенька. Не надо так делать — ошибка дизайна. А если без этого никак — в таких случаях erlang style guide рекомендует применять record-ы и делать декларации в отдельных модулях. А в пределах одного модуля изменить туплы — это не рефакторинг. Это фигня.

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

Это языки группы ML. F#, OCaml, и прочее.

G>>А вот с процессами мы таки получим обратно изменяемое состояние и референсы. Тогда и начнется самое интересное.

C>Процесс вместо объекта в каждом случае, ИМХО, слишком — даже для Эрланга. Объекты в ОО — это самодостаточные легковесные сущности, а процессы в Эрланге все же находятся под контролем планировщика и требуют глобальной регистрации.

Его никто и не применяет в каждом случае. Общая философия такая — процессы применяются на для декомпозиции на макроуровне, чистый ФП стиль — на микроуровне.
Чтобы понять, в каких пределах можно применять процесс как объект — один процесс занимает порядка килобайта — их запросто можно сотню тысяч закрутить без деградации производительности. Короче, эрланговские процессы не треды, их можно не считать и не трястись над ними.
Re[7]: дизайн-паттерны, Эрланг
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 13:56
Оценка:
Здравствуйте, Курилка, Вы писали:

К>Здравствуйте, Gaperton, Вы писали:


G>>Здравствуйте, Курилка, Вы писали:


К>>>Здравствуйте, Gaperton, Вы писали:


G>>>>3) behaviours. Примеры и документация есть в OTP.


К>>>Единственное что не очень понравилось — композицию в смысле наследования (скажем банкомат, который ещё и сервер) из них делать не очень удобно, хотя это не сильно надо, не ООП ведь. Наверное лучше это делегированием решать


G>>Ну, можно делать композицию и не в смысле наследования. Ты законченный пример приведи, в котором ты считаешь тебе наследование поможет. А мы посмотрим, как это сделать идио... матическим образом


К>Да была задача аля http://www.trapexit.org/Cascading_Behaviours только у меня не fsm, а server был, и ничего, вполне себе работало.


Ну вот, тут уже все написано.

Result = Mod:handle_event(Event, ExtStateName, ExtStateData),

И все дела.
Re[3]: Код expression, sheet, cell: задачка
От: red75  
Дата: 07.11.07 14:05
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Кстати, приглашаю поиграть в "пятнашки". Помоги мне, попробуй улучшить дизайн. Принципы дизайна, которые надо сохранить — очень простые: если ты посмотришь внимательно на код, то ты обратишь внимание, что модули не знают про структуры данных друг друга — между ними ходят либо абстрактные, либо очень простые типы данных. Предлагается это свойство сохранить, и попытаться уменьшить связность.


Хм. А ведь Cell — это, в общем-то, простое хранилище, однозначно определяемое по {Ref,Table}. Почему-бы не вынести cell:evaluate, cell:eval_and_get в Table?
Re[8]: Решение Problem K на Эрланг: дизайн
От: Cyberax Марс  
Дата: 07.11.07 14:38
Оценка:
Здравствуйте, Gaperton, Вы писали:

C>>Иногда, тем не менее, оно сильно мешает. Сейчас пишу на Эрланге модуль к ejabberd — в паре мест существенно помогла бы возможность иметь нормальные мутабельные переменные.

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

C>> Определенные типы рефакторингов (например, изменение форматов туплов) тоже не очень приятно делаются.

G>1) Делается это просто — достаточно найти все вхождения функции, возвращающей тупл, и сделать замену. Ничего особо неприятного — не сложнее, чем поменять местами параметры функции.
У меня тупл передавался через несколько функций — поэтому одно использование я случайно пропустил. В результате, меня среди ночи разбудили звонками клиенты — у них все не работало после перехода на зимнее время

G>2) У тебя туплы сложного формата между модулями ходят? Это нехорошо, батенька. Не надо так делать — ошибка дизайна. А если без этого никак — в таких случаях erlang style guide рекомендует применять record-ы и делать декларации в отдельных модулях. А в пределах одного модуля изменить туплы — это не рефакторинг. Это фигня.

Не сложные, но случайное добавление/удаление поля из тупла в три элемента — может многое сломать.

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

G>Это языки группы ML. F#, OCaml, и прочее.
Я знаю. Мне реально подходит только OCaml, но я сколько ни пробовал его — все никак он у меня не приживается. Марсианский он какой-то.

У меня все есть мысль сделать микро-CLI на LLVM и портировать туда Nemerle. Но пока жду, что кто-нибудь наконец-то доведет поддержку GC в LLVM до вменяемого уровня

C>>Процесс вместо объекта в каждом случае, ИМХО, слишком — даже для Эрланга. Объекты в ОО — это самодостаточные легковесные сущности, а процессы в Эрланге все же находятся под контролем планировщика и требуют глобальной регистрации.

G>Его никто и не применяет в каждом случае. Общая философия такая — процессы применяются на для декомпозиции на макроуровне, чистый ФП стиль — на микроуровне.
Тут получается интересный вопрос — а где деление проводить? Это не всегда так четко понятно — у меня лично в первое время на Эрланге с этим проблемы были. Потом привык и стал писать более-менее нормально.

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

Я это знаю. Но с другой стороны, эрланговские процессы — и не объекты. Интересно было бы еще подумать о системе, где процессы вели бы себя полностью как объекты — т.е. не имели глобальной регистрации. Хотя это Smalltalk опять получится.
Sapienti sat!
Re[8]: дизайн-паттерны, Эрланг
От: Курилка Россия http://kirya.narod.ru/
Дата: 07.11.07 14:48
Оценка:
Здравствуйте, Gaperton, Вы писали:

G>Здравствуйте, Курилка, Вы писали:


К>>Да была задача аля http://www.trapexit.org/Cascading_Behaviours только у меня не fsm, а server был, и ничего, вполне себе работало.


G>Ну вот, тут уже все написано.


G> Result = Mod:handle_event(Event, ExtStateName, ExtStateData),


G>И все дела.


Дак яж не спорю, что не написано, просто для меня это далеко не тривиально было когда я задумался о такой задаче. Тут тебе надо Состояние в Состояние заворачивать по сути, ну и протаскивать, что на мой взгляд для понимания будет посложней чем class A extends B
Re[4]: Объяснение устройства cell
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 15:00
Оценка: :)
Здравствуйте, red75, Вы писали:

R>Здравствуйте, Gaperton, Вы писали:


G>>Кстати, приглашаю поиграть в "пятнашки". Помоги мне, попробуй улучшить дизайн. Принципы дизайна, которые надо сохранить — очень простые: если ты посмотришь внимательно на код, то ты обратишь внимание, что модули не знают про структуры данных друг друга — между ними ходят либо абстрактные, либо очень простые типы данных. Предлагается это свойство сохранить, и попытаться уменьшить связность.


R>Хм. А ведь Cell — это, в общем-то, простое хранилище, однозначно определяемое по {Ref,Table}. Почему-бы не вынести cell:evaluate, cell:eval_and_get в Table?


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

Cell — не хранилище. Этот модуль на самом деле скрывает тип данных "содержимое ячейки".

Вот этот тип:
cell() = Value | { expression, expression() }
Value = integer() | string() | { error, atom() }


За пределами модуля этот тип не видно — видно только его конструкторы. Таким образом, sheet никак не завязан на формат данных cell. Вот внешний интерфейс cell — все, что о нем знает sheet, что его можно создать из текста:

create( string() ) -> cell().

А также, преобразовать обратно в текст.

to_string( cell() ) -> string().

Почему внутри кроме прочего находится еще и eval? Потому, что он также завязан на внутреннюю структуру типа cell().

%% eval_and_get( Ref, sheet() ) -> { Value, sheet() }.
%% Evaluate cell and get its value...
eval_and_get( Ref, Sheet ) ->
    case sheet:get_cell( Ref, Sheet ) of
        { expr, Expr } -> expression:evaluate( Ref, Expr, Sheet );
        Value -> { Value, Sheet }
    end.

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

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

[code]
expression() =  BinaryOp | Ref | integer()
BinaryOp = { expression(), operation(), expression() }
Ref = { integer(), integer() }


Однако, cell этого не знает, плевать cell-y, как устроен expression. Он знает, что expression можно создать
create( string() ) -> expression()
и вычислить
evaluate( Ref, expression(), sheet() ) -> { Value, sheet() }.

Вот, кстати, тут виден реальный прощот. Value и Ref проходят явно в межмодульном интерфейсе. Вот это — ай-ай-ай. cell и expression слишком сильно связаны. Буду думать, как править.
Re[9]: Решение Problem K на Эрланг: дизайн
От: Gaperton http://gaperton.livejournal.com
Дата: 07.11.07 15:45
Оценка:
Здравствуйте, Cyberax, Вы писали:

C>>> Определенные типы рефакторингов (например, изменение форматов туплов) тоже не очень приятно делаются.

G>>1) Делается это просто — достаточно найти все вхождения функции, возвращающей тупл, и сделать замену. Ничего особо неприятного — не сложнее, чем поменять местами параметры функции.
C>У меня тупл передавался через несколько функций — поэтому одно использование я случайно пропустил. В результате, меня среди ночи разбудили звонками клиенты — у них все не работало после перехода на зимнее время

Бывает. Я тоже накушался. Тестить надо, это же динамика. Хотя хиндли-милнер определенно гарантирует от таких ошибок.

Диалайзер кстати свежий, с аннотациями типов, не пробовал? Есть мнение, он должен радикально помочь.

G>>2) У тебя туплы сложного формата между модулями ходят? Это нехорошо, батенька. Не надо так делать — ошибка дизайна. А если без этого никак — в таких случаях erlang style guide рекомендует применять record-ы и делать декларации в отдельных модулях. А в пределах одного модуля изменить туплы — это не рефакторинг. Это фигня.

C>Не сложные, но случайное добавление/удаление поля из тупла в три элемента — может многое сломать.

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

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

G>>Это языки группы ML. F#, OCaml, и прочее.
C>Я знаю. Мне реально подходит только OCaml, но я сколько ни пробовал его — все никак он у меня не приживается. Марсианский он какой-то.
Ага, меня он тоже бесит.

C>У меня все есть мысль сделать микро-CLI на LLVM и портировать туда Nemerle. Но пока жду, что кто-нибудь наконец-то доведет поддержку GC в LLVM до вменяемого уровня


Не знаю, не знаю . Я бы так не делал ни в коем случае.

C>>>Процесс вместо объекта в каждом случае, ИМХО, слишком — даже для Эрланга. Объекты в ОО — это самодостаточные легковесные сущности, а процессы в Эрланге все же находятся под контролем планировщика и требуют глобальной регистрации.

G>>Его никто и не применяет в каждом случае. Общая философия такая — процессы применяются на для декомпозиции на макроуровне, чистый ФП стиль — на микроуровне.
C>Тут получается интересный вопрос — а где деление проводить? Это не всегда так четко понятно — у меня лично в первое время на Эрланге с этим проблемы были. Потом привык и стал писать более-менее нормально.

Пока не знаю. Интуитивно как-то получается. Та же проблема будет у тебя и в Nemerle, кстати.

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

C>Я это знаю. Но с другой стороны, эрланговские процессы — и не объекты. Интересно было бы еще подумать о системе, где процессы вели бы себя полностью как объекты — т.е. не имели глобальной регистрации. Хотя это Smalltalk опять получится.

В каком смысле регистрация глобальная и почему это тебя волнует?
Re[5]: Объяснение устройства cell
От: red75  
Дата: 07.11.07 16:18
Оценка: 26 (1)
Здравствуйте, Gaperton, Вы писали:

R>>Хм. А ведь Cell — это, в общем-то, простое хранилище, однозначно определяемое по {Ref,Table}. Почему-бы не вынести cell:evaluate, cell:eval_and_get в Table?


G>Склеить их в принципе можно. Однако, стоит объяснить, почему они разделены.


Нет, я не говорю о склеивании. Связь Cell->Table осуществляется только через table:get_cell, но зачем Cell'у знать как получать значения из таблицы? Это нужно только для expression.

G>Cell — не хранилище. Этот модуль на самом деле скрывает тип данных "содержимое ячейки".


Да, я это и имел в виду. АТД абстрагирующий хранение содержимого ячейки.

G>Почему внутри кроме прочего находится еще и eval? Потому, что он также завязан на внутреннюю структуру типа cell().


G>
G>%% eval_and_get( Ref, sheet() ) -> { Value, sheet() }.
G>%% Evaluate cell and get its value...
G>eval_and_get( Ref, Sheet ) ->
G>    case sheet:get_cell( Ref, Sheet ) of
G>        { expr, Expr } -> expression:evaluate( Ref, Expr, Sheet );
G>        Value -> { Value, Sheet }
G>    end.
G>


G>Только зная внутреннюю структуру типа cell, мы сможем понять, что внутри лежит формула. Перенеся этот код в модуль sheet, мы получим более сильную связность — потому что человек, разбирающийся в устройстве sheet, не сможет понять его, не разобравшись в деталях cell. А это именно то, чего мы хотим избежать.


Я имел в виду:
-module(expression).
evaluate( { ref, Ref }, Sheet ) -> table:evaluate( Ref, Sheet );
-module(table).
evaluate(Ref,Sheet)->cell:evaluate(Ref,get_cell(Ref,Sheet)).
-module(cell).
evaluate(Ref,Cell)->и т.д.


G>Однако, cell этого не знает, плевать cell-y, как устроен expression. Он знает, что expression можно создать

G>create( string() ) -> expression()
G>и вычислить
G>evaluate( Ref, expression(), sheet() ) -> { Value, sheet() }.

G>Вот, кстати, тут виден реальный прощот. Value и Ref проходят явно в межмодульном интерфейсе. Вот это — ай-ай-ай. cell и expression слишком сильно связаны. Буду думать, как править.


Избавиться от передачи Ref'ов по-моему нельзя. Может запромотить его в АТД? Что-то вроде:
-module(table).
%% make_ref::(integer(),integer())->ref()
make_ref(X,Y)->{X,Y}.
-module(expression).
create_arg( [ Row, Col ] ) when Row >= $a, Row =< $z, Col >= $0, Col =< $9 ->
    { ref, table:make_ref(Row - $a + 1, Col - $0) };


А на счёт Value бродят смутные мысли о разделении значения ячейки (формула/литерал/пусто) и вычисленного значения ячейки (число/строка/ошибка), но пока не оформились.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.