Atom - TypeScript implementation

    Hello, my name is Dmitry Karlovsky and I ... a professional cyclist. During my life I tried a lot of iron horses, but ultimately settled on a makeshift. It’s not that I really liked working with a file, spending a lot of free time on the invention of the wheel, but the end result, where each bump does not give off pain in the lower half of the body, is worth it. And now, when you know that I started all this for a reason, but to make the world a better place, let me introduce you the TypeScript / JavaScript module $ jin.atom.

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

    Why TypeScript?


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

    TypeScript has two alternatives:

    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 more than actually useful code. Case in point:

    	/**
    	 * @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 to be broadcast in JavaScript. It uses completely different idioms, due to which it has many limitations when integrating with JavaScript code, significantly higher memory consumption, problems when debugging using the built-in browsers tools, and the code generated from Dart is quite voluminous noodles. The above example, it 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 also used structural:

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

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

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

    In total, when switching to TypeScript:
    + code is reduced
    + integration with the development environment is improved
    + additional validation appears as you type
    - the need for translation in JavaScript before execution is added

    Myths and legends FRP


    Reactive libraries can be divided into two main types:
    1. Actually FunctionalRP, where the entire application is described as a lot of pure functions.
    2. ProceduralRP, which is often confused with FRP. In them, the application is described 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, sequentially apply certain transformations to them, 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' )
    	}
    

    Well-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 branch will 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 one:

    	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 given time this value depends on only two out of three streams. In the first version there is no such problem, but this is achieved by a significant complication of the 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 there are relatively few data sources and their composition is practically unchanged, while 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 in many different streams, and in FRP exactly one function is responsible for one state, according to which it is always clear how the value is formed and what it directly depends on.

    Another popular misconception is that reactivity is only needed at the junction of a model and a representation. However, in reality, 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.

    The properties


    Before undertaking the implementation of atoms, it is worth distinguishing between two concepts: value (RValue) and container (LValue).

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

    	var count // создать контейнер если ещё не создан
    	count = 2 //записать значение
    	return count // вернуть значение
    

    Another, no less 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 many interfaces and they look completely different. To implement more complex containers, of which atoms are undoubtedly, we need much more interfaces, so we implement such a container as a class. Here's what a variable implementation might look like:

    	var count = new $jin.prop.vary({}) // создать контейнер
    	count.set( 2 ) // записать значение
    	count.get() // прочитать значение
    

    image

    On the one hand, we changed the awl for soap: the container (the variable count) stores in itself another container (an instance of the class $ jin.prop.vary) which stores the value itself. On the other hand, the container object, unlike a regular variable, is already an entity 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 vast majority of cases it is unnecessary. Much more useful if the implementation of the interfaces differs 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 is an implementation of a stateless container, which can be either an “ordinary variable” or an “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 set calls put. Such a replacement was not made for a reason - in the general case, these are really completely different interfaces. To understand the difference, it is enough to enter the state and add obvious conditions:
    1) get calls pull only if the value has not been set yet, otherwise it just returns it - the so-called “lazy initialization”
    2) set calls put only if the value being set is different from the current one - this prevents execution put idle.

    For example, if we work with the name of the document only through our container, we can define it so that we don’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 more simply:

    	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’s worth doing for properties that do not need the ability to inherit. But if you declare a property like this in the prototype of the class, then all instances will work with the same container, which is usually not what you need. But it is necessary that each instance has its own containers. To do this, we create a container through a getter and pass it a link to the object and the field name in it - it is in it that the container will save its data (or will save itself - it depends on the implementation). Another striking 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 delegating to 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 here - when we change the value of the atom, the notify (or fail) function is immediately called, in which we can imperatively reflect the state change on the o-runtime. Normally, the FRP code of the application practically does not need such manual synchronization - most of them are easily eliminated by the declarative description of Werski, according to which similar synchronizing atoms are already generated automatically. But this is the topic of a separate large article, so further we will concentrate on the capabilities of the atoms themselves.

    The atom is a generalization of the "promise", so it is not surprising that it supports the 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 limitations of promises:
    1. the 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, 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, message also 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 suicidal. If you do not redefine the behavior of reap, then the atoms will destroy themselves, freeing up memory, when there is not a single atom that depends on them.

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

    	// провайдер координат указателя
    	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, a special logic is required that is different from the basic one: “a new conception replaces the old”. For example, if a Date instance is stored in an atom, then it would be nice to check when pasted into an atom. but does he really point to another timestamp. This is done by redefining 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 check, but also merge the values. For example, we need to store disparate data by key in it:

    	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" }
    

    The chapter on properties listed the main interfaces of variables and properties, 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 our container for numerical 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 clear due to which 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 rest are the most common javascript "prototype functions".

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

    	module $jin.atom {
    		// атом для списков
    		export class list extends $jin.atom.prop {
    			// проверяем, а действительно ли новый список отличается от старого
    			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 first, I must warn that the project lives on pure enthusiasm, is developed in my spare time from one’s main job, by one person, without any community or investment, so I don’t have comprehensive documentation, lots of examples, manuals and answers to StackOverflow. If you are interested in this topic - do not hesitate to ask questions, report jambs, express ideas or even send patches. Compiled

    JS library ~ 27KB without compression
    TypeScript sources Sources for
    JSFiddle

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

    Constructor parameters (all are optional ):
    owner - the owner of the atom, which must have a globally unique identifier in the field objectPath
    name - the name of the atom, unique within the owner of
    value - the initial value
    get (value: T): T - is called every time the value is requested, the
    pull parameter is proxied ( default : T): T - called to “retract” the value from leading states (for example, from the server), by default returns the current value
    merge (next: T, prev: T): T - called to validate and / or merge the new value with the current by default returns the new value of the
    put (next: T, prev: T): void - the inverse of the operator pull tion, the transfer of new value to the leading state (eg, the server), writes a new value in the atom by default
    reap (): void - called. when no one is subscribed to the atom and it can be painlessly deleted, which by default
    does notify (next: T, prev: T): void - is called when the current value changes, the default does
    fail (error: Error): void - called when an exception object is saved instead of the current value.

    The main atomic methods:
    get () - get the
    pull () value - force the
    update () value to be calculated - schedule the
    set () value to be updated - offer a new value (which it may not write to itself but to the leading state)
    push () - force write the new value
    fail ( error) - force write exception object
    mutate ((prev: T) => T) - apply the transformation function
    then ((next: T1) => T2) - execute the function when the atom takes the actual value
    catch ((error: Error) => T2) - execute the function, when an atom takes an exception object

    image

    Only registered users can participate in the survey. Please come in.

    In conclusion, I would like to ask what is better to write further?

    • 13.1% Internal structure of atoms with analysis of real code 8
    • 68.8% Comparison with other popular solutions (Angular, KO, Meteor, Backbone, Basis, offer your own) 42
    • 18% Reactive MVP and application layering 11

    Also popular now: