
Foreword
For about four years, I have been fascinated with the JS language, and especially attracted to him the prototype implementation of object orientation and closure. Since I am a big fan of “exercise bikes” in programming and I love to learn something new with practical examples, I have long wanted to try to implement this on my own, and quite recently I had a chance. One cold winter day, I was fascinated by the Vim editor, and studying its scripting language, I drew attention to some important features, namely associative arrays and passing functions by reference. I could not pass by and realized my prototype object orientation in Vim with inheritance and polymorphism.
I want to immediately please those who are not familiar with the syntax of the Vim scripting language, I will try to accompany the code with detailed comments. I’ll make a reservation that the goal of this work was not to create a full-fledged object orientation in Vim, but to practice the implementation of the object paradigm through prototyping. Of course, I tried to make the implementation as light and fast as possible, but I still doubt that the result can be effectively applied in “combat” scripts, so please treat this accordingly.
What had to start
First, a few words about what is out of the box in Vim and what we will use for work.
Typing - where without it, has 6 data types: Integers, Fractional Numbers, Strings, Function Pointer, Arrays, Associative Arrays. Here the most interesting for us is the latter type. Associative arrays are created in the same way as in JS:
'' «» let Obj = {'foo': 'bar'}
at the same time functions can be values. Perfect base for future objects.
')
Variables - nothing special, except for the important "syntactic sugar" - the ability to access the elements of an associative array through a dot:
'' foo echo Obj.foo
comfortable, not the word!
Functions - an important feature is that you can assign a function to an “object”, that is, when you call a function from an associative array, you can access it through the self variable:
function! Obj.getFoo() dict return self.foo endfunction
Also, the possibility of adding functions directly to objects should not be overlooked.
Standard structures, such as if, for, and so on - what a scripting language without them ?!
Theory
Having studied in detail the possibilities of the Vim scripting language (hereinafter, I’ll simply write Vim), I began to think about what must necessarily be for the prototype object orientation, and this is what I thought:
- Classes - or rather, prototypes, that is, certain objects, on the basis of which other objects are created (instances of these classes);
- Inheritance - without this great opportunity, it will be hard to expand the existing classes;
- Strong typing and polymorphism - I would like the properties of future objects to be strongly typed, and if the property expects a certain class, then objects of the child classes can be written to it;
- Packages, namespace and use - otherwise we will get cluttering up the global scope and unreasonable memory consumption.
Ideally, everything should look like this:
use foo\bar\Class as Class '' MyClass Class foo bar let MyClass = Class.expand({'foo': '', 'bar': Class}) let obj = new MyClass() call obj.set('prop', 'val') echo obj.get('prop') call obj.set('bar', obj)
Pay attention to the process of creating a new class. We extend some base class by adding its properties, while the foo property is of type string, and the bar property is the type corresponding to the base class itself, that is, Class. What this should mean in practice - using the set method, we can write only a string to the foo property, and only an object of the Class class or its subclasses (including MyClass for obvious reasons) to the bar property. In the second case, if we try to write an object of the class MyClass into the bar property, then it should be “restricted” to the structure of the Class class, that is, it should not have the foo and bar properties. It should also be possible to re-expand the object to the class MyClass, but only if necessary (by analogy with Java).
Let's not forget about the constructor, we need the ability to determine the state of the object when it is created, for example:
let obj = new MyClass('val')
Add here the possibility of defining methods and redefining them, and we get a real prototype object orientation.
The first steps
Of course, in practice, everything is not the same as in theory, many things cannot be realized in Vim because of the peculiarities of the language; something had to be abandoned due to its high bulkiness, and as a result I received the following:
Use D/base/Object " A Object let A = Object.expand('A', {'x': 1}) " A . function! A.new(x) dict let obj = self._construct() call obj.set('x', a:x) return obj endfunction " B A let B = A.expand('B', {'y': Object}) " B function! B.new(x, y) dict let obj = self._construct() " , call obj.set('x', a:x) call obj.set('y', a:y) return obj endfunction " let s:a = A.new(2) let s:b = B.new(3, s:a) echo s:b.get('x') " Object, echo s:b.get('y') " echo s:b.get('y').instancedown('A').get('x')
The case began with the following idea: you need to divide all objects into two types, those that represent classes and contain information about the structure of their instances and special methods, such as inheritance and constructor, and those that represent objects. I liked the idea and started to implement it, and I started with the Object base class and the two main methods expand and _construct:
let Object = {'class': 'Object'} function! Object.expand(className, properties) dict endfunction function! Object._construct() dict endfunction
Due to the fact that objects in Vim do not have names, it was necessary to somehow store the name of the class in order to be able to typify their properties with these classes. For this, the class property has been added, which stores the name of the class, and for one it distinguishes the associative arrays of Vim from my classes (if this property is present, it means we are dealing with a class or its instance). The expand method takes two parameters, the name of the new class and its properties, and returns an object representing the new class created from the class being called (that is, the prototype). The _construct method simply creates an object of the called class and returns it.
Let's start with the first method. Its algorithm is quite simple: create a new object, add a class property to it with the value passed in the first parameter, so that the future class has a name; add a reference to the parent class using the parent property; for convenience, we create references in the current object to all methods of the parent class; We supplement the resulting object with properties based on the second parameter. The last step is the most important, the fact is that we do not just copy the properties from the parameter, but create the structure of the future class. In particular, we define the type of property and its default value.
function! Object.expand(className, properties) dict let obj = {'class': a:className} " . " parent. let obj.parent = self " . " . for k in keys(self) let t = type(self[k]) " if t == 2 let obj[k] = self[k] endif endfor " . " . for k in keys(a:properties) " class parent, if k == 'class' || k == 'parent' continue endif let t = type(a:properties[k]) " , if t == 0 || t == 1 || t == 5 let obj[k] = {'value': a:properties[k], 'type': t} " , elseif t == 3 let obj[k] = {'value': deepcopy(a:properties[k]), 'type': t} " , elseif t == 4 " if has_key(a:properties[k], 'class') let obj[k] = {'value': '', 'type': a:properties[k].class} " elseif let obj[k] = {'value': a:properties[k], 'type': 4} endif endif endfor return obj endfunction
Thanks to the link to the parent class, we don’t need to copy all of its properties to the child class, and the lack of the parent property indicates that this is the root class Object.
Now let's talk about the constructor. The _construct method creates and returns an object based on a class by copying the property values ​​of the class into an object. Copying methods does not make sense, since they are the same for all objects, therefore, for convenience, we will add only references from objects to class methods:
function! Object._construct() dict let obj = {} " let obj.class = self if has_key(self, 'parent') " let obj.parent = self.parent._construct() " , let obj.parent.child = obj endif for property in keys(self) let type = type(self[property]) " , if type == 2 && property != 'expand' && property != '_construct' && property != 'new' let obj[property] = self[property] " elseif type == 4 && has_key(self[property], 'type') let propertyType = self[property].type if propertyType == 0 || propertyType == 1 || propertyType == 5 let obj[property] = self[property].value elseif propertyType == 3 let obj[property] = deepcopy(self[property].value) elseif propertyType == 4 if has_key(self[property].value, 'class') let obj[property] = self[property].value._construct() else let obj[property] = deepcopy(self[property].value) endif endif endif endfor return obj endfunction
Notice how the object is built according to the inheritance hierarchy. For each class, a separate object (subobject) is created, after which they are all glued into one object by reference. This allows you to regress objects when you write an object to a property with the type of the parent class, and also makes the process of creating an object more elegant (copy all the properties of all classes to an object? Fuu!)
Some properties of prototypes are already manifest, namely: everything is an object; objects are implemented by copying properties to other objects. In other words, prototype object orientation is the simplest approach to implementing objects in programming languages.
Expanding
In essence, the methods described are the foundation of the entire implementation. Next you just need to add some additional methods for convenience, namely:
- new - constructor with parameters;
- get - getting the property value;
- set - setting the value of the property with type checking;
- has - whether the object has the specified property with regard to the inheritance hierarchy;
- instanceup - getting the subobject up the inheritance hierarchy (regression);
- instancedown - getting a subobject down the inheritance hierarchy (progression);
- instanceof - whether the object belongs to the specified class or its subclasses.
Before describing the implementation of these methods (I think you yourself already know how to implement them), let me show you how to work with them:
let A = Object.expand('A', {'a': 1}) let B = Object.expand('B', {'b': A}) '' function! B.new(b) dict let obj = self._construct() obj.set('b', b) return obj endfunction let s:a = A.new() call s:a.set('a', 2) let s:b = B.new(s:a) '' , echo s:b.class.class '' B '' , b echo s:b.get('b').class.class '' A '' b B. , B A. call s:b.set('b', s:b) '' , A echo s:b.get('b').class.class '' A '' s:a, a 2 ( ), 1 ( s:b) echo s:b.get('b').get('a') '' 1 '' , b , B echo s:b.get('b').instancedown('B').class.class '' B
Interesting, isn't it? I will not give detailed listings of the methods, they are on GitHub with detailed comments, I will describe only the algorithms of each method:
- new - why extra constructor? The fact is that when redefining the standard constructor, the called class will not trigger the mechanism of copying the level properties of this class, but we do not need it;
- get - nowhere is easier, if the property is present in this object or subobjects higher in the inheritance hierarchy, we return, otherwise we report an error;
- set is also nothing complicated, check the type and write to the current object or subobjects. It is only important to identify the objects of our classes and regress if necessary;
- has is also pretty simple, look for the property in the current object and subobjects;
- Instanceup - go up the hierarchy of subobjects using the parent property and check the class name with the requested one, if found, return the object;
- instancedown - similar to the previous one, just use the child property;
- instanceof - cause instanceup and if an object is returned, then return true.
Use and namespace
Unfortunately, for the time being we have failed to implement an intelligent namespace I tried by analogy with the namespace in YUI 3, but it turned out not very nice. Use implemented quite simply:
comm! -nargs=1 Use so $HOME/.vim/ftplugin/vim/<args>.vim
We define the Use command, which includes the .vim / ftplugin / vim / File.vim file address, with each class located in a separate file, that is:
Use D/base/Object
This is an Object.vim file located in the .vim / ftplugin / vim / D / base directory.
Usage example
In order to test the resulting base class, implemented the Stack class, which represents sets with the Stack access type. Below is an example of use:
if exists('Stack') finish endif Use D/base/Object let Stack = Object.expand('Stack', {'_val': [], '_index': 0}) function! Stack.push(el) dict … endfunction function! Stack.pop() dict … endfunction function! Stack.length() dict … endfunction let s:stack = Stack.new() call s:stack.push(1) call s:stack.push(2) call s:stack.push(3) echo s:stack.length() '' 3 echo s:stack.pop() '' 3 echo s:stack.pop() '' 2 echo s:stack.pop() '' 1 echo s:stack.pop() '' ERROR
In my opinion quite nice, and most importantly functional. Full (hopefully) object orientation based on prototypes in Vim, weighing only 4 kilos.
References and resources
- GitHub project
- The book "Just about Vim"
- About creating scripts in Vim clearly and with examples