Erlang по-русски. Форум » Erlang

Рейтинг топика

Всего проглосовало: 2
Ваша оценка:

Как обойтись без наследования?

(16 posts)

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

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

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

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

    На ум приходит несколько решений, и каждое из них чем-нибудь да кажется уродливым:
    -в функции, запускающей процесс "потомка" (модуля, работающего со специфичными для конкретного случая данными), spawn-ить процесс "родителя" (модуля, работающего с общими данными), запоминать его PID и передавать ему все сообщения, которые "потомок" не смог распарсить сам;
    -для хранения состояния процесса определять структуру (в смысле запись, record) с общими данными, для полей и методов "потомка" выделить 2 списка или ETS и в "конструкторе" (т.е. функции, вызываемой при старте процесса) заполнять этот список кортежами вида {имя_поля, значение} и {имя_метода, метод}; при разборе сообщений каждый раз искать поля и методы в этой структуре;
    -состояние процесса (аргумент рекурсивной функции, содержащей конструкцию receive) сделать замыканием (closure), возвращающим функцию-диспетчер, возвращающую, в свою очередь, поля и методы "объекта"; в функциях-диспетчерах "потомков" предусмотреть обращение к функции-диспетчеру "предка".

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

    Подозреваю, что правильное решение вообще не предполагает рукосуйной имитации ООП --- но мой искалеченный C++ мозг его не находит. =)

    Отправлено 9 мес. назад #
  2. Disclaimer: у меня у самого опыта негусто.
    Насколько я понимаю, приблизательно для этого предначзнаены behaviours (хотя делают они меньше, чем ты хочешь).

    Это более или менее похоже на твой третий вариант, с точностью до двух деталей:
    1. В "родительском" модуле (собственно описании поведения) запоминаются не функции "потомка" (модуля, реализующего поведение), а просто его имя. Потом их можно звать через Mod:fun (Arg) (где Mod -- переменная). Язык-то, в общем, интерпретируемый и связывание позднее.
    2. Есть некоторая языковая поддержка, которая позволяет компилятору проверить, все ли нужные "методы" в "потомке" определены (см. функцию behaviour_info).

    В исходниках библиотек можно посмотреть, скажем, модули gen или gen_server, которые свои поведения реализуют.

    Наследовать одно поведение от другого (то есть строить более чем двухуровневую иерархию) можно, но довольно неудобно.

    Отправлено 9 мес. назад #
  3. Ага, спасибо. Примерно понял, буду смотреть.

    А как при этом принято хранить состояние? Мне кажется правильным делать структуру state, у которой будут поля, скажем, parent и child, хранящие "поля" родителя и потомка соответственно. Но слегка смущает, что при этом получается, что state будет описываться несколько раз --- для родителя (как имеющая только поле child) и каждого из потомков (для каждого со своим типом в поле parent); позволяет ли такое Эрланг и если да, то как?

    Отправлено 9 мес. назад #
  4. Я бы просто хранил поля родителя впрямую, а для детских имел бы в родительской структуре отдельное нетипизированное поле (вообще говоря, вся типизация ведь динамическая). В каких-то случаях только его можно передавать детским функциям.
    Но, в общем, у меня уже складывается впечатление, что весь этот OO-разговор -- попытка описать эрланговские идиомы на несвойственном для них языке. Или, наоборот, найти привычные конструкции там, где их нет.

    Отправлено 9 мес. назад #
  5. Ну да, так и есть. Собственно, в этом и заключался вопрос --- "есть ли в Эрланге типовое решение для проблем, которые в ООП решают с помощью наследования?", а не "как в Эрланге реализовать наследование?".

    Плохо получилось сформулировать, к сожалению.

    Отправлено 9 мес. назад #
  6. Есть, но разбиение здесь совершенно другое.
    В ООП разбиение идёт от сущьностей (объекты) и их иерархий, а в ФП разбиение идёт от действий, которые тебе нужно выполнить. :-)

    Отправлено 9 мес. назад #
  7. Как оно устроено "в ФП вообще", я примерно понимаю, благо, опыт написания всякой мелочи на Scheme имеется. Интересует именно Эрланг, который, во-первых, благодаря своим независимым процессам в каком-то смысле как раз ОО-язык, более того, "ОО-язык as it shall be" (я по работе много пишу на Qt и регулярно замечаю, как легко было бы многие подаваемые как killer feature возможности этой библиотеки реализовать на Эрланге), а во-вторых, в отличие от Scheme/Haskell/*ML, широко применяется на практике, благодаря чему уже должны были сложиться типовые решения типовых проблем. Шаблоны проектирования, ага.

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

    Предположим, что мы пишем программу имитационного моделирования. Модель описывает покупателей и продавцов. И продавцы, и покупатели умеют а)вносить деньги на свой расчётный счёт и тратить их, б)оценивать своё географическое положение, географическое положение партнёра и транзакционные издержки, зависящие от удалённости партнёра. Продавцы при этом умеют также производить единицы продукции; потребители умеют испытывать нужду в единицах продукции и покупать их у продавцов. В реальном коде на C++ был написан базовый класс "экономический агент", в котором были расписаны операции с банковским счётом и вычисление издержек; от него были унаследованы классы "продавец" и "покупатель", реализовывавшие свои специфические методы.

    Как ту же самую модель следовало бы писать на Эрланге?

    Отправлено 9 мес. назад #
  8. Тут, даже на С++, на мой взгляд, необязательно затевать иерархию.
    Вполне достаточно вынести обшюю функциональность в отдельный модуль.
    Что с лёгкостью ложиться на Erlang - модуль процессов для продавцов, модуль процессов для покупателей и модуль с функциональностью экономики. :-)

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

    Соответственно, с позиций чистого ФП здесь решать пака нечего. :-)

    P.S. Кстати, сам работаю на Qt + Python.
    Жасль, что для Erlang-а (да и для Haskell) до сих пор нет биндинга к Qt.

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

    "на С++ ... необязательно затевать иерархию" --- обязательно, если не хочется одноразового неподдерживаемого кода.

    Что тут "с лёгкостью ложится на Эрланг", мучительно не понимаю. Попробую ещё раз, м.б., мне не удалось как следует объяснить: "функциональность экономики" выходит за рамки обсуждаемой темы. Я рассказывал о части задачи по имитационному моделированию, в которой есть агенты, умеющие определённый набор вещей. Но при этом одна часть этих агентов имеет один набор дополнительных умений и данных о своём состоянии, а другая --- другой.

    Вопрос: как спроектировать систему на Эрланге для реализации этой модели? С помощью behaviours, как предлагает пользователь gogabr? А если, действительно, уровней в иерархии больше двух (экономические агенты делятся на потребителей и производителей, а производители на производителей пушек и производителей масла, например)? А как хранить характеристики агентов?

    Отправлено 9 мес. назад #
  10. удалите плиз это сообщение, случайно 2 раза запостил

    Отправлено 9 мес. назад #
  11. Везде же написано, что аналог объекта в эрланге - это процесс. А что такое процесс? Это функция. Данные храним в переменных состояния, запросы/ответы получаем через механизм сообщений. Приведу пример.


    start() ->
    A = new_proc(a, [[]]), %Создали "объект" А,
    B = new_proc(b, [[]]), %А теперь Б
    B ! {do1, 10, self()}. %вызвали "метод".

    %Базовый процесс (объект)
    a(State, {do1, Num, Pid}) ->
    Pid ! {retun, Num + 1},
    State.

    %производный процесс (объект)
    b(State, {do1, Num, Pid}) ->
    Pid ! {Return, Num * 2},
    State;
    b(State, Any) ->
    a(State, Any).

    loop(Fun, State) ->
    receive
    Data ->
    NewState = Fun(State, Data)
    loop(Fun, NewState).

    new_proc(Fun, Arg) ->
    spawn(?MODULE, loop, [Fun, Arg]).

    Но это одниночное наследование. А вот множественное:

    %одно свойство объекта
    a(State, {do1, Num, Pid}) ->
    Pid ! {retun, Num + 1},
    State.

    %второе свойство объекта
    b(State, {do2, Num, Pid}) ->
    Pid ! {Return, Num * 2},
    State.

    start() ->
    A = create_proc( [a/2], []),
    B = create_proc( [b/2], []),
    AB = create_proc( [b/2, a/2], []),
    AB ! {do1, 10, self()}. %вызвали "метод".

    runner(Fun, Args) ->
    case catch Fun(Args) of
    {'EXIT', _} ->
    Args;
    NewState ->
    throw {ok, NewState}
    end.

    poly_object(Components, State) -> %Components - список функций,
    %объединённых в объекте
    case catch lists:foldl( runner/2, State, Componets ) of
    {'ERROR', _} ->
    poly_object(Components, State);
    {ok, NewState} ->
    poly_object(Components, NewState);
    end.

    create_proc(Components, InitState) when is_list(Components) ->
    spawn(?MODULE, poly_object, [Components, InitState]).

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

    PS. Вообще мыслить объектами (т.е. их методами) ИМХО неправильно, у эрлага все данные слишком абстрактны и работать надо с ними на высоком уровне. В конце концов можно вызвать ЛЮБУЮ функцию для ЛЮБЫХ данных, в ООП языках так низя, там есть типы, поэтому там и "назначают" функции типам - обединяют их в объект. В эрланге "назначают" задачи процессам...

    Отправлено 9 мес. назад #
  12. О, то что надо, спасибо (единственное что --- я не очень понимаю, зачем в функции b сопоставление с образцом State, {do1, Num, Pid} --- ведь в функции a уже есть обработка этого случая, и b вызовет её благодаря
    b(State, Any) ->
    a(State, Any),
    правильно?

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

    Единственный оставшийся вопрос --- в реальной ситуации State ведь будет записью довольно сложной структуры, правильно? Кроме того, функции модулей-"потомков" могут ожидать обнаружить в ней разный набор дополнительных полей. Как правильно поступить в этом случае? Завести одно нетипизированное поле, как предлагает gogabr (а в нём --- ещё одно, на случай дополнительного "наследования", и т.д.)? Или есть ещё какие-то решения?

    "Вообще мыслить объектами (т.е. их методами) ИМХО неправильно" --- опять же, повторюсь: целью исходного поста и было выяснить, каков "Erlang way" в таких ситуациях, а не "как реализовать наследование на Эрланге". См, например, тему этого треда. =)

    Отправлено 9 мес. назад #
  13. Это я копипастом увлёкся. Вообще хотел так написать:

    b(State, {do2, Num, Pid}) ->
    Pid ! {Return, Num * 2},
    State;
    b(State, Any) ->
    a(State, Any).

    Теперь касательно State. Сам если честно опыта не имею, всего пол года эрланг знаю и ещё ничего стоящего не написал. Поэтому опять ИМХО.
    Безтиповость эрланга даёт полную абстракцию данных. Т.е. в переменной может быть любая структура, любой сложности, но ты видишь её всего лишь как 1 переменную. Это способ скрыть сложность. А скрыть сложность - цель лубого программирования на ЯВУ. В отличие от типизованых языков в эрланге есть возможность вызывать функцию сначала для одного типа, а потом ту же самую функцию вызвать для другого типа. И этим надо пользоваться.
    "State ведь будет записью довольно сложной структуры" - да, во время выполнения будет. Но тебя не должно это волновать. Ты скроешь эту сложность в полях State. Вернёмся к нашим функциям a и b. Пусть a нужны данные data1 и data2. А b нужны data3 и data4. Если b знает что кушает a (и что возвращает a) и сама пользуется этими данными, то ещё State будет выглядеть как {data1, data2, data3, data4}, а при вызове функции a она будет передавать ей только поля {data1, data2}:

    %Базовый процесс (объект)
    a({Data1, Data2} = State, {do1, Num, Pid}) ->
    Pid ! {retun, Num + 1},
    State.

    %производный процесс (объект)
    b({Data1, Data2, Data3, Data4} = State, {do1, Num, Pid}) ->
    Pid ! {Return, Num * 2},
    State;
    b({Data1, Data2, Data3, Data4}, Any) ->
    {NewData1, NewData2} = a({Data1, Data2}, Any),
    {NewData1, NewData2, Data3, Data4}.

    Если b не знает, что кушает a, то она будет передавать её просто переменную StateA, и State у функции b будет таким: {data3, data4, StateA}. Разве это похоже на сложные структуры?). Даже если функция a потом вызывает функцию c, то b об этом ничего не знает, и соответственно не видит сложность структуры StateA. Этот вариант предпочтительнее первого.

    %Базовый процесс (объект)
    a({Data1, Data2} = State, {do1, Num, Pid}) ->
    Pid ! {retun, Num + 1},
    State.

    %производный процесс (объект)
    b({Data3, Data4, StateA} = State, {do1, Num, Pid}) ->
    Pid ! {Return, Num * 2},
    State;
    b({Data1, Data2, StateA}, Any) ->
    NewStateA = a(StateA, Any),
    {Data3, Data4, NewStateA}.

    Отправлено 9 мес. назад #
  14. Второй вариант просто великолепен, спасибо! Это, собственно, пример того подхода, ради которого я и изучаю Эрланг (и Scheme): лаконичный и выразительный результат достигается не за счёт синтаксического сахара, а за счёт продуманности и гибкости базовых конструкций языка (первый вариант мне не подошёл бы, так как часто аналог StateA --- это крокодил на двадцать с лишним полей, и передавать их явно было бы, мягко выражаясь, утомительно =)).

    И, конечно, как всякое красивое решение, теперь оно кажется очевидным, да.

    Отправлено 9 мес. назад #
  15. только не забывайте, что таким же образом - то есть совершенно правильно - Госплан считал, сколько Советскому Человеку надо туалетной бумаги.(и прочего)
    В тему - лично меня Erlang имено тем и устроил, что нет нужды в запутанной иерархи классов, и проблему можно решать не надувая предварительно абстрактый базовый пузырь.

    Отправлено 9 мес. назад #
  16. Действуя по подсказкам уважаемого architect, сделал на пробу два "класса": один хранит значение x и имеет "методы" set_x и get_x, другой является "потомком" первого, а кроме того, имеет "поле" y и, соответственно, методы set_y и get_y. Однако у меня остаётся подозрение, что я в ряде мест изобретаю велосипед...

    Получилось вот что:

    common.erl


    -module(common).
    -export([rpc/2, loop/2]).

    rpc(Pid, Query)->
    Pid ! {self(), Query},
    receive
    {Pid, Answer}->
    Answer
    after 1000->
    {error, "Timeout"}
    end.

    loop(Fun, State)->
    receive
    {Pid, Method}->
    NewState = Fun(State, {Method, Pid}),
    loop(Fun, NewState)
    end.


    parent.hrl


    -record(parent, {x=0}).


    child.hrl


    -include_lib("parent.hrl").
    -record(child, {parent=#parent{}, y=0}).


    parent.erl


    -module(parent).
    -include_lib("parent.hrl").
    -export([init/0, do/2]).

    do(State, {{set_x, NewX}, Pid})->
    NewState = State#parent{x=NewX},
    Pid ! {self(), {ok, NewX}},
    NewState;
    do(State, {{get_x}, Pid})->
    Pid ! {self(), {ok, State#parent.x}},
    State;
    do(State, {Any, Pid})->
    Pid ! {self(), {error, ["Unknown method", Any]}},
    State.

    init()->
    spawn(fun()-> common:loop(fun parent:do/2, #parent{}) end).


    child.erl


    -module(child).
    -include_lib("child.hrl").
    -export([init/0, do/2]).

    do(State, {{set_y, NewY}, Pid})->
    NewState = State#child{y=NewY},
    Pid ! {self(), {ok, NewY}},
    NewState;
    do(State, {{get_y}, Pid})->
    Pid ! {self(), {ok, State#child.y}},
    State;
    do(State, {Any, Pid}) ->
    NewParent = parent:do(State#child.parent, {Any, Pid}),
    NewState = State#child{parent=NewParent},
    NewState.

    init()->
    spawn(fun()-> common:loop(fun child:do/2, #child{}) end).

    Поймите меня правильно: не то, чтобы я был таким уж фанатом ООП. Но, ИМХО, есть области (например, имитационное моделирование), где без ОО-решений действительно бывает тяжело.

    Отправлено 8 мес. назад #

RSS экспорт этой темы

Отправить сообщение

Вы должны войти в систему, чтобы оставлять сообщения.