Я считаю VimScript крайне недружелюбным, но неожиданно мощным языком. К счастью его возможно одружелюбить, чем я и предлагаю вам заняться в этом цикле статей. Здесь мы рассмотрим некоторые решения типичных задач на VimScript с использованием объектно-ориентированной парадигмы, но не будем касаться основ этого языка. Возможно статья будет интересна так же программистам, интересующимся нестандартной реализацией ставших нам привычными решений.
Возможно некоторые из вас уже читали мои статьи о VimScript и изучали мою библиотеку vim_lib, не правда ли она удобна и проста в использовании? Не правда! Порой "запахи кода" так режут мне глаза, что я не могу его читать. Даже слабый "запашок" вызывает у меня непреодолимое желание сделать "лучше", "правильнее", "проще". К счастью это не сложно, достаточно все еще больше упростить и у меня это получилось, но сейчас не об этом. В этом цикле статей я лишь приведу шаблонные решения (паттерны если вам будет угодно) конкретных задач, а не буду изобретать новую библиотеку.
За более чем год использования моего класса Object в VimScript я убедился, что он содержит "код для галочки", от которого можно безболезненно избавится. Когда появляется "такой запах", это означает что пора все упрощать. В частности от чего можно смело отказаться при реализации объектно-ориентированной модели в VimScript:
Возможно вы уже задаетесь вопросом: "Как же реализовать объектно-ориентированную модель без классов?" — все крайне просто. Для этого нам нужна одна функция на каждый тип объектов, которая называется конструктором. Эта функция должна создавать и возвращать нам инициализированный объект с нужной структурой. Напоминает JavaScript, не так ли? Вот как это выглядит в готовом виде:
Четыре строчки кода для реализации целого класса. Это решение сводится к инициализации нового словаря и расширению (с помощью функции extend) его методами прототипа.
Далее рассмотрим реализацию наследования с переопределением конструктора и одного из методов родительского класса:
Всего то конструктор дополняется еще одним вызовом функции extend, что позволяет расширить базовый словарь сначала объектом родительского класса, а затем методами прототипа (дочернего класса). В свою очередь вызов родительского метода из переопределяющего так же довольно просто реализуется с помощью функции call (аналог apply в JavaScript).
Дальнейшее наследование реализуется без добавления новых вызовов extend:
Внимательный читатель уже догадался, что здесь реализовано множественное наследование, что позволяет использовать миксины:
Полиморфизм очень важная часть объектно-ориентированной парадигмы, и я не мог обойти ее стороной, тем более у меня имеется несколько плагинов, для которых она необходима. Чтобы сделать ее реальностью необходим метод instanceof, позволяющий оценить семантику класса. Все что от него требуется, это проверить, присутствуют ли в объекте методы, объявленные в целевом классе и если да, то можно считать его экземпляром данного класса. Почему именно методы, а не свойства? Потому что мы условились работать с объектами через методы. Это так называемая "Утиная типизация".
Имея такую прекрасную функцию, не сложно реализовать интерфейсы, определяющие семантику будущих классов:
Важно помнить, что объекты расширяются классом интерфейса, а не его экземпляром. Впрочем, как в любом другом языке.
Для кого то это будет открытием, но JSON — это двоюродный брат 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
Пока все
Надеюсь эта статья вас заинтересует и побудит попробовать этот замечательный редактор. Если так, то я постараюсь сделать вашу жизнь проще в следующих статьях.
myxo
Вы наверняка объяснили это в предыдущих статьях, но я, быстренько пробежавшись, не нашел ответа. Насколько я помню в vim можно писать плагины на питоне. Зачем тогда vimscript, дело в скорости?
ZyXI
Если дело в скорости, то писать нужно на чём угодно, кроме vimscript.
Дело в том, что Vim можно скомпилировать так, что в нём не будет поддержки Python. Но нельзя так, чтобы в нём не было поддержки vimscript (хотя можно так, что vimscript там будет, но 99?% дополнений работать не будут).
Delphinum
Если хорошо постараться, под Vim можно писать на любом языке, но я так не люблю зависимости ((
uvelichitel
Скрипты vim всегда пишутся на vimscript, а внутри vimscript можно вызывать python командой :py
И все же, как со скоростью? Вызов внешнего интерпретатора дает выигрыш? Есть объективные оценки? Что показывает ваш личный опыт?
Delphinum
Совершенно верно.
Объективных оценок не делал, но опыт есть.
Если подойти к делу с умом, воспользоваться потоками при парсинге проекта и подобных, тяжелых операциях, то выигрышь явный. Проблема в том, что далеко не везде можно этим воспользоваться, так как на скорость сильно влияют все подключаемые плагины редактора, а не только один конкретный, и часто эти плагины настолько просты, что с помощью того же питона не сильно то оптимизировать получится. Возьмем к примеру плагин для вывода в качестве стартового меню списка последних рабочих проектов. Все что нужно этому плагину для работы, это один файл с этим списком, который будет считываться при старте редактора и отображаться. Что тут оптимизируешь? А вот когда подобных плагинов появляется десяток, то система начинает заметно тормозить на старте.
Другая проблема это инициализация плагинов при старте редактора. Ведь чтоб они заработали интерпретатору нужно распарсить их скрипты и выполнить их. Не думаю, что использование стороннего языка сколько нибудь облегчит эту операцию. Я уповаю здесь на lazy load и активно его применяю. Благо в Vim есть механизм autoload кода, позволяющий подгружать код при его первом использовании, это сильно облегчает старт редактора. Именно поэтому я основную логику плагинов выношу в autoload. Эта модель еще не полностью мной реализована, есть над чем работать, но в голове уже решение имеется.
Резюмировав вышесказанное: от сторонних языков есть толк при реализации сложной логики, которая не может быть разделена на мелкие составляющие и выполняться при первом использовании (типа парсинга всех файлов проекта, поиска в проекте и любых тяжелых, но атомарных операциях). Во всех остальных случаях VimScript себя показывает хорошо, если правильно разложить файлы плагинов и максимально использовать lazy load.
ZyXI
Lazy loading работает вне зависимости от того, на чём написано дополнение. На парсинг использование не?VimL влияет сильно: в Vim нет парсера, код исполняется напрямую. В ряде случаев он исполняется с флагом, говорящим, что делать ничего не нужно (к примеру, в случае с
if
, когда условие не выполняется), но в любом случае ни AST, ни байткода просто нет. Соответственно, с одной стороны, нет кэшей вроде *.pyc даже внутри самого Vim и исполняться всё будет намного дольше, с другой стороны добавление вещей вроде AST и байткода замедлит выполнение всего, что выполняется один раз, т.к. придётся тратить ресурсы на различные преобразования, при однократном исполнении излишние.Ещё конкретно
function A()…endfunction
будет обработано быстрее, чем Python обработаетdef a():…
(возможно, даже в байткоде), т.к. текущий «парсер» просто ищет строку сendfunction
, даже не пытаясь как?либо интерпретировать тело функции.Переписыванием на Python или на другой язык можно оптимизировать практически любое дополнение, функциональность которого используется 50 и более раз, но если эта функциональность и так отрабатывает менее, чем за 100?мс, то зачем что?то делать, если никто не увидит разницы? Плюс время старта интерпретатора поднимает минимальное количество использований для того, чтобы оптимизация являлась оптимизацией.
Именно поэтому на не?VimL пишут, когда
В списке отсутствует «не хочется тратить время на изучение VimL», поскольку тратить его придётся при использовании любого языка. В текущем состоянии даже переход на Neovim с его API не поможет.
Delphinum
Я и не утверждал обратное.
Верно. Потому не рекомендую все писать на питоне.
uvelichitel
ZyXI
В VimL нет никаких способов создать новый поток и что?то сделать в нём. До недавнего времени даже нельзя было вызвать процесс и продолжить работу, не дожидаясь его завершения. В Python можно и то, и то. Так что то, что внешний код вызывается из VimL скрипта абсолютно ничего не говорит о его синхронности.
ZyXI
Единственное но: вы можете создать новый поток из Python и заставить его делать некоторую работу, но он обязан не вызывать собственные функции редактора. По крайней мере, не заблокировав перед этим основной поток (для чего встроенных функций нет; или не было, я не смотрел код каналов; в любом случае если и есть, то в Python binding’ах они не используются). В Neovim такой проблемы быть не должно, т.к. «внешние» запросы попадают в очередь, исполняющуюся вполне себе синхронно.
Это не мешает, делая «тяжёлые» вычисления в неосновном потоке сохранять результаты в недрах интерпретатора Python, откуда их будет тянуть основной поток. GIL Python’а Vim не блокирует, так что всё будет полностью параллельно, пока основной поток не воспользуется Python интерфейсом.