Как стать автором
Обновить
171.79
Рейтинг
JetBrains
Делаем эффективные инструменты для разработчиков

Code Completion. Часть 1: сценарии и требования

Блог компании JetBrains Программирование *

Знаете, сколько всего успевает сделать IDE, чтобы показать окно сode сompletion, когда вы начинаете набирать новое слово? Под капотом формулы, полученные с помощью машинного обучения.

Сортировка подсказок — одно из первых применений машинного обучения в IDE JetBrains. Думаем, вам будет интересно узнать, через какие тернии нам пришлось пройти, чтобы эту сортировку реализовать. Данная статья — первая из цикла, в рамках которого мы расскажем, зачем окну с подсказками вообще нужно машинное обучение и как мы решали некоторые связанные с этим задачи:

  • Сбор датасета

  • Защита данных

  • Обучение модели

  • Оценка качества (включая A/B-тесты для десктопных приложений)

  • И много чего еще

Вы даже не представляете, сколько всего надо сделать, чтобы показать «простое» окошко типа такого:

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

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

Чего разработчик хочет от code completion?

На этот вопрос есть два ответа. Один — сложный и правильный, другой — простой и тоже правильный. Но одного простого недостаточно.

Простой ответ: меньше печатать

Сравнивая системы подсказок, обычно считают количество нажатий, необходимое для набора одной и той же программы. Пример сценария, в котором эта метрика хороша: разработчики используют длинныеМнемоничныеИдентификаторы и хотят вводить их быстро и без опечаток. Хорошие подсказки позволяют делать идентификаторы все более длинными и мнемоничными. Программист сам создает длинные имена для сущностей в своем коде и сам же экономит на их наборе с помощью подсказок.

В реальности все заметно сложнее.

Это косвенно подтверждается статьей В. Хеллендоорна и соавторов. В числе прочего они исследовали, в каких ситуациях программисты пользуются подсказками, а в каких — нет (продолжают набор текста или просто закрывают всплывающее окно).

Оказывается, пользователи очень редко выбирают из подсказок ключевые слова языка и названия встроенных типов. Самый частый сценарий использования подсказок — вызов метода (чаще всего API самого проекта), на втором месте — доступ к полям классов. Имена локальных переменных выбирают тоже редко: видимо, они, все же, оказываются недостаточно длинными и мнемоничными.

Наши наблюдения плюс-минус совпадают с выводами авторов статьи, несмотря на то, что они изучали только код на C#, а мы рассмотрели много языков.

В самом деле, кому нужна подсказка, чтобы написать if? Помните Pascal? Представляете, сколько нажатий можно было бы автоматизировать на integer и begin? Даже жалко, что на нем уже почти никто не программирует.

Вывод типов в современных языках программирования работает хорошо, и без явного объявления удается обойтись даже в статически типизируемых языках (пример: var-декларации в Java). В Python с его «утиной» типизацией слова вроде int и float вообще пишешь, только если надо сделать явное приведение. И то каждый раз кажется, что просто плохо программируешь, потому и пришлось их написать.

Ключевые слова языка и встроенные типы подсказывать легко, но у комплишена есть и еще одна задача.

Сложный ответ: снизить когнитивную нагрузку

Какой процент рабочего времени вы пишете код? Готов поспорить, что чтение и отладка кода занимают гораздо больше времени, чем его написание. И, даже когда вы пишете код, основные задержки связаны не с тем, что вы плохо попадаете пальцами по кнопкам. Меньшее количество нажатий на клавиши не гарантирует высокую скорость разработки.

Нам хотелось бы помочь вам быстрее и проще думать, а не только печатать.

Думать разработчику тем быстрее и проще, чем меньше ему приходится помнить. Что именно помнить? В соответствии с упомянутой выше статьей В. Хеллендоорна, более половины выбираемых подсказок — внутрипроектные API. Библиотечные вызовы тоже популярны (как стандартные, так и внешние библиотеки), но внутрипроектное API, все же, используется намного чаще.

Мы считаем, что сделали свою работу хорошо, если вам не пришлось лезть в код коллеги или в документацию (которой зачастую нет), чтобы вспомнить, как называется нужный вам метод — isSetDropAllItemsOnExit() или shouldDropEverythingWhenLeaving()? Мы даем быстрый ответ, а вы экономите время и силы.

Также мы вызываем подсказки, когда  не помним API. Предположим, вам нужно посмотреть, какие вообще методы есть у объекта. Давайте пофантазируем и разовьем этот сценарий. Представьте, что можно вызвать подсказку в начале новой строки, вообще ничего не набирая. Если мы находимся внутри класса, с абстрактным предком, в меню появятся имена еще не реализованных методов этого предка. И не надо ничего запоминать.

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

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

А как сгенерировать такую подсказку, которая поможет?

Требования к окну подсказок

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

  1. Статический анализ — для определения всех доступных символов.

  2. Сортировка списка, полученного в предыдущем пункте.

Последующие статьи этого цикла будут посвящены сортировке. А сейчас давайте обсудим поиск символов-кандидатов. Чего мы от них ждем в первую очередь?

Корректность

Любой разработчик скажет вам, что подсказки прежде всего должны «иметь смысл». Вроде бы это означает, что они должны быть «корректными»: нужно, чтобы они учитывали, что типы в левой и правой частях выражения должны совпадать, ключевые слова должны стоять в правильных местах в соответствии с синтаксисом языка и так далее.

Вы хотите, чтобы ваш код компилировался? Ну а как же! Мы тоже хотим, чтобы ваш код работал. Но оказывается, что требование к корректности сильно переоценено. Когда разработчик начинает писать код, а не рассуждать о нем в теории, приоритеты резко меняются. Вот пара примеров.

Пример 1: приватный метод суперкласса

Допустим, у нас есть такой код:

public class Parent {
    private void doSomething() {...}
}

public class Child extends Parent {
    public void enhanceSomething() {
        doS| ⇐ курсор тут
    }
}

Надо ли нам показать doSomething() в подсказках? Это ведь приватный метод суперкласса, и его не видно с того места, где стоит курсор. Однако показывать его надо, потому как именно этого хочет пользователь. Очень часто разработчику нужно сначала вставить вызов метода, а уж потом отрефакторить суперкласс, сделав этот метод protected или public.

А ведь название может быть длинным, вроде doSomethingSpecialAndBeProudAboutIt(). Пользователь явно разозлится, если мы заставим его набирать это по буквам.

Пример 2: не то ключевое слово

Еще один сценарий, свойственный только Java, — использование ключевых слов extends и implements. Наследуя класс, мы пишем extends, а реализуя интерфейс — implements.

Но разработчики ведь не всегда помнят, какого типа те сущности, с которыми они имеют дело. Ваш ActionRepeater — интерфейс или абстрактный класс? Мой SmartActionRepeater наследует его (extends) или реализует (implements)? Не хотелось бы это запоминать и тем более сверяться с документацией. А хотелось бы так: написать все наугад, имя предка выбрать из подсказки, которая (черт побери!) должна его содержать, а ключевое слово, если его не удалось отгадать, поправить уже потом, ориентируясь на красную подсветку.

Бесполезный, но забавный факт: ошибки в использовании extends и implements не симметричны. Если разработчик пишет implements — это признак уверенности. Если же автор кода не помнит, класс там или интерфейс, то, скорее всего, напишет extends. У нас сохранилось множество старых запросов от пользователей, где они предлагают после extends показывать в подсказках имена интерфейсов. Аналогичные пожелания по классам и implements встречается гораздо реже.

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

Таким образом, в подсказках должны появляться элементы, которые формально ведут к некорректному коду, но при этом все равно «имеют смысл». Дело в том, что корректные подсказки вычислить нетрудно: у нас есть синтаксическое дерево для каждого файла в проекте, мы умеем обогащать его семантической информацией, и анализ областей видимости в таких условиях относительно прост. А вот «некорректные, но все равно имеющие смысл» варианты найти гораздо сложнее. Для этого надо обработать гораздо больше данных. Например, приходится учитывать все стандартные библиотеки языка, а не только то, что пользователь явно импортировал.

Это приводит к новому классу проблем, и думаю, вы уже догадались, к какому.

Производительность

Окно с подсказками должно появляться быстро. Разработчик не будет ждать его по несколько секунд. Терпение пользователя измеряется миллисекундами. Экспертные оценки максимального приемлемого количества миллисекунд выглядят примерно так:

В идеальном мире мы должны получить список всех разумных вариантов, отсортировать его и отдать пользователю за 150 миллисекунд. В реальности же этого трудно достичь, поскольку для каждого класса подсказок работает свой алгоритм и нам нужно собрать их ответы вместе. Обычно есть отдельные алгоритмы (мы называем их контрибьюторами) для ключевых слов, переменных, имен классов, полей классов и вызовов методов — для разных языков и IDE они могут быть разными. Некоторые из этих контрибьюторов могут иногда тормозить. В худшем случае подсказки не появятся не только за 300 мс (граница «приемлемого)», но и за секунду (хуже, чем «ужасно»).

Чтобы с этим бороться, после того, как отработал первый контрибьютор, мы засекаем еще 100 мс — по истечении этого времени мы показываем окно со всеми полученными к этому моменту результатами. Остальные контрибьюторы тем временем продолжают асинхронно поставлять подсказки.

Что делать с хорошими кандидатами, которые пришли после таймаута? Что делать, если лучший кандидат пришел после таймаута? Мы, конечно, стараемся писать контрибьюторы так, чтобы они в первую очередь возвращали самые релевантные варианты, но очевидно, что так бывает не всегда.

Выбрасывать хорошие результаты жалко, поэтому мы осторожно редактируем окно с подсказками уже после того, как показали его. Это, в свою очередь, может повлиять на удобство использования. Представьте, что вы выделили и приготовились выбрать подсказку, как вдруг она исчезает буквально из-под пальцев и заменяется на другую — релевантную на наш взгляд, но пришедшую с опозданием. Такие подмены могут приводить к ошибкам в коде, если вы их не замечаете, а если замечаете — то здорово раздражать.

Именно поэтому уже около десяти лет мы подменяем элементы только в нижней части окна. Даже если мы уверены, что подсказка хорошая, мы все равно добавим ее на несколько строк ниже текущей выбранной позиции. Верхние позиции «замораживаются» в момент появления окна.

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

Сортировка кандидатов

Допустим, нам повезло: все контрибьюторы отработали вовремя, и у нас на руках есть 820 подсказок. Какие из них показать? Это приводит нас ко второй большой задаче в реализации комплишена — сортировке.

А прямо сейчас

Нам очень интересен ваш опыт использования комплишенов на основе машинного обучения. Интересные ошибки, нестандартные требования — все, с чем вы сталкивались и чем хотели бы поделиться. Мы будем признательны за любую обратную связь.

Уже сейчас подсказки на основе машинного обучения включены по умолчанию для Java, Python, Ruby, JavaScript, TypeScript и Scala. Для других языков пока работают эвристики. Но модную высокотехнологичную версию можно опробовать, поставив соответствующую галочку в настройках:

Не забудьте рассказать нам о своем опыте. А мы будем обрабатывать вашу обратную связь и продолжать улучшать подсказки.

Теги: idecode completionmachine learning
Хабы: Блог компании JetBrains Программирование
Всего голосов 23: ↑23 и ↓0 +23
Комментарии 17
Комментарии Комментарии 17

Похожие публикации

Лучшие публикации за сутки