Я считаю VimScript крайне недружелюбным, но неожиданно мощным языком. К счастью его возможно одружелюбить, чем я и предлагаю вам заняться в этом цикле статей. Здесь мы рассмотрим некоторые решения типичных задач на VimScript с использованием объектно-ориентированной парадигмы, но не будем касаться основ этого языка. Возможно статья будет интересна так же программистам, интересующимся нестандартной реализацией ставших нам привычными решений.




Объектная-ориентация может быть минималистичнее


Возможно некоторые из вас уже читали мои статьи о VimScript и изучали мою библиотеку vim_lib, не правда ли она удобна и проста в использовании? Не правда! Порой "запахи кода" так режут мне глаза, что я не могу его читать. Даже слабый "запашок" вызывает у меня непреодолимое желание сделать "лучше", "правильнее", "проще". К счастью это не сложно, достаточно все еще больше упростить и у меня это получилось, но сейчас не об этом. В этом цикле статей я лишь приведу шаблонные решения (паттерны если вам будет угодно) конкретных задач, а не буду изобретать новую библиотеку.

За более чем год использования моего класса Object в VimScript я убедился, что он содержит "код для галочки", от которого можно безболезненно избавится. Когда появляется "такой запах", это означает что пора все упрощать. В частности от чего можно смело отказаться при реализации объектно-ориентированной модели в VimScript:

  • Классы — их нет как таковых. Класс сводится к набору методов и конструктору, который умеет создавать объекты, расширять их этими методами и инициализировать свойства
  • Инкапсуляция — чем городить костыльный велосепед, проще условится и не использовать свойства объекта напрямую. Раз язык не реализует инкапсуляцию на прямую, не следует мучать его
  • Статичные свойства и методы — это полезная вещь, но не настолько полезная, чтобы заполнять конструктор условиями, выбирающими только не статичные свойства и методы для копирования в объект. Если нужна статика, лучше реализовать ее в виде глобального сервиса

Возможно вы уже задаетесь вопросом: "Как же реализовать объектно-ориентированную модель без классов?" — все крайне просто. Для этого нам нужна одна функция на каждый тип объектов, которая называется конструктором. Эта функция должна создавать и возвращать нам инициализированный объект с нужной структурой. Напоминает JavaScript, не так ли? Вот как это выглядит в готовом виде:

Базовый класс
let s:Parent = {}
function! s:Parent.new(a) dict
  return extend({'a': a:a}, s:Parent)
endfunction

function! s:Parent.setA(a) dict
  let l:self.a = a:a
endfunction

function! s:Parent.getA() dict
  return l:self.a
endfunction

let s:pobj = s:Parent.new('foo')
echo s:pobj.getA() " foo


Четыре строчки кода для реализации целого класса. Это решение сводится к инициализации нового словаря и расширению (с помощью функции extend) его методами прототипа.

Далее рассмотрим реализацию наследования с переопределением конструктора и одного из методов родительского класса:

Дочерний класс
let s:Child = {}
function! s:Child.new(a, b) dict
  return extend(extend({'b': a:b}, s:Parent.new(a:a)), s:Child)
endfunction

function! s:Child.setB(b) dict
  let l:self.b = a:b
endfunction

function! s:Child.getB() dict
  return l:self.b
endfunction

function! s:Child.getA() dict
  return call(s:Parent.getA, [], l:self) . l:self.b
endfunction


Всего то конструктор дополняется еще одним вызовом функции extend, что позволяет расширить базовый словарь сначала объектом родительского класса, а затем методами прототипа (дочернего класса). В свою очередь вызов родительского метода из переопределяющего так же довольно просто реализуется с помощью функции call (аналог apply в JavaScript).

Дальнейшее наследование реализуется без добавления новых вызовов extend:

Дальнейшее наследование
let s:SubChild = {}
function! s:SubChild.new(a, b, c) dict
  return extend(extend({'c': a:c}, s:Child.new(a:a, a:b)), s:SubChild)
endfunction


Миксины


Внимательный читатель уже догадался, что здесь реализовано множественное наследование, что позволяет использовать миксины:

Дальнейшее наследование
let s:Publisher = {}
function! s:Publisher.new() dict
  return extend({'listeners': {}}, s:Publisher)
endfunction

let s:Class = {}
function! s:Class.new() dict
  return extend(extend({}, s:Publisher.new()), s:Class)
endfunction


Интерфейсы


Полиморфизм очень важная часть объектно-ориентированной парадигмы, и я не мог обойти ее стороной, тем более у меня имеется несколько плагинов, для которых она необходима. Чтобы сделать ее реальностью необходим метод instanceof, позволяющий оценить семантику класса. Все что от него требуется, это проверить, присутствуют ли в объекте методы, объявленные в целевом классе и если да, то можно считать его экземпляром данного класса. Почему именно методы, а не свойства? Потому что мы условились работать с объектами через методы. Это так называемая "Утиная типизация".

instanceof
function! s:instanceof(obj, class)
  for l:assertFun in keys(filter(a:class, 'type(v:val) == 2'))
    if !has_key(a:obj, l:assertFun)
      return 0
    endif
  endfor

  return 1
endfunction

echo s:instanceof(s:childObject, s:Parent) " 1
echo s:instanceof(s:childObject, s:SubChild) " 0


Имея такую прекрасную функцию, не сложно реализовать интерфейсы, определяющие семантику будущих классов:

Пример интерфейса
let s:Iterable = {}
function! s:Iterable.valid() dict
endfunction

function! s:Iterable.next() dict
endfunction

function! s:Iterable.current() dict
endfunction

let s:Iterator = {}
function! s:Iterator.new(array) dict
  return extend(extend({'array': a:array, 'cursor': 0}, s:Iterable), s:Iterator)
endfunction

function! s:Iterator.valid() dict
  return exists('l:self.array[l:self.cursor]')
endfunction

function! s:Iterator.next() dict
  let l:self.cursor += 1
endfunction

function! s:Iterator.current() dict
  return l:self.array[l:self.cursor]
endfunction

let s:iterator = s:Iterator.new([1,2,3])
echo s:instanceof(s:iterator, s:Iterable) " 1


Важно помнить, что объекты расширяются классом интерфейса, а не его экземпляром. Впрочем, как в любом другом языке.

JSON — легко!


Для кого то это будет открытием, но JSON — это двоюродный брат VimScript! Не верите? Я вам это докажу:

JSON
let s:childObj = s:Child.new(1, 2)
let s:json = string(filter(s:childObj, 'type(v:val) != 2'))
echo s:json " {'a': 1, 'b': 2}
echo eval(s:json) == s:childObj " 1


Пока все


Надеюсь эта статья вас заинтересует и побудит попробовать этот замечательный редактор. Если так, то я постараюсь сделать вашу жизнь проще в следующих статьях.

Комментарии (10)


  1. myxo
    05.03.2016 00:53

    Вы наверняка объяснили это в предыдущих статьях, но я, быстренько пробежавшись, не нашел ответа. Насколько я помню в vim можно писать плагины на питоне. Зачем тогда vimscript, дело в скорости?


    1. ZyXI
      05.03.2016 00:56

      Если дело в скорости, то писать нужно на чём угодно, кроме vimscript.

      Дело в том, что Vim можно скомпилировать так, что в нём не будет поддержки Python. Но нельзя так, чтобы в нём не было поддержки vimscript (хотя можно так, что vimscript там будет, но 99?% дополнений работать не будут).


    1. Delphinum
      05.03.2016 02:13
      +2

      Если хорошо постараться, под Vim можно писать на любом языке, но я так не люблю зависимости ((


      1. uvelichitel
        05.03.2016 14:37

        Скрипты vim всегда пишутся на vimscript, а внутри vimscript можно вызывать python командой :py
        И все же, как со скоростью? Вызов внешнего интерпретатора дает выигрыш? Есть объективные оценки? Что показывает ваш личный опыт?


        1. Delphinum
          05.03.2016 16:17

          Скрипты vim всегда пишутся на vimscript

          Совершенно верно.

          Вызов внешнего интерпретатора дает выигрыш?

          Объективных оценок не делал, но опыт есть.
          Если подойти к делу с умом, воспользоваться потоками при парсинге проекта и подобных, тяжелых операциях, то выигрышь явный. Проблема в том, что далеко не везде можно этим воспользоваться, так как на скорость сильно влияют все подключаемые плагины редактора, а не только один конкретный, и часто эти плагины настолько просты, что с помощью того же питона не сильно то оптимизировать получится. Возьмем к примеру плагин для вывода в качестве стартового меню списка последних рабочих проектов. Все что нужно этому плагину для работы, это один файл с этим списком, который будет считываться при старте редактора и отображаться. Что тут оптимизируешь? А вот когда подобных плагинов появляется десяток, то система начинает заметно тормозить на старте.

          Другая проблема это инициализация плагинов при старте редактора. Ведь чтоб они заработали интерпретатору нужно распарсить их скрипты и выполнить их. Не думаю, что использование стороннего языка сколько нибудь облегчит эту операцию. Я уповаю здесь на lazy load и активно его применяю. Благо в Vim есть механизм autoload кода, позволяющий подгружать код при его первом использовании, это сильно облегчает старт редактора. Именно поэтому я основную логику плагинов выношу в autoload. Эта модель еще не полностью мной реализована, есть над чем работать, но в голове уже решение имеется.

          Резюмировав вышесказанное: от сторонних языков есть толк при реализации сложной логики, которая не может быть разделена на мелкие составляющие и выполняться при первом использовании (типа парсинга всех файлов проекта, поиска в проекте и любых тяжелых, но атомарных операциях). Во всех остальных случаях VimScript себя показывает хорошо, если правильно разложить файлы плагинов и максимально использовать lazy load.


          1. ZyXI
            05.03.2016 18:39

            Lazy loading работает вне зависимости от того, на чём написано дополнение. На парсинг использование не?VimL влияет сильно: в Vim нет парсера, код исполняется напрямую. В ряде случаев он исполняется с флагом, говорящим, что делать ничего не нужно (к примеру, в случае с if, когда условие не выполняется), но в любом случае ни AST, ни байткода просто нет. Соответственно, с одной стороны, нет кэшей вроде *.pyc даже внутри самого Vim и исполняться всё будет намного дольше, с другой стороны добавление вещей вроде AST и байткода замедлит выполнение всего, что выполняется один раз, т.к. придётся тратить ресурсы на различные преобразования, при однократном исполнении излишние.

            Ещё конкретно function A()…endfunction будет обработано быстрее, чем Python обработает def a():… (возможно, даже в байткоде), т.к. текущий «парсер» просто ищет строку с endfunction, даже не пытаясь как?либо интерпретировать тело функции.

            Переписыванием на Python или на другой язык можно оптимизировать практически любое дополнение, функциональность которого используется 50 и более раз, но если эта функциональность и так отрабатывает менее, чем за 100?мс, то зачем что?то делать, если никто не увидит разницы? Плюс время старта интерпретатора поднимает минимальное количество использований для того, чтобы оптимизация являлась оптимизацией.

            Именно поэтому на не?VimL пишут, когда

            1. Нужно запускать что?то тяжёлое.
            2. Нужно делать что?то асинхронно. Обычно является следствием первого пункта. Недавно добавленные каналы тут могут помочь в ряде случаев остаться на чистом VimL.
            3. Нужно использовать сторонние библиотеки.
            4. Нужно написать дополнение для более, чем одного редактора.

            В списке отсутствует «не хочется тратить время на изучение VimL», поскольку тратить его придётся при использовании любого языка. В текущем состоянии даже переход на Neovim с его API не поможет.


            1. Delphinum
              05.03.2016 19:25

              Lazy loading работает вне зависимости от того, на чём написано дополнение

              Я и не утверждал обратное.

              но если эта функциональность и так отрабатывает менее, чем за 100?мс, то зачем что?то делать, если никто не увидит разницы?

              Верно. Потому не рекомендую все писать на питоне.


            1. uvelichitel
              05.03.2016 21:19

              Нужно делать что?то асинхронно.
              Уточните, пожалуйста. Внешний код ведь вызывается из viml скрипта, который блокирует всегда by_design. Кажется основная цель проекта neovim решить это и обеспечить неблокирующее исполнение скриптов. Что вы подразумеваете под async?


              1. ZyXI
                06.03.2016 00:21

                В VimL нет никаких способов создать новый поток и что?то сделать в нём. До недавнего времени даже нельзя было вызвать процесс и продолжить работу, не дожидаясь его завершения. В Python можно и то, и то. Так что то, что внешний код вызывается из VimL скрипта абсолютно ничего не говорит о его синхронности.


                1. ZyXI
                  06.03.2016 00:33

                  Единственное но: вы можете создать новый поток из Python и заставить его делать некоторую работу, но он обязан не вызывать собственные функции редактора. По крайней мере, не заблокировав перед этим основной поток (для чего встроенных функций нет; или не было, я не смотрел код каналов; в любом случае если и есть, то в Python binding’ах они не используются). В Neovim такой проблемы быть не должно, т.к. «внешние» запросы попадают в очередь, исполняющуюся вполне себе синхронно.

                  Это не мешает, делая «тяжёлые» вычисления в неосновном потоке сохранять результаты в недрах интерпретатора Python, откуда их будет тянуть основной поток. GIL Python’а Vim не блокирует, так что всё будет полностью параллельно, пока основной поток не воспользуется Python интерфейсом.