You are viewing [info]gaperton's journal

April 2012   01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
инженер

Problem K на Эрланг: решение №2, последнее + выводы

Posted on 2007.11.20 at 17:02
Tags: , , , , ,
Исправленное решение - по результатам обсуждения на РСДН.



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

Мы сделаем это для элементарных типов, одновременно с применением еще одного дизайнерского хода - "слоеных" архитектрур. Типа как в семиуровневой модели OSI, и все такое. Суть слоев проста - верхний слой использует нижний, нижний ничего не знает про верхний. Также, этими средствами я исправляю косяк в моем предыдущем дизайне - а именно, такие важные штуки как value, ref, и error у меня не проходят явными типами, их структура - это неявный контракт между модулями cell, expression, и operation, что, вообще говоря, error-prone и, если кто не заметил - то это очень сильный косяк.

Для Хаскеля это не будет _таким_ сильным косяком, там такие типы явно определяются, а вот для динамического Эрланга - это реально сильный косяк. Вообще, планка good enough дизайна для Эрланга выше, чем для Хаскеля с его системой типов, строгой типизаций.

Итак, выделяем слой базовых типов в явном виде. Это будут reference, value, и operation.

Начнем с главного - value. Имеем конструктор из строки create, которая нам обязательно вернет валидный value. Ошибка - также вполне валидный value.

Обратите внимание - пустое значение также значение, и оно может трактоваться как 0 в операциях с целыми. Потому, у нас определены функции приведения к целому (as_integer) для нашего значения, которыми воспользуется operation. Результат придения обязан быть целым, или вылетит value:error в виде exception.

Кроме того, кто-то (а именно - expression) может захотеть создать число из строки, зная, что там должно лежать число (конструктор integer). Тут уж извините - если не вышло, то ничего не создастся и вылетит value:error.

Принципиально здесь следующее - вылет exception находится также в данном модуле, и вылетает гарантированно альтернатива варианта error. Если мы не хотим exception, а хотим чтобы вернулся честный variant, то нам достаточно перед вызовом нашей функции написать catch.

-module( value ).
-export( [ create/1, integer/1 ] ). %% create value from string
-export( [ as_string/1, as_integer/1 ]  ).
-export( [ throw_error/1 ] ). %% throw value:error

%% Public data type
%% value() = integer() | string() | error() | empty.
create( [ $', String ] ) -> String;
create( [ $#, String ] ) -> error( list_to_atom( String ) );
create( "" ) -> empty;
create( Int ) -> catch integer( Int ).

%% to_string( value() ) -> string().
%% get text representation for the given cell value
as_string( Int ) when is_integer( Int ) -> integer_to_list( Int );
as_string( { error, Msg } ) -> "#" ++ atom_to_list( Msg );
as_string( empty ) -> "";
as_string( String ) is_list( String ) -> String.

%% error( atom() ) -> error() = { error, Code }
error( Code ) when is_atom( Code ) -> { error, Code }.
throw_error( Msg ) when is_atom( Msg ) -> throw( error( Msg ) ).

%% integer( String ) -> integer() | throw error()
integer( String ) ->
	case string:to_integer( T ) of
		{ Int, [] } -> Int;
		{ error, no_integer } -> throw_error( 'not a number' ).
	end.

as_integer( empty ) -> 0;
as_integer( Int ) when is_integer( Int ) -> Int;
as_integer( _ ) -> throw_error( 'not a number' ).


ref у нас можно создавать из текста, а если не получается - то вылетает exception. Что логично - результат данной операции _только_ валидный тип ref, и ничто иное. Обратите внимание - как именно он вылетает. Вылетает гарантировано value:error, через функциональный интерфейс.

-module( ref ).
-export( [ create/1, create/2, row_range/3 ] ).

%% create( integer(), integer() ) -> ref().
create( Row, Col ) -> { Row, Col }.

%% create( string() ) -> ref() | throws value:error().
create( [ Row, Col ] ) when Row >= $a, Row =< $z, Col >= $0, Col =< $9 ->
	{ Row - $a + 1, Col - $0 };
create( _ ) -> value:throw_error( 'invalid ref' ).

%% row_range( Row, From, To ) -> [ ref() ]
%% Row = From = To = integer()
row_range( Row, From, To ) -> [ ref( Row, Col ) || Col <- lists:seq( From, To ) ].


Если вдуматься, то бинарные операции также могут являться элементарным типом данных. С операциями мы также правим один косяк. Теперь конструктор у нас возвращает тупо бунарную функцию, которая делает то, что нам нужно. Вот такую: fun( value(), value() ) -> value() | throws error(). Да, из нее могут лететь exception, но если мы напишем catch F( X, Y ) то тип этого выражения будет честный value(). Чем мы и воспользуемся в реализации expression.

Использование функций в качестве возворащаемых значений - чрезвычайно мощное средство инкапсуляции, которое радикально уменьшает связность. Чем это лучше предыдущего варианта? Например, я могу эту функцию передать в третий модуль параметром, и этот модуль может ничего не знать о модуле operation. Что в предыдущем дизайне невозможно. ФВП - совершенно потрясающая штука, я их ни на какие классы теперь не променяю.

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

%% create( char() ) -> Operation
%% Operation = fun( value(), value() ) -> value() | throws error()
%% create binary operator...
create( $+ ) -> fun add/2;
create( $- ) -> fun sub/2;
create( $* ) -> fun mult/2;
create( $/ ) -> fun divd/2.

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

%% Binary operations implementation... 
%%______________________________________
add( X, Y ) when is_list( X ), is_list( Y ) -> X ++ Y;
add( X, Y ) -> value:as_number( X ) + value:as_number( Y ).

mult( X, Y ) -> value:as_number( X ) * value:as_number( Y ).

sub( X, Y ) -> value:as_number( X ) - value:as_number( Y ).

divd( X, Y ) ->
	X_1 = value:as_number( X ),
	Y_1 = case value:as_number( Y ) of
		0 -> value:throw_error( 'division by zero' );
		Y -> Y
	end,
	X_1 / Y_1.


Вот так. Теперь перейдем, так сказать, к логике нашего решения. Как я уже говорил, Трурль с РСДН предложил замечательное решение проблемы связности модуля expression. Суть его состоит в том, чтобы конструктор expression возвращал список ссылок, которые надо просчитать. Мы дополним его решение применением ФВП, и получим волшебную штуку - интерфейс expression сократился до одной функции.

create_expr( string() ) -> { Expression, [ ref() ] } | value().
Expression = fun( [ value() ] ) -> value()

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

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

%% create_expr( string() ) -> { Expression, [ ref() ] } | value().
%% Expression = fun( [ value() ] ) -> value()
create( String ) -> catch
		{ Expr, Refs } = create_expr( String ),
		F = fun( Args ) -> catch
			{ Res, [] } = evaluate( Expr, Args ),
			Res
		end,
		{ F, Refs }

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

update_refs( { ref, Ref }, Refs ) -> [ Ref | Refs ];
update_refs( _, Refs ) -> Refs. 

%% create_arg( string() ) -> Ref | integer()
%% Ref = { integer(), integer() }
create_arg( Arg ) ->
	try value:integer( Arg ) catch
		{ error, _ } -> ref:create( Arg )
	end.

%% evaluate( Expression, Args ) -> { Value, [] }.
%% Evaluate expression.
evaluate( { A, Op, B }, Args ) ->
	{ ResA, Args_1 } = evaluate( A, Args ),
	{ ResB, Args_2 } = evaluate( B, Args_1 ),
	{ Op( ResA, ResB ), Args_2 };
evaluate( { ref, _ }, [ Arg | Rest ] ) -> { Arg, Rest };
evaluate( Val, Args ) -> { Val, Args }.


Теперь клетка таблицы. Ее можно создать из текста, также ее можно рассчитать, тогда она перестанет быть клеткой, и станет просто value(). Собственно, что такое клетка, если вдуматься? Это либо value(), либо формула. Что такое процесс вычисления? Замена в таблице cell() на value(). Вот и все.

Код выглядит так страшно потому, что мы вычисляем формулы рекурсивно, ловя циклы и модифицируя sheet, который протягивается сквозь алгоритм "монадическим образом" ((с) Зефиров). Да собственно, если вдуматься, то и не так страшно он выглядит.

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

%% create( String ) -> value() | Formula
%% Formula = { expression(), [ ref() ] }
%% value:create, expression:create, Cell
create( [ $= | Expr ] ) -> expression:create( Expr );
create( Value ) -> value:create( Value ).

%% evaluate( ref(), sheet() ) -> sheet().
%% eval_and_get
evaluate( Ref, Sheet ) ->
	{ _, Sheet_1 } = eval_and_get( Ref, Sheet ),
	Sheet_1.

%% eval_and_get( ref(), sheet() ) -> { value(), sheet() }.
%% table:get_cell, table:set_cell, value:error, Cell
eval_and_get( Ref, Sheet ) ->
	case table:get_cell( Ref, Sheet ) of
		updating -> { value:error( 'cyclic reference' ), Sheet };
		{ Expr, Refs } ->
			Sheet_1 = table:set_cell( Ref, updating, Sheet ),
			{ Args, Sheet_2 } = lists:mapfoldl( fun eval_and_get/2, Sheet_1, Refs ),
			Value = Expr( Args ),
			{ Value, table:set_cell( Ref, Value ) };
		Value -> { Value, Sheet }
	end.


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

-module( sheet ).
-export( [	new/0, 
			create_row/3, row_as_text/3, evaluate/1 ] ).

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

%% update_row( Row, [ string() ], sheet() ) -> sheet()
%% table:set_cell, ref:row_range, create_cell
create_row( Row, TextCells, Sheet ) ->
	Cells = [ cell:create( String ) || String <- TextCells ],
	Refs = ref:row_range( Row, 1, lists:length( Cells ) ),
	lists:foldl( fun table:set_cell/2, Sheet, lists:zip( Refs, Cells ) ).

%% get_row( Row, Length, sheet() ) -> [ string() ]
%% value:as_string, table:get_cell, ref:row_range
row_as_text( Row, Cols, Sheet ) ->
	[ value:as_string( table:get_cell( Ref, Sheet ) ) || Ref <- ref:row_range( Row, 1, Length ) ].
	
%% evaluate( sheet() ) -> sheet().
%% cell:evaluate, table:get_refs
evaluate( Sheet ) -> lists:foldl( fun cell:evaluate/2, Sheet, table:get_refs() ).

get_cell( Row, Col, Sheet ) -> 
	Value = table:get_cell( ref:create( Row, Col ), Sheet ),
	value:as_string( Value ).
	
set_cell( Row, Col, String, Sheet ) ->
	 table:set_cell( ref:create( Row, Col ), cell:create( String ), Sheet ),

-module( table ).
-export( [ new/0, set_cell/2, set_cell/3, get_cell/2, get_refs/1 ] ).

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

get_refs( Sheet ) -> gb_trees:keys( Sheet ).

%% set_cell( ref(), cell(), sheet() ) -> sheet().
set_cell( _, empty, Sheet ) -> Sheet; %%% do NOT store empty values...
set_cell( Ref, Value, Sheet ) -> gb_trees:enter( Ref, Value, Sheet ).

%% set_cell( { ref(), cell() }, sheet() ) -> sheet().
set_cell( { Ref, Value }, Sheet ) -> set_cell( Ref, Value, Sheet ).

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


Вот так, мне кажется, должен выглядеть идиоматический дизайн для данной задачи. Выводы:
1) ФВП - рулят.
2) Для описания дизайна чистых программ великолепно подходит метод CRC-Cards. Буду им пользоваться.
3) Дизайн в случае функциональной чистоты - удивительно простое и приятное занятие, не приходится морочится с состоянием, identity, и следующими из них схемами агрегации. Дизайн уже не является трудоемкой фазой разработки. Это меня изумляет.
4) Рефакторинг чистой программы - чистое удовольствие. Цена ошибки дизайна на порядок ниже в случае чистой программы.
5) Перечисленное, кстати, означает применимость подходов agile в случае функциональной чистоты. Я пока особых противопоказаний не вижу. Удивительное рядом, господа.
6) Организация разработки в случае смешанных чисто-грязных языков, таких как Эрланг или Немерле, вообще отдельная тема. Дело в том, что функциональная декомпозиция (в рамках моего определения) получается 100% отделена от декомпозиции по состоянию (то есть объектной модели). Это сложно понять, это надо объяснять отдельно и долго. Грубо говоря, можно спроектировать, написать, и отладить чистую часть независимо - первым этапом. А вторым этапом - натянуть на эту готовую систему объектную модель, в которые завернуть состояние. Это потрясающе классный подход - сначала проектируем ядро системы - наслаждаясь дешевым дизайном и рефакторингом, а потом нарезаем ее на объекты в разных позах (что элементарно - stateless код гораздо пригоднее к повторному использованию - в нем нет самой сильной связности - по состоянию) - радикально сокращая таким образом комбинаторную сложность дизайна (вместо X*Y получим X+Y, если вы понимаете о чем я).

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

Comments:


Курилыч
[info]kurilka at 2007-11-20 22:05 (UTC) (Link)
А роль первой C в CRC для функциональных языков будут играть модули, получается?
Кстати, хочется снова повторить свой вопрос про юнит-тесты и "глубокий" рефакторинг - как всёж ты считаешь их следует скрещивать?
(Anonymous) at 2007-11-21 13:37 (UTC) (Link)
Первая С - не только модули, но и АТД. У меня в Эрланге АТД языком не поддерживается, и я их заворачиваю в модули. В принципе - получается не хуже. Даже лучше - язык проще.

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

Второй момент - юнит юниту рознь. В большой системе юнит тест будет масштаба системного теста для маленькой программы.

Короче, не надо мельчить с единицами тестирования. Лучше нашпиговать код проверкой инвариантов..
Курилыч
[info]kurilka at 2007-11-21 13:44 (UTC) (Link)
Ну тогда получается, что твоё помимание юнит-тестов несколько отличается от понимания TDD и иже с ним. Там же тестируют вплоть до функций. А у тебя выходит нечто промежуточное между функциональными тестами и мелкими юнит-тестами.
Но в целом понятно, спасибо.
(Anonymous) at 2007-11-21 13:58 (UTC) (Link)
В моем понимании необходимость в юнит-тесте возникает при групповой и/или многоэтапной разработке, чтобы позволить исполнителям отлаживаться независимо. До сшивки фрагментов в большую систему.

Для микротирования есть инварианты в коде (что дешевле тестов - проверено), и, разумеется, Repl. Здесь моя позиция сходна с позицией Зефирова.

Эта методика замечательно работает и на С++ - разумеется, без Repl, но в плюсах у нас зато есть отладчик.
coderoid
[info]coderoid at 2007-11-21 01:09 (UTC) (Link)
>> Дизайн в случае функциональной чистоты - удивительно простое и приятное занятие, не приходится морочится с состоянием, identity, и следующими из них схемами агрегации. Дизайн уже не является трудоемкой фазой разработки. Это меня изумляет.

Хм... А может это просто из-за того, что дизайн был "с нуля"? И был достаточно небольшим, чтобы помещаться в голове целиком? Если нужно что-то такое-эдакое дописать к серъезной существующей программе, не верится, что также легко получится... Особенно с объектной частью...
(Anonymous) at 2007-11-21 09:10 (UTC) (Link)
Нет, дело не в этом. Я специально решаю те задачи, которые я в прошлом очень удачно решал для С++ и ОО. Чтобы понаблюдать за собой, и сравнить возникающие проблемы и усилия.

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

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

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

Это, короче, без примера понять сложно. Я потом напишу.
sinfonia_girl
[info]sinfonia_girl at 2007-11-21 21:31 (UTC) (Link)
Это, конечно, полнейший ОФФ... но gaperton вернулся??!! Я тебя не узнаю (по морде лица).
alexey_rom
[info]alexey_rom at 2007-11-22 14:25 (UTC) (Link)
Здравствуйте! Пишу методичку по Эрлангу, можно ли включить это решение как пример?
Gaperton
[info]gaperton at 2007-11-22 16:39 (UTC) (Link)
Это большя честь для меня, однако - в этом виде - не стоит включать его методичку, потому что я (шепотом) этот код после последнего рефакторинга не отлаживал :). Там ошибки есть почти наверняка. Вот если его отладить - то будет хорошо.
alexey_rom
[info]alexey_rom at 2007-12-03 19:26 (UTC) (Link)
Кстати, извините за вопрос -- Вы сейчас в CQG работаете или где-то в другом месте?
Gaperton
[info]gaperton at 2007-12-03 19:28 (UTC) (Link)
Уже два года как в другом месте. И вот думаю, надо бы еще разок работу поменять.
alexey_rom
[info]alexey_rom at 2007-12-03 19:52 (UTC) (Link)
Ясно, спасибо. Не скажете, где? А то студенты интересуются, где Эрланг применяется...
Gaperton
[info]gaperton at 2007-12-03 20:02 (UTC) (Link)
Список из презентации Джо Армстронга. Студентов можно обрадовать тем, что все российские сотовые операторы используют решения от synapse. Например, когда вы просите прислать вам настройки GPRS через сим-меню, у оператора отрабатывает код на Эрланге.

Ericsson AXD301 (part of “Engine”) (1.1 million lines of Erlang)
Ericsson GPRS system
Alteon (Nortel) SSL accelerator
Alteon (Nortel) SSL VPN
Teba Bank (credit card system – South Africa)
T-mobile SMS system (UK)
Kreditor (Sweden)
Synapse
Tail-f
jabber.org /uses ejabberd)
twitter (uses ejabberd)
alexey_rom
[info]alexey_rom at 2007-12-03 20:24 (UTC) (Link)
Это я им говорил, но ведь не только в сотовых операторах жизнь. Помню, где-то ещё упоминалось в комментариях у thesz... Вот: "Те работы по моделированию, что велись в рамках нашей Лаборатории, велись на erlang." Это, если я правильно понимаю сроки, и есть ваше нынешнее место работы?

Моделирование электронной аппаратуры -- как раз по нашей специальности (МИЭТ).
Serguey Zefirov
[info]thesz at 2008-03-13 22:28 (UTC) (Link)
Где это вы такое у меня нашли?
alexey_rom
[info]alexey_rom at 2008-03-13 22:50 (UTC) (Link)
Как я сказал -- в комментариях в Вашем журнале. Конкретно здесь: http://thesz.livejournal.com/666023.html?thread=4511911. Извиняюсь, если выразился не совсем ясно и создал почву для неправильного понимания.
Serguey Zefirov
[info]thesz at 2008-03-13 23:00 (UTC) (Link)
Этот комментарий - и который выше, - сплошное вранье. Человек потом сам в этом признался, в ЖЖ. Однако, пост со ссылкой на его признание пришлось скрыть, а вслед он скрыл свой пост с признанием.

Для моделирования аппаратуры на Эрланге все было создано, но работы толком запущены не были, по причинам, к Эрлангу не относящимся.

Я же продолжал делать модели на Хаскеле.
alexey_rom
[info]alexey_rom at 2008-03-13 23:16 (UTC) (Link)
Собственно, поскольку за Вашим журналом слежу и за журналом Гапертона я слежу -- его признание я видел, но это было уже позже.
Агафоклия
[info]vakitosubu at 2011-03-08 02:02 (UTC) (Link)

I love to read your articles

You made some decent factors there. I seemed on the web for the issue and found most individuals will go along with along with your website.
Previous Entry  Next Entry