📜 ⬆️ ⬇️

Atom - TypeScript implementation

Hello, my name is Dmitry Karlovsky and I ... a professional cyclist. During my life I tried many iron horses, but ultimately I stopped at a self-made one. Not that I really liked to work with a file, spending a lot of free time to reinvent the wheel, but the end result, where every bump is not given pain in the lower half of the body, is worth it. And now, when you know that I started all this for a reason, and to make the world a better place, let me introduce to you the TypeScript / JavaScript module $ jin.atom.

Summary of the previous series : the simplest application has reached a critical level of complexity, and to cope with it, the atom abstraction was introduced, which absorbed the entire routine, allowing the developer to concentrate on the description of invariants in a functional style, without losing connection with the object-oriented platform . All theory and pictures are there. There will also be a lot of practice, code samples and console dumps.

Why precisely TypeScript?


The first implementation of the module was in pure JavaScript, but recently it was rewritten in TypeScript. TypeScript is practically the same JavaScript, but with classes, type deduction and normal lambdas. More, it practically does not additionally change anything and, as a result, it integrates very well with regular JavaScript code. You can directly access TypeScript modules from JavaScript and vice versa. Unless, for JS, it is advisable to still write the so-called "declarations of the environment" in order not to lose the advantages that static typing gives. And she gives the following bonuses:
* Tips in the IDE save the programmer from having to keep in memory the documentation on all methods and properties of all classes.
* Search all places of use of the entity - indispensable when refactoring.
* Identification of inconsistencies in types between different parts of the application at the stage of editing / assembly.
Unfortunately, there are also disadvantages:
* Sometimes you have to dance with a tambourine, explaining to the compiler what you mean.

There are two alternatives to TypeScript:
')
JSDoc is an extremely non-expressive format for static description of dynamic code in comments. Often the volume of JSDoc-comments (without taking into account the verbal description) is obtained more than the actual useful code. Illustrative example:

/** * @callback onTitleChange_handler * @param {string} next * @param {string} prev */ /** * @param {onTitleChange_handler} handler */ function onTitleChange( handler ){ // ... } onTitleChange( /** * @type {onTitleChange_handler} */ function( next, prev ){ // ... } ) 

Dart - suck another language, which, however, is designed for translation in JavaScript. It uses completely different idioms, which has many limitations when integrating with JavaScript code, significantly more memory consumption, problems when debugging using the tools built into browsers, and the code generated from Dart is quite bulky noodles. The above example, looks like this:

  typedef void onTitleChange_handler( String next , String prev ); onTitleChange( onTitleChange_handler handler ){ // ... } void main() { onTitleChange( ( next, prev ) => { // ... }); } 

Already better, but also requires the introduction of too many named interfaces / types. This is the main disadvantage of nominative typing. In TypeScript , structural is used:

  function onTitleChange( handler : ( next : string , prev : string ) => void ){ // ... } onTitleChange( ( next, prev ) => { // ... }); 

But it is possible to give names to interfaces if necessary:

  interface onTitleChange_handler { ( next : string , prev : string ) : void } function onTitleChange( handler : onTitleChange_handler ){ // ... } onTitleChange( ( next, prev ) => { // ... }); 

Total, at transition to TypeScript:
+ decreases the amount of code
+ improved integration with the development environment
+ Additional validation checks appear as you type.
- the need to translate to javascript before execution is added

FRP Myths and Legends


Reactive libraries can be divided into two main types:
1. Actually FunctionalRP, where the whole application is described as a set of pure functions.
2. ProceduralRP, which is often confused with FRP. They describe the application imperatively in the form of streams of events (streams).

In the second case, the application is described as a set of procedures of the form: take data from different places, apply certain transformations to them successively, and then write them to other places.

  this.message = Bacon.combine( [ this.mouseTarget, this.mouseCoords ] , function( target, coords ) { return target + ' ' + coords } ) .map( trimSpaces ) .map( htmlEncode ) .map( htmlParse ) .onValue( function( messaage ){ document.getElementById( 'log' ).appendChild( message ) } ) 

Compare with the same, written in a less veiled form:

  this.onChange( [ 'mouseCoords', 'mouseTarget' ] , function( ){ var message = this.mouseTarget + ' ' + this.mouseCoords message = trimSpaces( message ) message = htmlEncode( message ) message = htmlParse( message ) this.message = message document.getElementById( 'log' ).appendChild( message ) this.fireChange( 'message' ) } 

Known PRP libraries ( Rx , Bacon ) in accordance with the PRP architecture have a rather complex API. The difficulty lies both in the huge number of methods that implement all kinds of operators on streams, and in how the simplest operations are described. For example, the correct conditional branching would look like this:

  var message = config.flatMapLatest( function( config ) { if( config ) { return mouseCoords.map( function( coords ) { return 'Mouse coords is ' + coords } } else { return mouseTarget.map( function( target ) { return 'Mouse target is ' + target } } } ) 

And here is the wrong:

  var message = Bacon.combineWith( function( config, coords, target ) { if( config ) { return 'Mouse coords is ' + coords } else { return 'Mouse target is ' + target } }, config, mouseCoords, mouseTarget ) 

The second option is much simpler and more intuitive, but in it the calculation of the message value will occur with any changes in all three streams, although it is obvious that at any time this value depends on only two of the three streams. In the first version there is no such problem, but this has been achieved by a considerable complication of logic.

Looking ahead, I will show for comparison the correct code on atoms:

  var message = $jin.atom.prop( { pull : function( ) { if( config.get() ) { return 'Mouse coords is ' + coords.get() } else { return 'Mouse target is ' + target.get() } } } ) 

In simple terms, in PRP it is convenient to describe dependencies, where data sources are relatively few and their composition practically does not change, and in FRP, on the contrary, the set of sources can be arbitrary and dynamic without loss of expressiveness. With data consumers, the opposite is true: in PRP, the same state can change with many different streams, and in FRP, exactly one function is responsible for one state, by which it is always clear how the value is formed and on which it directly depends.

Another popular misconception is that reactivity is only needed at the intersection of a model and a view. However, in fact, reactivity is a more fundamental concept. It is necessary to maintain invariants between states. Any cache is a state. Any persistent storage is a state. Any visualization is a state. States are everywhere and they are not independent even within the same application layer.

Properties


Before undertaking the realization of atoms, it is worthwhile to distinguish between two concepts: value (RValue) and container (LValue).

The most famous container is the variable. The variable supports only three interfaces:

  var count //       count = 2 //  return count //   

Another equally well-known container is the field of the object. It supports all variable interfaces:

  obj.count = 2 //   (   )      return obj.count //    

But in addition to them, the field supports a couple more:

  delete obj.field //   'field' in obj //     

As you can see, there are not so many interfaces and they look completely different. To implement more complex containers, which are undoubtedly atoms, we need much more interfaces, so we implement such a container as a class. Here is how the implementation of a variable might look:

  var count = new $jin.prop.vary({}) //   count.set( 2 ) //   count.get() //   

image

On the one hand, we changed the awl to soap: the container (count variable) stores another container (instance of the $ jin.prop.vary class) that stores the actual value. On the other hand, the container object, in contrast to a normal variable, is already the essence of the “first class”, that is, it can be passed as an argument to a function or returned from it as a result and so on. This is sometimes useful, but in the overwhelming majority of cases - unnecessarily. Much more useful if the interface implementations differ from the standard ones:

  var title = new $jin.prop.proxy({ put : function( next ) { document.title = next }, pull : function( ) { return document.title }, }) title.set( 'Hello!' ) //   title.get() //   

image

$ jin.prop.proxy - implementation of a stateless container, which can be either a “normal variable” or a “property of an object”:

  var doc = { get title( ) { return new $jin.prop.proxy({ put : function( next ) { document.title = next }, pull : function( ) { return document.title }, }) } } doc.title.set( 'Hello' ) //   doc.title.get() //   

image

In this case, the get interface calls the pull handler, and the set call put. Such a replacement was not made casual - in general, these are really completely different interfaces. To understand the difference, just enter the state and add obvious conditions:
1) get calls pull only if the value is not set yet, otherwise it just returns it - the so-called “lazy initialization”
2) set causes put only if the set value is different from the current one - this prevents execution of the put idly.

For example, if we work with the name of the document only through our container, then we can define it so that it doesn’t need to access the slow browser api again:

  var doc = { get title( ) { return new $jin.prop.vary({ owner : this, name : '_title', put : function( next ) { document.title = next }, pull : function( ) { return document.title }, }) } } doc.title.set( 'Hello' ) //      doc.title.get() //   doc.title.update() //    

image

If in the last two examples you were confused by such a cumbersome definition of a property, then let me tell you why it is so. In this case, it could be defined and easier:

  var doc = { title : new $jin.prop.vary({ put : function( next ) { document.title = next }, pull : function( ) { return document.title }, }) } doc.title.set( 'Hello' ) 

image

So it should be done for properties that do not need the possibility of inheritance. But if you declare a property in a prototype class like this, then all instances will work with the same container, which is usually not what you want. But it is necessary that each instance has its own containers. To do this, we create a container through the getter and give it a link to the object and the name of the field in it - it is in it that the container will save its data (or save itself - depends on the implementation). Another vivid example of using such a getter is a lazy registry, with an arbitrary number of keys:

  var info = { item : function( key ) { return new $jin.prop.vary({ owner : this, name : '_item:' + key, pull : function( ) { return 0 }, }) } } info.item( 'foo' ).get() // 0 info.item( 'bar' ).set( 123 ) info.item( 'bar' ).get() // 123 

image

And, finally, a common situation is the delegation of another property:

 var user = { get name ( ) { return new $jin.prop.vary({ owner : this , name : '_name' , pull : function( prev ) { return 'Anonymous' } }) } } var app = { get userName ( ) { return user.name } } app.userName.get() // Anonymous app.userName.set( 'Alice' ) // Anonymous app.userName.get() // Alice 

image

Reactive properties


So now we are ready to create our first atom:

  var message = new $jin.atom.prop( { notify : function( next, prev ) { document.body.innerText = next }, fail : function( error ) { document.body.innerText += ' ' + error.message }, } ) message.push( 'Hello' ) //   message.fail( new Error( 'Exception' ) ) //    

image

Everything is simple - when we change the value of an atom, the function notify (or fail) is immediately called, in which we can imperatively reflect the change of state at the oop runtime. Normally, the FRP code of the application practically does not need such manual synchronization - most of them are easily eliminated by a declarative description of the version, according to which such synchronizing atoms are automatically generated. But this is a topic for a separate large article, so that later we will focus on the capabilities of the atoms themselves.

Atom is a generalization of the "promise", so it is not surprising that it supports and thenable interface :

  var message = new $jin.atom.prop({}) message.then( function( next ) { document.body.innerText = next }, function( error ) { document.body.innerText += ' ' + error.message } ) message.push( 'Hello' ) //   message.fail( new Error( 'Exception' ) ) //    ,      

image

It is important to keep in mind the promise limits:
1. handler is called deferred
2. The handler is called only once.

The then method returns an atom that listens to the original atom and when it accepts a non-undefined value, it calls the handler and self-destructs.

And now, finally, the FRP in action:

  var user = { firstName : new $jin.atom.prop({ value : 'Alice' }), lastName : new $jin.atom.prop({ value : 'McGee' }), getFullName : function(){ //        fullName : new $jin.prop.proxy(...) return user.firstName.get() + ' ' + user.lastName.get() } } var message = new $jin.atom.prop( { pull : function( ) { return 'Hello, ' + user.getFullName() }, notify : function( next , prev ) { document.body.innerText = next }, reap : function( ) { } } ) message.get() user.firstName.push( 'Alice' ) //   setTimeout( function( ) { user.lastName.push( 'Bob' ) //   }, 1000 ) 

image

Here, in general, everything is simple: message is implicitly declared as a function of the user.firstName and user.lastName properties, and when at least one of them changes, the message changes, and this is reflected in the document. There are two features here:
1. Atoms are lazy. Until someone pulls them (via get or pull), they will be inactive.
2. Atoms are prone to suicide. If you do not override the behavior of reap, then atoms will destroy themselves, freeing memory when there is not a single atom dependent on them.

Let's implement an atom that will track the pointer coordinates:

  //    var pointer = { handler : function( event ) { var point = event.changedTouches ? event.changedTouches[0] : event //        pointer.position.push([ point.clientX , point.clientY ]) event.preventDefault() }, position : new $jin.atom.prop( { pull : function( prev ) { //      document.body.addEventListener( 'mousemove' , pointer.handler , false ) document.body.addEventListener( 'dragover' , pointer.handler , false ) document.body.addEventListener( 'touchmove' , pointer.handler , false ) document.body.addEventListener( 'pointermove' , pointer.handler , false ) //   ,     return [ -1, -1 ] }, reap : function( ) { //       //   - document.body.removeEventListener( 'mousemove' , pointer.handler , false ) document.body.removeEventListener( 'dragover' , pointer.handler , false ) document.body.removeEventListener( 'touchmove' , pointer.handler , false ) document.body.removeEventListener( 'pointermove' , pointer.handler , false ) //  ,     pull      pointer.position.clear() } } ) } //     var title = new $jin.atom.prop( { pull : function( ) { return 'Mouse coords: ' + pointer.position.get() }, notify : function( next , prev ) { document.body.innerText = next }, reap : function( ) { } } ) title.pull() //  5     setTimeout( function( ) { title.disobeyAll() }, 5000 ) 

image

Typed atoms


Sometimes when changing the value of an atom, special logic is needed, which is different from the basic “new value replaces the old one”. For example, if a Date instance is stored in an atom, then when inserted into an atom, it would be good to verify. does he really point to another timestamp? This is done by overriding the merge interface:

  var lastUpdated = new $jin.atom.prop( { merge : function( next , prev ) { if( !prev ) return next if( prev.getTime() === next.getTime() ) return prev return next }, notify : function( next , prev ) { document.body.innerText += next.getFullYear() } } ) lastUpdated.push( new Date( 2014 , 1 , 1 ) ) //    2014 lastUpdated.push( new Date( 2014 , 1 , 1 ) ) //   lastUpdated.push( new Date( 2015 , 1 , 1 ) ) //    2015 

As the name suggests, the merge interface generally does not just a check, but also a merge of values. For example, we need to store scattered data in it by key:

  var userInfo = new $jin.atom.prop( { value : {}, merge : function( next , prev ) { //   var updated = false for( var key in next ) { if( prev[ key ] === next[ key ] ) continue prev[ key ] = next[ key ] updated = true } //  ,    if( updated ) this.notify() return prev } }) userInfo.push({ firstName : 'Alice' }) userInfo.push({ lastName : 'McGee' }) userInfo.get() // { firstName: "Alice", lastName: "McGee" } 

In the chapter on properties, the main interfaces of variables and properties were listed, but there are many others:

  a ++ //      1    a += N //      N    //       

These interfaces are for primitives. Their behavior is rigidly defined and cannot be redefined. But we have custom containers! Let's write your container for numeric values:

  module $jin.atom { export class numb < OwnerType extends $jin.object > extends $jin.atom.prop < number , OwnerType > { summ( value ) { this.set( this.get() + value ) } multiply( value ) { this.set( this.get() * value ) } //     } } var count = new $jin.atom.numb({ value : 5 }) //     count.summ( -1 ) //    1 count.multiply( 2 ) //    count.get() //    (8) 

Here, in the example, TypeScript is already used, since inheritance in JavaScript is not very obvious because of what each framework has its own helper that implements it. You can use them, as well as $ jin.atom.prop and $ jin.atom.numb and all the others are the most common javascript "functions with a prototype."

But we are not limited to primitives alone - it is useful, for example, to have atoms for collections:

  module $jin.atom { //    export class list<ItemType,OwnerType extends $jin.object> extends $jin.atom.prop<ItemType[],OwnerType> { // ,         merge( next : ItemType[] , prev : ItemType[] ) { next = super.merge( next , prev ) if( !next || !prev ) return next if( next.length !== prev.length ) return next for( var i = 0 ; i < next.length ; ++i ) { if( next[ i ] !== prev[ i ] ) return next } return prev } //      append( values : ItemType[] ) { var value = this.get() value.push.apply( value, values ) this.notify( null , value ) //          } //      prepend( values : ItemType[] ) { var value = this.get() value.unshift.apply( value, values ) this.notify( null , value ) } //     } } var list = new $jin.atom.list({ value : [ 3 ] }) list.append([ 4 , 5 ]) list.prepend([ 1 , 2 ]) list.get() // [ 1 , 2 , 3 , 4 , 5 ] 

Summary


Well, it's time to try it yourself. But before that, I have to warn you that the project lives on pure enthusiasm, it is developed in its spare time, by one person, without any community or investment, so it does not have comprehensive documentation, heaps of examples, manuals, and answers to StackOverflow. If you are interested in this topic - do not hesitate to ask questions, report on the doorposts, express ideas or even send patches.

The assembled JS library ~ 27KB without compression
Sources on TypeScript
JSFiddle stock

Main classes:
$ jin.prop.proxy - stateless property
$ jin.prop.vary - state property
$ jin.atom.prop - reactive property

Constructor parameters (all optional):
owner - the owner of the atom, which must have a globally unique identifier in the objectPath field
name - the name of the atom, unique within the owner
value - the initial value
get (value: T): T - called on every request for a value, by default, proxies the parameter
pull (prev: T): T - called to “pull in” values ​​from leading states (for example, from the server), by default it returns the current value
merge (next: T, prev: T): T - called to validate and / or merge the new value with the current one, by default it returns the new value
put (next: T, prev: T): void - reverse the pull operation, transfer the new value to the leading states (for example, to the server), by default write the new value to the atom
reap (): void - is called. when an atom is not signed by anyone and it can be safely removed, which is what the default does
notify (next: T, prev: T): void - called when the current value changes, does nothing by default
fail (error: Error): void - called when an exception object is saved instead of the current value

The basic methods of atoms:
get () - get value
pull () - force a value
update () - schedule update value
set () - to offer a new value (which it can not write to itself but to the leading state)
push () - force a new value
fail (error) - force write exception object
mutate ((prev: T) => T) - apply the transformation function
then ((next: T1) => T2) - perform the function when the atom accepts the current value
catch ((error: Error) => T2) - execute the function when the atom accepts the exception object

image

Source: https://habr.com/ru/post/240773/


All Articles