Skip to content

Instantly share code, notes, and snippets.

@avesus
Last active August 29, 2015 14:19
Show Gist options
  • Save avesus/82aaed9dfc6ef0f1dcdf to your computer and use it in GitHub Desktop.
Save avesus/82aaed9dfc6ef0f1dcdf to your computer and use it in GitHub Desktop.
Reactive Actors
Актор потребляет ресурсы:
CPU
Память
Uplink/Downlink bandwidth
Ресурсы предоставляет актор верхнего уровня (владеющий контейнер)
двумя способами: а) из собственных ресурсов (отнимая/резервируя их из своего пула).
Связывающийся с другими акторами актор явным образом задаёт максимально возможную скорость,
с которой он будет передавать сообщения по разным исходящим каналам, которые у него есть.
б) актор может иметь связь с актором другого уровня, который предоставит ресурсы.
Это аналог динамической аллокации.
Для крупных содержащих акторов процесс их вычисления должен уметь параллелиться между несколькими компьютерами.
Мы это делаем репликацией state-машины актора через CRDT. Суть в том, что содержащиеся внутри актора субакторы
будут присутствовать на разных машинах, но все они будут содержаться внутри одного большого актора и взаимодействовать
с его внутренней частью.
Субакторы прикрепляются к родительским акторам системой каналов. Акторы могут соединять субакторы напрямую между собой,
не присоединяясь посередине.
Субактор может быть соединён всего одним каналом с другим субактором,
и не быть привязанным напрямую к содержащему актору-контейнеру. Но этот субактор по-сути может находиться только в одной полости содержащего актора. Поэтому введём обязательное требование для субактора всегда быть привязанным специальной системой каналов
к содержащему актору. Так как акторы можно передавать через каналы в виде сообщений, в них будет действовать другая привязка.
Вопрос: как реализовать механизм "выделения" актора? Необходимо, чтобы субактор передал содержащему его актору
другой актор, который он создал.
A {
internal (processing) space
-------------------------|A channel|----------
container space
-------------------------|A channel|--\
B { |
create C |
--------|subchannel|--|A channel|--|
send C by channel |
----------------------------------/
}
}
В момент создания, C прикреплён каналом subchannel к B: B владеет C. При пересылке C, происходит серия сложных действий:
B отправляет C по каналу A channel. A принимает новую сущность С к себе в processing space. Но, т.к. акторы должны находиться
только в container space, вместо самого актора C B должен по каналу передать своё внутреннее processing-space ПРИКРЕПЛЕНИЕ
к актору C, т.е., внутреннюю часть канала subchannel. Таким образом, на какое-то время, C будет находиться внутри B,
а A будет иметь handle канала subchannel, который "физически" будет проксироваться через канал A channel.
Как передать объект C в полость объекта A?
С акторами, проблемы реализации семантики отправки нет: есть внешние каналы, есть внутренние каналы,
актор-сообщение просто помещается внутрь одного из таких каналов. Там есть только нюансы обработки неподключенных каналов. Трактовать ли внутреннюю и внешнюю полости как отдельные сущности? Иметь внутреннюю полость необязательно, если актор не содержит
других акторов.
Гораздо любопытнее реализовывать семантику обработки входящих сообщений. Любое сообщение будет активировать процессинг?
Что с параллельными сообщениями по разным каналам? Синхронизация? Очереди? Как будет выглядеть код реакции на входящие сообщения?
Наличие одного входящего сообщения может активировать процессинг.
Сообщения могут складываться и ассоциироваться, меняя состояние актора таким образом, что определённые сообщения будут приводить
к определённым изменениям состояния и, как результат - к активации внутренних комплексных обработчиков. Внутренние комплексные
обработчики содержат в себе условия их активации. Для простых сообщений могут быть простые обработчики, которые меняют внутреннее
состояние. Активаторы внутренних комплексных обработчиков могут содержать в себе условия, зависящие как от состава новых пришедших сообщений, так и от внутреннего состояния. Вызов обработчика реализует концепцию предоставления CPU.
Условие активации любого обработчика всегда внешнее (относительно блока обработки) событие.
Если что-то должно произойти в определённый момент времени, этот момент должен быть внешним событием.
Так, при приходе определённого сообщения может активироваться проверка сложного условия, просто анализирующая (сравнивающая)
текущее внутреннее СОСТОЯНИЕ актора с заданным. Если состояние совпадает, активируется сложный обработчик.
Состояние - наличие полей, имеющих определённые свойства.
Передача сообщения во внешний канал: заявления о наличии потребностей, заявления о наличии возможностей. Заявления о наличии возможностей - автоматическое накапливаемое качество, аггрегируемое на базе статистики фактов удовлетворения потребностей.
Прямая обратная связь удовлетворения необязательна. Равно как и точная идентификация актора, осуществившего удовлетворение.
Акторы - поставщики значения. Наделяют смыслом ссылки на них из других акторов. Ссылка на другой актор является свойством.
Значение, которое извлекает актор, обращаясь к своему полю-ссылке на другой актор, является потоком удовлетворения потребности
в значении. Многозначные акторы могут представлять разные значения с разных сторон-ссылок на них.
Суть актора - совокупность всех его свойств (структура ссылок).
Состояние актора - совокупность текущих значений (потоков удовлетворения потребностей в значениях) всех свойств.
При получении сообщений (при изменении состояния определённых свойств самого актора, т.е., при изменении значений свойств актора),
актор осуществляет обработку: он может изменить своё состояние на новое, путём изменения значений своих свойств. Значение появляется в интерфейсе ссылки на актор - при ПОСТАВКЕ другому актору такого значения. Сам актор значением не является.
Отсюда вывод: чтобы повлиять на другие акторы (изменить их состояние), необходимо иметь значение, т.е. внешнюю интерфейсную часть ссылки, то есть БЫТЬ чьим-то СВОЙСТВОМ. Передавать сообщения (значения) можно в каналы ссылок на данный актор в других акторах.
Значение может быть, а может быть promise. При передаче нового значения или исходного, получающий актор активируется - ему сигнализируется об изменении значения его свойства.
Любой атрибут объекта (часть состояния) всегда является ссылкой на конкретный другой объект, удовлетворивший потребность объекта в наличии такого атрибута. Значением являются не некие цифры, а причина появления значения - последовательность действий,
приведшая к тому, что потребность получившего значение объекта стала удовлетворена. Пустая ссылка - это абстрактная потребность.
Состояние объекта само по себе является внешним значением, как удовлетворяющим чьи-то потребности, так и заявляющим о собственных.
Some google mentions
---------------------
Some things stand out in the approach so far.
The actors in the test are stateless. They do computation, but do not maintain internal state that affects follow-on communication.
Addressing the above will reveal that "require" approach is not sufficient. When you "require" a module, the module itself is a singleton, meaning that if you "require" it again, you will get the same exact one. Illustrative example:
assume foo.js contains:
var k = 0;
module.exports = {foo: k};
then in console execute:
var f = require('./foo.js');
console.log(f.foo); // prints "0"
f.foo = 7;
console.log(f.foo); // prints "7"
var g = require('./foo.js');
console.log(g.foo); // prints "7" ?!
Lastly, the identity and the "hard" part of communicating between actors via actor references is left out so far. The actor communicates to only one hard coded "next" actor sort-of by convention.
Some fundamental things to consider about actors:
Actors have internal state.
When an actor handles a message, it can *send* a finite number of messages to other actors.
When an actor handles a message, it can *create* a finite number of new actors.
When an actor handles a message, it can change it's behavior for how it will handle any follow-on messages (*become*).
I am not claiming that you cannot build useful systems without the above, but the above are a requirement for a system to be an actor system and to gain the benefits of it. To get the object capability benefits two other considerations should be taken into account:
Actor addresses are unique and unforgeable.
Actor proxies are transparent. (this basically means that if I put a man-in-the-middle actor that all it does is forward messages to some other actors and responses back to wherever, nobody is able to tell that I'm doing that)
I hope this is helpful.
Cheers

Structuring

Reactive Actors (RA) should be structured like ordinary classes. But unlike of classes, it should be distinct internal inputs/outputs, external inputs/outputs and internal 'shell' state. In case of simple RA, it should be external i/o and internal state. For simplest RA, internal state doesn't exist, for ex. actor Sum could always recalculate its outputs and don't store state inside - calculation result is the last one, if its inputs would be disconnected or emptied, it will empty its output.

Inputs

Reactive Actors have their inputs in their public input properties. Their conception is like 'slots' in Qt.

Outputs

Reactive Actors have their outputs in their public output properties. Their conception is like 'Observable' in pub/sub systems.

I/O states

Inputs and outputs always have value semantics. Output always exist as a value container. It could be empty. Inputs are distinct from outputs by a way in which they could be connected or not. Actor never should 'mind' about its outputs connecting state - it should not be exist.

So, inputs can be connected or not. Ever if connected, they could have empty values - because their connected outputs of another actors are exhibit empty values on their outputs.

Processing

Reactive Actors processing happened when an input is first connected to an output of another actor or a connected output value has changed.

RA recalculate function activated each time when its input changed. There is no internal 'processing' possible by something time-clock generators. Ofcourse, RA recalc functions could be written so they recalculate its outputs in long calculation process and provide time-like semantics, but it should be avoided writing such while(1)-like code.

Caching/Queueing

A Reactive Actors engine should not store property values in actors inputs and shouldn't queue it too. If actors should cache previous state, they should do this explicitly in their recalculate function.

Outputs should be presented as inherent properties of the actor - when an actor changes a property, it should be replaced by new value and change notification should be distributed to all subscribers connected externally.

Connecting outputs and inputs. Owning and Containing

Outputs should be connected with inputs by information-controlled process in the containing actors. It is exist internal exhibited properties which are allowed to internal actors to observe. Connection control process should be described in declarative terms instead of functions/methods calls.

It should exist a standard mechanism (observable properties) which directs internal actors' connections. So, containing actor should be capable to exhibit such controlling values to its internal environment - which is actor itself with a role to connect another actors.

So, any container actor should contain actor 'internal-environment'. Without thinking about creation semantics let's suppose that two actors already exist inside actor-container. To exist inside, the actors should be filled in 'internal environment', it should know about their outputs and inputs. So, container actor can query int. env. actor which actors it wants to connect and specify which outputs and which inputs.

To be implemented in available code, for ex. in Javascript, container semantics should be explicit.

Classes are exist in ideal environment, without time and space. They are ideal constructs describing how to create its instances.

Each programmer should write classes and structure his/her program in OOP manner. We don't write objects directly contained inside another objects. We write distinct classes, A and B, then say to class A: 'you should have your internal property of type B which would be your inherent part'. Our code describing such containee object is not in A. It is in B.

But controlling code is in A. It sends state changes to B, it receives state changes from B.

Actor-like objects should be implemented as pure functions with inputs and outputs. Their purity is not as in FP. The actors are pure in sence of time: its outputs solely depend on its previous inputs.

For each current moment in time, actors calculate their output values and present them as observable-like properties visible from outside. The properties are connected with inputs of another actors by connectors, which translate current values from outputs to inputs. After receiving a new value on any input, actors should re-calculate their state (external observable) and present it in reactive manner.

Any function/method call semantics should be replaced by a property exhibition - external or internal (of actors-containers).

Connecting channels

Values which circulate on connecting channels are stored on outputs of actors. Each time when actors should have its own copy of external properties, they should explicitely make their own copies. So, channels 'transmit' just references to values.

Word 'transmit' is quoted because of there is no transmission in ordinary sense. Channels transmit notifications about property changes - not ever pointers to their new values. So, recalculating function could be used for 'lazy'-like evaluation of its outputs.

When input value is really needed, it could be acquired by delta-CRDT transmission process. Deltas are always calculated when an actor changes its outputs (actually, values are stored in delta-CRDT logs, so, we can get any historical value at any time when need it, although actor could compress the value to its latest state).

So, any change of observable actor state (its outputs) should lead to point notification that interesting part of the state is changed. Subscribing actor (subscriber) should immediately observe its inputs change events and run recalculation. It is not clear if actors ever should have its non-observable internal state persisted between its reactive recalculations.

Connecting IS containg and owning

By definition and design, containers actors are implementors of all internal connections of all their internal actors. By design, all contained actors are part of the structure of containers. Live cell-like actors which have their shell are another actors which are part of the container actors. There are internal inputs and outputs and external i/o. So, difference between them is in container actor's explicit public interface. Cell-like container actors are not necessary!

Container actors just could describe their internal actors connections explicitly in their constructor's definition and specify which inputs and outputs of its internal actors are external and which are part of internal state interactions.

A: to describe all connections between internal actors; B: to describe which inputs and outputs to be external.

Writing reactive functions

The most complex thinkable part of the thing is a question, how to write reactive code. The function can be composited by small pure functions, which gets their inputs and calculate their outputs. The most simple implementation can be activated each time when ever a single input changes. Such simple reactive function uses all values of it inputs like all of them are supplied only just now.

To make reactive calculations possible, we should introduce empty and not connected semantics for all used variables. Calculation process in any expression should support each of three possible states. Most nearest concept is monad Maybe in Haskell and other functional languages. We should write our code in such style which uses this Maybe semantics.

A short and efficient intro into Maybe monad: http://sean.voisen.org/blog/2013/10/intro-monads-maybe/

Most usable pair to monad Maybe`` for Reactive Functional Actors: monad Either```: http://folktalejs.org/data.either/

A short description from a comment on http://kawagner.blogspot.ru/2007/02/why-monads-are-evil.html :

Monads merely provide a structuring mechanism that also allows you to denote imperative constructs. One way of looking at them is in terms of overloading the semicolon that separates instructions in a sequence.

Take that last word into account: sequence. Monads only allow a linear sequence of monadic values to be bound together into one monadic value. Therefore monads most definitely do not allow you to generate any semantics you want.

Further on you seem to want to reclaim the functionality in the functional language, using the world as input to your program. However, any output logically has to become part of the world, therefore the program should be of type world -> world. What is more, the world cannot logically be duplicated by the program. And there exactly is the issue: with referential transparency there is no problem in duplicating any value, as the result of a program does not get changed by it. Therefore, functional programs are normally free to do so. With the world, they should not. And exactly that is what monads allow you to express: you can not diverge from the sequence of functions operating on the world. Therefore you cannot duplicate the world.

Relations

There could be relation of many to many. Each actor represents a value. The value is current state of the object. Subscriptions are assigned by external connections.

When an object changes its state it fires an event via all connections.

Event slots are simple methods like setValue(obj), where obj - is just instance reference of changed object. In fact, its value could be ever not calculated at all (or not required by the setXxx method. The method's implementation access obj itself to get its value. We should distinct changed events and outputs (re)calculations.

By design, setXxxx() methods only access values they know. In ideal, we should pass a binded to obj getValue() function instead of direct obj - each time when an object changes.

It should be possible to connect multiples observables to an input.

A most complex thing in such solution is some kind of discipline in process of observable state changes.

It is often needed to know not only new object's state, but reason (concrete events) which leads to the state change. Therefore, to not create a lot of distinct concepts, we could represent our state together with its changes sequence.

Subscription should be full (and not partial) - to the whole object state changes (and the state itself).

Also, it could be very convenient and smart to specify state changes by some means of a command/events language, because dumb diffs could be calculated by consumers if need, but important reasons of change are not - and simply could be exhibited be those state change events which moves to calls of setInputNamedLikeBlaBlaBla(getter): the reason could be simple change event which is emitted by changed object.

The main important difference from systems like this is that all of the events comes as an input (in all sorts of events/signals subscriptions I know there is 1:1 mapping). But in our system, an object could generate a lot of distinct events all of which should come to a single downstream subscription channel without opt-in. And the object itself is accessible (digestible) by getter callback function which is supplied into the slot together with the change event. It is subscriber's busy deal to decide use or not the event's semantics, call or not the supplied getter function, call or not setImmediate() or nextTick() to make its own state changes directly at the call of its setXxx() method or defer its state change and generating "I'm changed!" events to some later stage - perhaps, at async time ever.

So, value (current state) and events (last state transition from previous state to the current one) should be distincted by a developer. Semantics of value should be differentiated for each use case - sometimes it should be needed to store complex state history as a state itself, sometimes it should be buffers at next layers of processing.

Loops are welcomed! Complex behavior is achievable by complex connections of inputs and outputs.

Event emitters, async event sources all are welcomed - an object could change its state at each moment in time it wants. At the moment, our notification framework should dispatch the event to all of connected setXxx() methods by means of direct calls in a loop of all subscribers. Therefore, each actor must be implemented in a manner that each its method could be called asynchronously at every surprising moment. So, if it makes a change on its internal state, it should queue such change explicitly: internal events are very important part of our implementation. But simple actors could change their state directly by such call and emit their changed events cascadely. I repeat, setImmediate() and nextTick() call stack isolation is optional!

State change event should be fired afer the state change and it should describe at application level the state change (or the reason of it's change).

I repeat, regardless of events types count emitted by an object, it should be only one slot (subscriber, listener) for all of them - each suscribee should be called at any state change of an object. In such way we could implement very pure functional/reactive mapping to actors/signals model and preserve lot of SoC and purity, so, maintenance and readability.

But single slot could be (surprisingly!) connected to multiple signals simultaneously in our way - the feature unthinkable for such sort of systems and connections between reactive functor-like/actor-like objects.

Multiple inputs could be implemented by common CRDT source which was an initializer for multiple objects - each of them could change independently later, but both versions could be simply merged at setXxx() side - in such way that multiple sources update single CRDT value. Therefore, multiple inbound connections should be supported only for CRDT types which were initialized form the same source. By such means, we observe a strange duality between inputs and outputs as resulting states.

So, ever out setXxx() methods could be not setters but transitioners which describe some transition form an old (maybe unknown and unobservable) state to a new one. So, semantics of such transitioners should be defined correspondingly.

Therefore, a reactive actor function is some function which operated over its inputs changes and not on input values itself.

We found that most simple change description is last state state itself and short last event describing the change - together.

In principle, all inputs could store its previous state values to easily observe detailed state change in their own semantics. But it should be work of our framework to dispatch to those transitionSomeInputStateChangeFromTo methods correct pre-calculated references which depend on subscriber's subscription state. So, 'introduce' or 'it is exist a value' events should be generated automatically by change distribution function when a subscriber just connects to an observable value.

So, to framework to work, actors should implement some special methods for framework: _getValue() should return object's latest state - and it will be called each time when an actor emits a state change event.

Never more complex connectors which connect signals and slots! Just connect an actor's state to a transitioning input method of another actor (and specify that actor too). Just at time of the connection it will be first transitioning input method's call. Instead of 4 arguments for connecting your signals and slots it would be 3.

Useful Monads

https://github.com/folktale/data.either https://github.com/folktale/data.maybe

https://github.com/fantasyland/fantasy-land

@avesus
Copy link
Author

avesus commented Apr 14, 2015

Work in progress. Comment here on English, please.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment