Pull to refresh

Как я классы в Vim писал

Reading time 9 min
Views 11K
image

Предисловие


Уже около четырех лет увлекаюсь языком JS, а особенно привлекает в нем прототипная реализации объектной ориентации и замыкания. Так как я большой любитель «велотренажеров» в программировании и обожаю изучать что-то новое на практических примерах, давно хотел попробовать реализовать подобное самостоятельно, и совсем недавно шанс выпал. Одним холодным зимним днем я увлекся редактором Vim и, изучая его скриптовый язык, обратил внимание на некоторые важные особенности, а именно ассоциативные массивы и передачу функций по ссылке. Я не смог пройти мимо и реализовал свою прототипную объектную ориентацию в Vim с наследованием и полиморфностью.

Хочу сразу обрадовать тех, кто не знаком с синтаксисом скриптового языка Vim, я постараюсь сопровождать код подробными комментариями. Оговорюсь, что целью этой работы было не создание полноценной объектной ориентации в Vim, а практика в реализации объектной парадигмы через прототипирование. Я, конечно, пытался сделать реализацию максимально легковесной и быстрой, но все равно сомневаюсь, что полученный результат можно эффективно применять в «боевых» скриптах, потому, прошу относиться к этому соответственно.


С чего пришлось начать


Для начала пара слов о том, что есть в Vim «из коробки» и что будем использовать для работы.
Типизация — куда же без нее, имеет 6 типов данных: Целые числа, Дробные числа, Строки, Указатель на функции, Массивы, Ассоциативные массивы. Здесь наиболее интересным для нас является последний тип. Ассоциативные массивы создаются так же, как и в JS:
'' Создаем «объект» и помещаем его в переменную
let Obj = {'foo': 'bar'}

при этом в качестве значений могут выступать функции. Прекрасная основа для будущих объектов.

Переменные — ничего особенного, кроме важного «синтаксического сахара» — возможность обращаться к элементам ассоциативного массива через точку:
'' Выводим на экран содержимое свойства foo
echo Obj.foo

удобно, не то слово!

Функции — важной особенностью является то, что можно закреплять функцию за «объектом», то есть при вызове функции из ассоциативного массива, можно получить к нему доступ через переменную self:
function! Obj.getFoo() dict
  return self.foo
endfunction

Так же не следует упускать из виду возможность добавления функций прямо в объекты.

Стандартные структуры, такие как if, for и так далее — какой же скриптовый язык без них?!

Теория


Подробно изучив возможности скриптового языка Vim (далее для сокращения буду просто писать язык Vim), принялся думать о том, что же обязательно должно быть для прототипной объектной ориентации, и вот что удумал:
  • Классы — а точнее прототипы, то есть некие объекты, на основании которых создаются другие объекты (экземпляры этих классов);
  • Наследование — без этой прекрасной возможности, тяжело будет расширять имеющиеся классы;
  • Строгая типизация и полиморфность — хотелось бы, чтобы свойства будущих объектов были строго типизированы, а если свойство ожидает определенный класс, то в него можно записать и объекты дочерних классов;
  • Пакеты, namespace и use — иначе получим захламление глобальной области видимости и необоснованное расходование памяти.


В идеале все должно было выглядеть примерно так:
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)


Обратите внимание на процесс создания нового класса. Мы расширяем некоторый базовый класс путем дополнения его свойствами, при этом свойство foo имеет тип string, а свойство bar тип, соответствующий самому базовому классу, то есть Class. Что это должно означать на практике — в свойство foo мы можем записать с помощью метода set только строку, а в свойство bar только объект класса Class или его подклассов (в том числе и MyClass по понятным причинам). Во втором случае, если мы попытаемся записать в свойство bar объект класса MyClass, то он должен быть «ограничен» до структуры класса Class, то есть в нем не должно быть свойств foo и bar. Так же должна быть возможность вновь расширить объект до класса MyClass, но только если это необходимо (по аналогии с Java).

Не будем забывать и о конструкторе, нам нужна возможность определения состояния объекта при его создании, на пример так:
let obj = new MyClass('val')


Добавим сюда еще возможность определения методов и их переопределение, и получим настоящую прототипную объектную ориентацию.

Первые шаги


Конечно на практике все не так, как в теории, многое реализовать в Vim нельзя из за особенностей языка, от чего-то пришлось отказаться из за высокой громоздкости, в результате получил следующее:
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')


Дело начал со следующей идеи: нужно разделить все объекты на два типа, те, которые представляют классы и содержат информацию о структуре своих экземпляров и специальные методы, такие как наследование и конструктор, и те, которые представляют объекты. Идея мне понравилась и я приступил к ее реализации, а начал с базового класса Object и двух основных методов expand и _construct:
let Object = {'class': 'Object'}
function! Object.expand(className, properties) dict
endfunction
function! Object._construct() dict
endfunction


В связи с тем, что объекты в Vim не имеют имен, нужно было как то хранить имя класса, дабы иметь возможность типизировать их свойства этими классами. Для этого было добавлено свойство class, которое хранит имя класса, а за одного отличает ассоциативные массивы Vim от моих классов (если это свойство присутствует, значит мы имеем дело с классом или его экземпляром). Метод expand принимает два параметра, имя нового класса и его свойства, и возвращает объект, представляющий новый класс, созданный на основе вызываемого класса (то есть прототипа). Метод _construct просто создает объект вызываемого класса и возвращает его.

Начнем с первого метода. Алгоритм его довольно простой: создаем новый объект, добавляем в него свойство class со значением, переданным в первом параметре, дабы будущий класс имел имя; добавляем ссылку на родительский класс с помощью свойства parent; для удобства создаем ссылки в текущем объекте на все методы родительского класса; дополняем полученный объект свойствами на основе второго параметра. Последний шаг самый важный, дело в том что мы не просто копируем свойства из параметра, а создаем структуру будущего класса. В частности мы определяем тип свойства и его значение по умолчанию.

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


Благодаря ссылке на родительский класс нам нет необходимости копировать все его свойства в дочерний, а отсутствие свойства parent говорит о том, что это корневой класс Object.

Теперь поговорим о конструкторе. Метод _construct создает и возвращает объект на основе класса, путем копирования значений свойств класса в объект. Копировать методы не имеет смысла, так как они одинаковы для всех объектов, потому, для удобства, добавим только ссылки из объектов на методы класса:
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


Обратите внимание на то, как собирается объект по иерархии наследования. Для каждого класса создается отдельный объект (субобъект), после чего они все склеиваются в один объект ссылками. Это позволяет реализовать регрессию объектов при записи объекта в свойство с типом родительского класса, а так же делает процесс создания объекта более элегантным (копировать все свойства всех классов в объект? Фуу!)
Уже сейчас проявляются некоторые свойства прототипов, а именно: все есть объект; объекты реализуются путем копирования свойств в другие объект. Другими словами прототипная объектная ориентация это простейший подход к реализации объектов в языках программирования.

Расширяемся


По сути, описанные методы это фундамент всей реализации. Далее нужно только добавить некоторые дополнительные методы для удобства, а именно:
  1. new — конструктор с параметрами;
  2. get — получение значения свойства;
  3. set — установка значения свойства с проверкой типа;
  4. has — имеется ли указанное свойство у объекта с учетом иерархии наследования;
  5. instanceup — получение субобъекта вверх по иерархии наследования (регрессия);
  6. instancedown — получение субобъекта вниз по иерархии наследования (прогрессия);
  7. instanceof — принадлежит ли объект указанному классу или его подклассам.


Прежде чем описывать реализацию этих методов (думаю вы уже и сами знаете как их можно реализовать), покажу как с ними работать:
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


Интересно, не правда ли? Не буду приводить подробные листинги методов, они есть на GitHub с подробными комментариями, опишу только алгоритмы работы каждого метода:
  1. new — зачем лишний конструктор? Дело в том, что при переопределении стандартного конструктора, в вызываемом классе не сработает механизм копирования свойств уровня этого класса, а нам это не нужно;
  2. get — проще некуда, если свойство имеется в данном объекте или субобъектах выше по иерархии наследования, возвращаем, иначе сообщаем об ошибке;
  3. set — тоже ничего сложного, проверяем тип и записываем в текущий объект или субобъекты. Важно только выявить объекты наших классов и выполнить регрессию при необходимости;
  4. has — тоже все довольно просто, ищем свойство в текущем объекте и субобъектах;
  5. instanceup — поднимаемся по иерархии субобъектов с помощью свойства parent и сверяем имя класса с запрашиваемым, если найдено, возвращаем объект;
  6. instancedown — аналогично предыдущему, только используем свойство child;
  7. instanceof — вызваем instanceup и есри возвращен объект, то возвращаем true.


Use и namespace


К сожалению, пока реализовать толковый namespace не удалось. Пробовал по аналогии с namespace в YUI 3, но получилось не очень симпатично. Use же реализовал довольно просто:
comm! -nargs=1 Use so $HOME/.vim/ftplugin/vim/<args>.vim


Определяем команду Use, которая подключает файл .vim/ftplugin/vim/адресФайла.vim, при этом каждый класс располагается отдельном файле, то есть:
Use D/base/Object

это файл Object.vim расположенный в каталоге .vim/ftplugin/vim/D/base.
Пример использования

Дабы протестировать полученный базовый класс, реализовал класс Stack, представляющий множества с типом доступа «Стек». Ниже пример использования:
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


По моему довольно симпатично, а главное функционально. Полноценная (надеюсь) объектная ориентация на базе прототипов в Vim весом всего в 4 кило.

Список литературы и ресурсов


  1. Проект на GitHub
  2. Книга «Просто о Vim»
  3. О создании скриптов в Vim наглядно и с примерами
Tags:
Hubs:
+19
Comments 3
Comments Comments 3

Articles