Заменят ли роботы программистов?
С каждым годом выходит всё больше инструментов, которые помогают автоматизировать часть рутинной работы программиста, — генераторы тестов, автодополнение кода, генераторы шаблонного кода. Мы воспринимаем как само собой разумеющееся, что условная IntelliJ IDEA предлагает нам после введения буквы метод, который мы и хотели.
А так ли нужен программист компаниям, если роботы становятся всё умнее и, казалось бы, вот-вот отправят разработчиков распахивать поля? Ведь компаниям важна прибыль, а дешёвый робот будет гораздо привлекательнее разработчиков, требующих огромные зарплаты.
Исследование этой проблемы провёл Тагир Валеев lany из JetBrains в докладе на Joker 2020. И результаты вышли весьма неоднозначными. Подробности — под катом, повествование далее будет от лица спикера.
Оглавление
Вступление
Перед вами фрагмент исходников одной из инспекций IntelliJ IDEA — IDE, глядя на ваш код, решает, нужно ли подсветить определённую конструкцию и предложить упростить её:
Если посмотреть на код повнимательнее, то можно увидеть, что в нём очень часто используется if. По-моему, вся суть программирования состоит в условных переходах. Не было бы их, не было бы ничего. Даже когда вы складываете 2 + 2, внутри процессора происходит условный переход, когда решается, надо ли перенести бит в соседнюю ячейку памяти. По сути, транзистор — базовый элемент интегральных схем — это маленький примитивный if-else, который в зависимости от управляющего тока открывает или закрывает эмиттер. Миром правят условные переходы.
Позвольте представиться — я Тагир Валеев, senior if-else developer в JetBrains. Я достаточно хорошо пишу if, умею вкладывать их один в другой, аккуратно менять местами, группировать и даже дописывать ветку else при необходимости. За это меня терпят и даже платят зарплату.
Я всем доволен, но в последнее время появляются некоторые «звоночки», которые начинают беспокоить. Люди всё больше говорят, что роботы тоже могут писать свои if не хуже, чем живые программисты. Но при этом роботы не требуют повышения зарплат, не сидят в рабочее время в Твиттере и не ноют про выгорание. Сплошные плюсы. Не пора ли осваивать свиноводство или другую полезную профессию, чтобы не умереть в нищете?
Кажется, не меня одного волнуют такие вопросы. К примеру, в 2020 году JetBrains проводила исследование экосистемы разработки, в котором приняли участие аж 20 тысяч разработчиков. 50% из них допускают, что искусственный интеллект заменит программистов в будущем, а 4% опрошенных уверены в этом. В России люди настроены ещё оптимистичнее: 62% допускают, а 8% уверены.
Конечно, неизвестно, когда это будущее наступит, и можно уповать на то, что на наш век хватит, а свиноводством придётся заниматься нашим детям. Тем не менее стоит посмотреть, куда дует ветер, уже сегодня.
Чтобы понять текущее состояние дела, представить, что нас ждёт в будущем, давайте посмотрим, чем вообще занимаются программисты и как роботы нас пытаются заменить.
Деятельность программистов довольно разнообразна, и роботы пока не универсальны. Обычно они автоматизируют только один аспект деятельности.
Давайте сперва поговорим про написание кода. Основной помощник в написании кода — это автодополнение (code completion).
Code completion
Насколько мне известно, широкая публика увидела эту фичу в Microsoft Visual Studio 6.0 в конце прошлого века, хотя исследования и экспериментальные решения были и раньше.
Сейчас это выглядит устаревшим, но тогда это был довольно прорывной шаг. Я даже нашёл обзор тех лет, который вы можете посмотреть и представить, насколько это выглядело революционно и какие восторги вызывало. Оно показывает имена методов.
Мало того, оно показывает только те методы, которые есть в объекте. Более того, оно не только показывает, но и само вставляет их в код, и можно не печатать имя метода. И главное, оно работает не только со стандартной библиотекой, но ещё и с вашим кодом. Вы сделали объект, написали методы, и оно это поняло. Заметьте, в статье сплошной восторг и ни слова про то, что роботы могут отнять у нас хлеб.
С тех пор автодополнение кода шагнуло далеко вперёд. Сегодня автодополнение активируется автоматически и даже без горячей клавиши. Появилось много разных вариантов дополнения. Например, в документации к IntelliJ IDEA можно встретить целых 5 терминов на эту тему.
Чтобы оценить, как далеко шагнуло автодополнение, я написал маленькую стандартную программу, которая считает частоты встречаемости слов во входном файле и выводит 20 самых популярных вместе с количеством:
import java.io.IOException;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class WordCount {
public static void main(String[] args) throws IOException {
if (args.length != 1) {
System.err.println("File name required");
return;
}
Path path = Paths.get(args[0]);
Map counts = Files.lines(path)
.flatMap(Pattern.compile("\\W+")::splitAsStream)
.map(String::trim)
.filter(Predicate.not(String::isEmpty))
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
counts.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(20)
.map(e -> e.getKey() + "\t" + e.getValue())
.forEach(System.out::println);
}
}
Здесь 873 символа и 26 переводов строк. Далее я захотел проанализировать, как же я набираю такую программу в IDEA. Сперва я думал поставить кейлоггер, чтобы записать всё, что я нажимаю, а затем решил: «Зачем мне кейлоггер, если у меня есть IntelliJ IDEA со встроенным кейлоггером?» Я просто включил режим записи макроса, набрал текст этой программы (неидеально: я иногда ошибался, что-то стирал), сохранил этот макрос и получил XML-файл где-то в недрах папки IDEA. Затем я расковырял сохранённый макрос и посмотрел, что реально было набрано мной, а что — машиной. Результат был такой.
Весь серый код набран роботом, а код других цветов — мной. Импорт я вообще не набирал. Когда я стал писать main, я только нажал букву m, и мне сразу сказали «ты хочешь main». Потом я написал if и букву a, и автодополнение мне сразу предложило args, и так далее. Видно, что робот потрудился больше меня.
Если говорить в цифрах, то я набрал 214 символов, включая все опечатки. Также я использовал 128 специальных клавиш. Например, если мне выпадал список completion, я иногда нажимал стрелку вниз или Enter, чтобы выбрать нужный вариант. Можно по-разному считать, но если сложить все нажатые мной кнопки и поделить на размер результирующего файла, то оказывается, что я сделал только 38% работы, а робот сделал 62%. Если бы я это писал за деньги, то заплатили бы мне, а не компьютеру. Так что я доволен, ведь роботы забрали у меня часть работы, но не зарплату.
Эволюция автодополнения в чём-то похожа на развитие поисковых систем в Интернете. Изначально показать хоть какие-то результаты поиска было уже успехом, но со временем результатов стало слишком много, и появилась задача их ранжирования.
Например, я в случайном месте исходников IntelliJ IDEA написал букву e.
Мы видим, что здесь очень много результатов:
- ключевое слово else (я стою после if, поэтому, возможно, хотел else);
- локальные переменные из текущего метода, которые начинаются с e;
- методы текущего класса, которые начинаются с этой буквы;
- метод equals () из внешнего класса;
- методы, в названии которых какое-либо слово, кроме первого, начинается с e;
- методы, где e встречается на конце, потому что люди иногда вводят суффикс;
- методы суперкласса;
- методы внешнего класса;
- deprecated-методы;
- поля и методы, где эта буква встречается вообще в произвольной позиции.
Как мы видим, есть разные критерии, по которым их можно отсортировать:
- степень совпадения;
- тип элемента;
- близость в его объявлениях точки использования;
- соответствие результирующего типа контексту;
- модификаторы;
- количество параметров метода;
- локальная статистика (если пользователь набирал метод часто, то его стоит поднимать).
Что делать, когда критериев много? Что поставить наверх? Метод, который идеально подходит по типу, или же метод, который по типу не подходит, но у него идеально совпадает префикс?
Оказывается, критерии сортировки тоже надо сортировать. Для Java эту работу аккуратно вели вручную почти 20 лет, пока вообще существует completion в IntelliJ IDEA. Причем это хрупкое равновесие, потому что если поменять логику в одном месте, чтобы улучшить приоритеты в каком-то конкретном случае, мы, вполне вероятно, ухудшим приоритеты в другом случае. И что бы мы ни сделали, всегда найдутся недовольные пользователи. Для других языков (вроде Python) IDE появилась позже, усилий на качество сортировки было потрачено меньше, и поэтому там недовольных больше.
Оказалось, что в этой области можно применить модный нынче data science (но сразу оговорюсь, что в data science, deep learning и ИИ я полный нуб и ничего не соображаю). Интересно, что здесь используется автоматизация программирования, то есть data science, для того чтобы автоматизировать программирование, то есть автодополнение. Сами фичи и критерии сортировки мы формируем всё ещё вручную, но для окончательной сортировки вариантов строим модель, которую тренируем на EAP-пользователях.
Как вы знаете, EAP-версии (Early Access Program) мы раздаём бесплатно, но кое-что требуем взамен. В частности, мы собираем анонимизированные данные о том, какие варианты автодополнения и с какими критериями пользователи выбирают чаще, а какие — реже. Никто не записывает точное название метода, который вы выбрали. Например, если вы предпочли статический метод нестатическому в каком-то контексте, то это будет записано.
На основании этих данных строится модель. Сейчас в IDEA используется random forest, но исследуются и другие подходы. На основании модели компилируется Java-код, и он же используется для ранжирования.
Надо убедиться, что ML-сортировка работает лучше обычной. Для офлайн-тестирования мы берём реальные проекты, в них ставим курсор на случайное слово, потом удаляем всё, кроме, например, первой буквы, и пытаемся дополнить. Понятно, что к такому тестированию много вопросов, но это позволяет убрать откровенные провалы и убедиться, что мы совсем всё не разломали.
Если здесь всё прошло нормально, то мы начинаем снова мучить EAP-пользователей, разделяя их на группы. Для одной группы мы включаем модель, а для другой группы оставляем традиционный completion, и смотрим, насколько успешнее какая из групп дополняет.
Есть много метрик, например:
- оказался ли искомый вариант первым в списке;
- попал ли вариант в топ-5 или топ-10;
- на какой средней позиции он оказался в списке;
- сколько символов пользователь набрал перед тем, как он выбрал нужный вариант (в идеале он должен набирать один символ или даже ничего не набирать);
- воспользовался ли пользователь completion или же он ввёл всё вручную.
Эти метрики показывают, что улучшения от ML есть не только для Python, но и для Java, хотя над Java работали вручную почти 20 лет. В PyCharm такое автозаполнение уже включено по умолчанию, в IntelliJ IDEA для Java в ближайшем релизе будет включено по умолчанию. Так мы все увидим, как работает машинное обучение.
Вот оно, светлое будущее! Оказывается, можно вообще не писать код, который будет заниматься сортировкой опций completion, а можно поручить это дело data science.
Меня в этом всём кое-что смущает. Вы же помните, что главное в программировании — это if-else? Именно это под капотом у так называемого ИИ. Это фрагмент, сгенерированный моделью completion на Java.
package com.jetbrains.completion.ranker.model.java;
/*
* WARNING: This file is a generated one, please do not edit it.
*/
public class Tree0 {
public static double calcTree(double[] fs) {
if (fs[54] <= 0.5) {
if (fs[59] <= 0.9736842215061188) {
if (fs[88] <= 0.5) {
if (fs[68] <= 25.0) {
if (fs[30] <= 0.41428571939468384) {
if (fs[38] <= 0.0005793680320493877) {
if (fs[84] <= 9953.5) {
if (fs[55] <= -903.5) {
if (fs[36] <= 0.00043574847222771496) {
if (fs[15] <= 0.5) {
if (fs[49] <= 0.5) {
if (fs[31] <= 0.31414473056793213) {
return 0.3141634980988593;
} else {
return 0.538899430740038;
}
} else {
Когда вы вызываете автодополнение в IntelliJ IDEA со включенной ML-сортировкой, по факту выполняется этот код. Эти 38 строчек — только начало, весь файл занимает 8923 строчки — мы просто упёрлись в размер class file, constant pool и так далее. Этих class file в модели около 60, то есть это более 100 000 строк. И меня это немного беспокоит.
Во-первых, меня беспокоит, что if пишут роботы, а не я сам. Причём робот пишет if гораздо быстрее, чем я. А значит, я могу остаться без работы. Представьте, придёт пользователь и скажет: «А почему вы этот пункт completion поставили выше, а тот ниже? Ведь тот, который ниже, главнее?». Мы смотрим на это и думаем, что пользователь, в принципе, прав, и хорошо бы починить этот баг. И это пугает даже сильнее безработицы. Что мы ему можем ответить, потому что отлаживать и редактировать это совершенно невозможно?
Любое изменение должно идти не привычным путём (найти какой-то if и поправить), а иным.
И здесь нет места обычному программированию, где мы пишем старые добрые if. И вот это меня беспокоит.
Whole line completion
Одно из модных сейчас направлений автодополнения — whole line completion. Даже с идеальным ранжированием обычный completion позволяет немного дополнить код. А давайте мы за пользователя мы будем писать приличный кусок программы с вызовом методов, аргументами и так далее.
Одно из доступных решений в продакшне — Codota. Можно установить плагин в IntelliJ IDEA, и он начинает предлагать интересные варианты, основываясь на имеющихся переменных и своём понимании.
Как я понимаю, его модели обучаются на опенсорс-репозиториях и смотрят, что вообще люди пишут. Например, здесь плагин решил, что если мы хотим создать новый файл, то, возможно, мы хотим сделать его во временном каталоге, и за нас написал код.
С Codota вышла забавная и поучительная история. Помимо обычного дополнения, у них есть шаблоны: когда вы пишете название шаблона, то плагин вставляет какой-то boilerplate.
Вставим этот шаблон.
А почему SS выделено жёлтым? Недавно мы написали новую инспекцию для Java, которая разбирает эти строки форматирования и смотрит, нет ли чего странного. И раз встретились часы, минуты и миллисекунды рядом, а секунд нет, то это подозрительно выглядит, и мы предлагаем это исправить.
Дело в том, что Codota действительно вставил неправильный код, и никто этого не знал. Я сообщил им про этот баг, они его пофиксили, но мне эта история показалась очень интересной, ведь фактически подрались два робота. Программист говорит: «Я хочу отформатировать дату». Codota: «Я умею это делать, предоставь это мне, я сейчас за тебя всё напишу». И тут выскакивает второй робот: «Ты же неправ, так нельзя программировать, вот как надо!».
Многие уже видели чаты, где двух ботов стравливают друг с другом и смотрят, что получится. Кажется, что-то подобное мы начинаем наблюдать в IDE. Можем ли мы прийти к ситуации, когда вокруг нас будет ещё больше роботов? Разные инструменты, плагины. Каждый со своим ИИ и с пониманием, как писать код. Каждый будет давать вам свои советы. Начнут ли они ругаться друг с другом? И в чём тогда роль человека-программиста? Будет ли он больше разнимать ругающихся роботов, чем писать программы? Кто знает.
У Codota есть ещё один проект по автодополнению — Tabnine. И кажется, что он ещё более искусственный интеллект. Это независимый движок, он развивался отдельно, но затем Codota его купила. Иногда он действительно предлагает в топе тот вариант, который нам нужен.
Например, здесь я хотел написать String: trim, и он вылез третьим пунктом, хотя completion от IDEA предложил этот вариант где-то внизу.
Но бывает и полная ахинея. Например, тут я хотел избавиться от пустых строк, и мне сразу предлагают Predicate.isNotBlank. Всё звучит как надо, плагин его вставляет, и тут Tabnine умывает руки, потому что никто не знает, что это за предикат и из какого класса.
IDEA дальше предлагает импортировать его, но на самом деле ни в одном из этих классов нет подходящего метода isNotBlank. То есть Tabnine плевать, что это за предикат и откуда он взялся. Плагин просто видел его в каких-то опенсорс-проектах.
Также он предлагает какие-то куски выражений. Например, сама IDEA никогда не опустилась бы до того, чтобы предложить вставить вызов метода и не указать круглые скобки, тем более что у него нет аргументов.
Вот ещё пример. Я только начал писать тип List, и он сразу предлагает new ArrayList и даже имя переменной придумал, хотя я хотел другое, но это неважно. Важно то, что он либо оставит меня без круглых скобок, если я выберу первый вариант, либо я могу выбрать последний вариант, но тогда Tabnine напишет тип справа, а у меня уже давно не Java 6.
Такое ощущение, что энергии у Tabnine много, он видел много исходного кода, и поэтому постоянно хочет что-то посоветовать. Но конкретно язык Java он не знает. А то, что мы вручную делали в IDEA с completion, очень хорошо знает язык.
Главное после Tabnine — никогда не знаешь, в каком ты месте окажешься после предложения, и что конкретно придётся дописать вручную. Может быть, я рано сдался, и надо было ему дать обучиться конкретно на моих «сорцах», но мне показалось, что с ним одно мучение. Здесь методы машинного обучения уступают completion, написанному вручную с любовью и заботой. Может, в других языках Tabnine более полезен, если вы очень медленно сами печатаете.
В общем, пока я не замечаю, чтобы code completion сам за вас написал всю программу. Программисту поработать всё равно придётся. А главное, эти технологии обычно экономят моторные действия, но не так уж экономят мысленную энергию. Вам всё равно придётся придумывать вашу программу самим. Так что пока рабочее место роботам не оставляем.
Code generation
Как насчёт автоматической генерации исходного кода? В целом, она близка к автозаполнению, но люди как-то разделяют эти понятия.
public class Person {
String firstName, lastName;
int age;
}
Мы начали писать класс мутабельного человечка, при этом нам completion почти не помогал. Что нужно дальше? Геттеры, сеттеры, конструкторы, equals (), hashcode (), toString () — скука смертная. Вы говорите компьютеру: «Я не хочу это писать, мне лень». Компьютер с радостью делает эту работу.
import java.util.Objects;
public class Person {
String firstName, lastName;
int age;
public Person(String firstName, String lastName, int age) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return age == person.age &&
Objects.equals(firstName, person.firstName) &&
Objects.equals(lastName, person.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName, age);
}
@Override
public String toString() {
return "Person{" +
"firstName='" + firstName + '\'' +
", lastName='" + lastName + '\'' +
", age=" + age +
'}';
}
}
Любая IDE это сделает, не только IntelliJ IDEA. Было 4 строки, а стало гораздо больше. Если вам платят за строки, то этот день кажется особенно удачным. Впрочем, часто люди не хотят не только писать всё это, а даже читать. Вот настолько программисты обленились. Пусть компьютер пишет это где-нибудь у себя, но нам вообще не говорит, не показывает, мы этого не хотим видеть.
import lombok.*;
@Data
@AllArgsConstructor
public class Person {
String firstName, lastName;
int age;
}
Конечно, так тоже можно, и вот один из вариантов. Код появляется при компиляции автоматом, в исходниках его нет, и вы можете его не читать.
data class Person(
var firstName: String,
var lastName: String,
var age: Int)
Другая альтернатива — перейти на язык, где этот код писать не нужно в принципе. И тут возникает философский вопрос: в данном случае писал ли робот код за вас или нет? Вы можете пользоваться геттерами, сеттерами, equals () и hashcode (), но язык не требует писать всё это.
Было ли здесь какое-то автоматическое программирование, сделанное за вас? Вообще термин «автоматическое программирование» — очень размытый. Он появился в 50–60-е годы XX века, и его использовали для обозначения программирования на языках высокого уровня типа Fortran, которые тогда появлялись. Мол, настоящее ручное программирование — это когда ты шпаришь в машинных кодах, и в самом крайнем случае — когда пишешь на Assembler.
А потом появились языки высокого уровня вроде Fortran, и программируем уже не мы, а транслятор. Настоящих программистов не осталось, и мы — лишь придатки к компиляторам. Мы описываем решение не в виде программы, а на каком-то высокоуровневом языке пишем алгоритмы, а сами общаться с машиной давно разучились.
Программистам от такого отношения обидно, и поэтому весь тот ужас, который мы пишем на высокоуровневых языках, называем программами. И несмотря на то что автоматическое программирование существует уже больше 60 лет, мы ещё не остались без работы.
Automatic refactoring
Предположим, что мы код тем или иным образом написали. Что же с ним надо сделать теперь? Правильно, переписать, потому что он написан плохо. Для того, чтобы переписывать код, придумали рефакторинг. Возможно, для вас будет звучать удивительно, но буквально 20 лет назад программисты рефакторили вручную.
Этому процессу посвящена монументальная книга Мартина Фаулера (Martin Fowler), Refactoring. Improving the Design of Existing Code. Некоторые говорят, что эта книга научила людей рефакторить, но главная её ценность в том, что она научила IDE рефакторить. По сути, эта книга — набор рецептов, как правильно написать IDE, чтобы она правильно рефакторила за вас. Когда книга вышла, производители инструментов для программирования бросились реализовывать эти алгоритмы и рецепты.
Ключевым моментом времени Фаулер назвал январь 2001 года, когда два инструмента преодолели рубикон рефакторинга и рефакторинг в Java получил серьёзную инструментальную поддержку. В этой заметке Мартин рассуждает, что ряд инструментов уже имел какую-то реализацию рефакторингов — очень простых вроде rename-методов. Но серьёзной заявкой на инструментальную поддержку была бы реализация Extract Method, потому что сделать реализацию этого метода — совсем не тривиальная задача. Я с удовольствием ссылаюсь на эту статью, потому что одним из двух инструментов, которые перешли Рубикон, являлась совершенно новая и малоизвестная среда разработки IntelliJ IDEA. Упоминание в статье Мартина Фаулера, конечно, стало большим успехом для JetBrains.
Сегодня автоматических рефакторингов в IDE довольно много. Есть какие-то популярные, например Rename, Change Signature, Move, Extract Variable, Inline, а есть такие, которые даже я не знаю, что делают, хотя я отвечаю за Java в IntelliJ IDEA. При этом самые популярные рефакторинги тоже прошли длинный путь от очень наивной реализации, которая часто ломалась, до очень умной, которая знает очень много частных случаев, корнер-кейсов, и они все покрыты разными if в коде.
Разберём несколько примеров с методом inline.
boolean checkValid(Data data) {
if (hasError(data.getContent())) return false;
return !hasError(data.getInheritedContent());
}
boolean hasError(List content) {
for (String str : content) {
if (isInvalidString(str)) return true;
}
return false;
}
Есть метод checkValid (), и в нём два вызова hasError (). По каким-то причинам мы решили, что метод hasError () лишний, и хорошо бы его заинлайнить. Сам метод hasError () выглядит несложно, но в нём две точки выхода, причём одна в цикле, а другая — снаружи. И в исходном методе checkValid () тоже две точки выхода.
Не так давно рефакторинг просто поднимал лапки вверх и говорил: «Не могу, не умею, слишком сложно для меня».
boolean checkValid(Data data) {
for (String str1 : data.getContent()) {
if (isInvalidString(str1)) return false;
}
for (String str : data.getInheritedContent()) {
if (isInvalidString(str)) return false;
}
return true;
}
Однако сегодня вы получите аккуратный, будто написанный вручную код, и даже непонятно, где заканчивается предыдущий заинлайненный метод и начинается следующий. Это уличная магия, и иначе вообще не скажешь.
Или вот здесь. У вас есть объект Person, вы вызвали конструктор и после этого вызываете геттер. Вы можете встать на конструктор и заинлайнить всё одним действием. Он понимает, что у вас есть объект, поля, и он всё это пропихнёт из параметров в поля, из полей перейдёт в геттеры, из геттера назад вытащит, поймёт, что возраст нам совершенно не нужен, и оставит только одно слово. Чудо же!
Что важно, здесь нет никакого новомодного ИИ и deep learning. Это всё нежно, аккуратно и заботливо написано вручную, то есть там много if, которые покрывают частные случаи. Конечно, это всё сильно помогает модифицировать уже написанный код, уменьшая количество работы, которое приходится делать вручную, а также количество ошибок, которое вы при этом сделаете.
Automatic solution finding
Часто программисту надо решать задачи, и очень часто они уже кем-нибудь решены. Казалось бы, зачем решать задачу, если её делал другой человек? Надо просто взять готовое решение. В принципе, все программисты этим пользуются: берут готовые библиотеки в зависимости от своих проектов, находят вопросы на StackOverflow, копипастят оттуда куски кода и так далее. Но всё равно это требует ручного труда, потому что нужно куда-то зайти, что-то поискать в Интернете, разобраться, что люди ответили… Могут ли роботы сами заниматься этой работой — искать готовые решения задач в Интернете?
Проверим, делится ли 15 на 3. Предположим, вы не умеете решать эту задачу или ваш язык программирования не умеет. Чтобы решить её, надо погуглить.
Сузим диапазон поиска и перейдём на сайт вопросов и ответов, где спросим «is 15 divisible by 3?».
Ответ выглядит так. Да, делится, задача решена. Нам не надо ничего самим считать — за нас уже решил другой человек, и мы ему поверим, ведь зачем делать работу во второй раз.
Один программист написал proof-of-concept решения задач FizzBuzz с использованием Automatic solution finding. Правда, написано на Python, но даже если никогда не видели Python, тут всё понятно.
if __name__ == '__main__':
divisors = [(3, 'fizz'), (5, 'buzz')]
for i in range(1, 101):
output = ''
for d in divisors:
if query_search_engine(i, d[0]):
output += d[1]
output = output or i
print(output)
Проверку делимости мы не решаем, а делегируем Интернету, ведь она уже решена.
base_url = 'https://www.answers.com/Q/'
def query_search_engine(numerator, denominator):
''' ask a search engine if these numbers are divisible '''
question = 'is %d divisible by %d' % (numerator, denominator)
question = question.replace(' ', delimiter)
url = '%s%s' % (base_url, question)
request = Request(url)
request.add_header(
'User-Agent',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:72.0) ' \
'Gecko/20100101 Firefox/72.0')
response = urlopen(request)
# politeness -- wait a few seconds before the next query
time.sleep(3)
if url != response.url:
return False
text = response.read().decode('unicode-escape')
parser = SearchParser()
parser.feed(text)
return parser.conclude_yes_no()
Здесь мы делаем такой запрос к answers.com. Парсер целиком показывать не буду, но суть здесь.
# words that indicate whether the answer is yes or no
self.positive_signs = ['yes', 'exactly', 'is divisible']
self.negative_signs = ['no', 'not', 'decimal']
…
def score_answer(self, answer):
''' evaluates an answer to determine if it's a yes or a no '''
score = 0
positive_signs = re.findall(
r'\b%s\b' % r'\b|\b'.join(self.positive_signs),
answer,
)
score += len(positive_signs)
negative_signs = re.findall(
r'\b%s\b' % r'\b|\b'.join(self.negative_signs),
answer
)
score -= len(negative_signs)
return score
Если в ответе вы встречаете слова yes, exactly, is divisible, то вы добавляете очко. Если же встречаются no, not, decimal, то отнимаете очко. Эти очки суммируются по всем ответам, и вердикт выдаётся на базе общего количества ответов. В итоге программа делегирует задачу проверки делимости вселенскому разуму вместо того, чтобы решать её самостоятельно. И это работает! Вы можете не решать эту задачу у себя, то есть программа работает с точностью до того, что умеет вселенский разум, потому что встречается и такое.
Слепо доверять людям из Интернета, конечно, не стоит. Но в целом подход интересный, ведь в мире накоплено много решений типовых задач, и вполне можно взять готовое вместо того, чтобы платить программисту за то, чтобы он решал это снова.
Automatic bug search
Видя, что ошибки всё-таки бывают, мы переходим к вопросу о том, что программы часто работают неправильно. Чтобы программа начала работать правильно, надо искать в ней ошибки и исправлять. Люди могут искать ошибки и сами, но лучше, когда это делает машина.
Конечно, любой компилятор умеет искать ошибки, которые препятствуют исполнению программы. Но часто этим проблемы не ограничиваются.
Да, я слышал, что программисты на Haskell хвастаются, что если у них компилируется, значит, работает. Я даже видел простые примеры, подтверждающие этот тезис, но я им всё равно не верю. Так или иначе в более мейнстримных языках типа Java все прекрасно знают, что можно наделать кучу ошибок, которых компилятор не заметит.
Одним из знаковых инструментов, специально сделанных не для компиляции, а для поиска ошибок, является lint. Первая его версия вышла в 1978 году, так что в плане поиска ошибок в программах роботы уже очень давно отнимают наш хлеб.
Вот пример программы на C из книги Checking C Programs with Lint Яна Дарвина.
myfunc(s)
char *s
{
void a();
int i, j;
a("hello");
}
Книга появилась в 1988 году, то есть на 10 лет позже, чем lint, но всё равно это доисторические времена. По крайней мере, в 1988 году я кушал кашу в детском саду, а не занимался проверкой задач на корректность.
Многие, глядя на листинг этой программы, даже не поймут, что здесь происходит, потому что это изначальный синтаксис языка Кернигана и Ритчи, когда типы параметров функции не писали в скобках.
При запуске lint выдаёт 3 предупреждения о неиспользуемом параметре функции и двух неиспользуемых локальных переменных.
% lint myfile.c
myfile.c:
myfile.c(2): warning: argument s unused in function myfunc
myfile.c(6): warning: i unused in function myfunc
myfile.c(6): warning: j unused in function myfunc
%
То есть уже тогда lint умел определять, что переменная объявлена, но не используется. Могло оказаться, что она осталась после рефакторинга (это не ошибка, её просто надо удалить), но бывает и реальная ошибка. Например, мы хотели использовать какую-то переменную, но по ошибке случайно использовали одну из объявленных ранее.
Любопытно, что буквально с первых страниц книги поднимается вопрос о том, как собственно с роботами бороться.
Вот простой пример. Вы печатаете сообщение в стандартный поток ошибок с помощью fprintf (), а lint на ругается на то, мы не используем возвращаемое значение.
fprintf(stderr, "%s: -f argument %s invalid\n",
progname, optarg);
lint:
function returns value which is always ignored
fprintf
В C функция fprintf () возвращает количество выведенных символов либо отрицательное число в случае ошибки. Если даже случилась ошибка, это значит, что мы не смогли в stderr () что-то написать, то есть у нас сломан stderr (). Мы ничего не можем сделать, и даже пользователю не можем сообщить об ошибке. Поэтому мы вполне считаем себя вправе проигнорировать результирующее значение функции fprintf (). Так что тут глупый робот неправ. Как мы видим, уже в 1988 году программисты видели, что роботы не всесильны, и, бывает, глупость советуют.
Как же заткнуть робота и сказать, что всё нормально? Добавляем явный каст к типу void. В C и не такая жесть допустима. Зато lint теперь заткнулся.
(void) fprintf(stderr, "%s: -f argument %s invalid\n",
progname, optarg);
В этих случаях всегда возникает вопрос:, а помогает ли статический анализатор или мешает? Ведь нам постоянно приходится бороться с предупреждениями, вставлять какие-то странные аннотации, комментарии или ещё что-то. Они загрязняют код, мешают его читать; если вы мёрджите что-то в Git или рефакторите, то эти комментарии могут случайно оторваться от нужного места и оказаться совсем не там, где надо. Получается полная ерунда, и вы тратите на это лишнее время вместо того, чтобы спокойно программировать.
Сегодня статический анализ ушёл далеко вперёд. Вместо десятков правил в самой IntelliJ IDEA, например, встроены уже тысячи. Но суть со времён lint не сильно изменилась — это поиск определённых шаблонов кода, которые считаются неправильными. И предупреждения тоже иногда приходится подавлять аннотациями или комментариями.
Конечно, не всегда это просто паттерны. Иногда за предупреждением стоит довольно сложная математическая теория, абстрактная интерпретация, widening, решётки и прочие страшные слова.
Например, на нашей конференции SnowOne я показывал такой код.
Он говорит, что это условие всегда истинно. И тут реально без бутылки не разберёшься, почему; надо крепко задуматься, а робот вам подсказывает за долю секунды. Вы пишете код, а он — хоп! — и подсветил, что ты неправильно написал.
Подобных примеров я видел в своей жизни десятки, потому что я как раз занимаюсь инспекцией, которая это делает. Обычно подобное приносит пользователь и говорит: «Ха-ха, ваш анализатор ошибся! У меня в коде нет ошибок!». Потом я внимательно смотрю на этот код, где-то полчаса разбираю все возможные варианты, доказываю, что ни в каком варианте это условие не может быть ложно, и пишу длинный комментарий пользователю, где говорю: «Нет, извини, у тебя в программе ошибка, мой анализатор всё сделал правильно».
И тут снова вс