AI-driven код
GitHub Copilot позиционирует себя как «ваш помощник по написанию кода на базе ИИ». Copilot выводит машинное обучение на совершенно новый уровень — он интегрируется с IDE по вашему выбору (ну, если конечно вы выберете что-нибудь из Jetbrains, VS Code или Neovim) и предоставляет в ваше распоряжение мощную систему AI-driven кода, обученную на миллиардах строк опенсорсных проектов на GitHub на дюжине с лишним языков.
Речь идет не о более умной аналитической версии существующего функционала автодополнения (auto-complete) ваших IDE. Copilot способен на интеллектуальное контекстно-зависимое дополнение всего: от отдельных строчек кода до целых функций как на основе кода, который вы только что написали, так и по комментариям на английском языке, описывающим то, что вы хотите получить.
Хорошо, а теперь плохая новость. Copilot пока доступен не всем. Продукт находится на этапе технического превью, и возможно вам придется встать в длинную очередь, если вы все-таки хотите получить заветное приглашение попробовать его в действии.
Апдейт от 25 июня 2022 г.: GitHub Copilot официально запущен и теперь доступен всем желающим! Цена на момент написания статьи составляет 10 долларов в месяц или 100 долларов в год.
Каким-то образом я оказался одним из тех счастливчиков, которым довелось провести последние несколько дней, тестируя это чудо. Может ли Copilot повысить мою продуктивность? Неужели через несколько лет я стану никому не нужным устаревшим программистом-человеком?
Мне не терпелось узнать узнать ответы на эти вопросы.
Простенький тест
В качестве IDE я использую PHPStorm, и это как раз один из тех продуктов, которые поддерживают интеграцию Copilot. Сам Copilot поставляется в виде на удивление маленького плагина, который добавляет запись GitHub Copilot в меню инструментов. Я логинюсь в GitHub, разрешаю ему подключиться к моему устройству, и все — после активации плагин сразу же начинает работать, анализируя все, что я печатаю, в режиме реального времени и предлагая то, как мог бы выглядеть мой дальнейший код.
Предложения отображаются в виде серого кода, который я могу принять и сразу же интегрировать в свой код по нажатию клавиши Tab. С помощью комбинаций клавиш Alt-[ и Alt-] вы можете посмотреть другие предложения, если вам не нравится первый предложенный вариант.
Я хотел сделать свои первые тесты как можно проще, поэтому я создал новый пустой проект в PHPStorm и добавил туда класс под названием Utilities
(да-да, я знаю), в котором собираюсь определить несколько статических вспомогательных методов для каких-нибудь надуманных типовых задач, которые я, возможно, захотел бы выполнить со строками и массивами.
Едва я начал писать свою первую идею для функции, как Copilot услужливо предложил дополнить за меня блок с комментарием.
Я принял предложение нажатием клавиши Tab. Следующее, что я увидел, это то, что ИИ Copilot прочитал мои комментарии о намерении сделать в классе публичный статический метод для преобразования нотации и уже готов воплотить его в коде.
Мне не нужно было написать ни единой лексемы. Copilot также написал вполне сносное тело для моей первой функции.
Но это очень простая задача для генератора кода (при условии, что он способен на не очень простую задачу интерпретации естественного языка). Теперь я хочу попробовать преобразование строки, которое будет немного посложнее.
Мне понадобилось несколько минут, чтобы обдумать все возможные варианты. Как насчет того, чтобы очистить строку с HTML от всех тегов, кроме тех, которые мы явно передаем как допустимые?
Интересно, предложит ли Copilot встроенную функцию strip_tags
, потому что я знаю, что это, по крайней мере в чистом виде, плохое рудиментарное решение для санации HTML, которое не работает для большинстве реальных юзкейсов этой задачи.
Я пишу свой комментарий, стараясь как можно точнее указать, чего я ожидаю от своей функции. Мне дается предлагаемое объявление функции с именем и параметрами, которые я немного корректирую. Copilot действительно начал с strip_tags
, но объединил его с некоторыми другими полезными преобразованиями, чтобы функция справлялась с поставленной задачей.
Тут я решил посмотреть альтернативное предложение. Мне больше понравился второй вариант, предложенный Copilot, хоть в коде и потребуется пара незначительных изменений, которые я внесу сам.
Тестирование простенького теста
Мне также любопытно, насколько хорошо этот ИИ помогает писать тесты для кода, который он только что сам помог мне написать. Я создал скелет теста для двух методов в Utilities и прописал имена тестовых функций.
На этот раз мне нужно начать печатать что-то в теле функции — автоматическое предложения полного тела теста, когда я нажимаю клавишу ввода, чтобы добавить новую строку, не появляется. Я начинаю с базового $input = 'StringToConvertToCamelCase';
Мой Copilot тут же предлагает следующую строчку:
$expected = 'string_to_convert_to_camel_case';
Я нажимаю Tab, чтобы принять. Copilot без промедления заканчивает тест вместо меня.
$actual = Utilities::camelCaseToSnakeCase($input);
$this->assertEquals($expected, $actual);
Затем я определяю ввод для функции санации HTML.
$input = '<p style="font-weight: bold" class="allowed">This is a <strong>test</strong> with some <em>disallowed tags</em>.' .
'</p><img src="http://example.com/image.jpg" alt="Image" /><p>This is also allowed.</p>';
И тут Copilot пытается предложить ожидаемы результат, но это не совсем то, что нужно. Я хочу разрешить теги p
и strong
, и атрибут class
, поэтому далее я прописываю вызов функции, не определяя ожидаемый результат.
$actual = Utilities::sanitizeHtml($input, ['p', 'strong'], ['class']);
ИИ почти понял мои намерения, но ошибочно предположил, что мой ожидаемый результат должен включать в себя атрибут style
.
$expected = '<p style="font-weight: bold" class="allowed">This is a <strong>test</strong> with some <em>disallowed tags</em>.</p>' .
'<p>This is also allowed.</p>';
Заметьте, однако, что он был достаточно умен, чтобы скопировать мой стиль конкатенации для переноса строк.
Я корректирую ожидаемый результат, и Copilot, как и ожидалось, заполняет утверждение за меня.
$input = '<p style="font-weight: bold" class="allowed">This is a <strong>test</strong> with some <em>disallowed tags</em>.' .
'</p><img src="http://example.com/image.jpg" alt="Image" /><p>This is also allowed.</p>';
$actual = Utilities::sanitizeHtml($input, ['p', 'strong'], ['class']);
$expected = '<p class="allowed">This is a <strong>test</strong> with some disallowed tags.</p>' .
'<p>This is also allowed.</p>';
$this->assertEquals($expected, $actual);
Задача посерьезней
Пора завязывать со всякой мелочевкой. Настало время посмотреть, насколько Copilot может помочь мне с реальным кодом. Я также хочу определить предел его возможностей; может ли он написать для меня весь класс целиком — действительно рабочий класс?
Чтобы выяснить это, в качестве примера я выбрал класс для генерации и проверки JSON веб-токенов (JWT). Я также решил, что класс должен поддерживать алгоритмы подписи HS256 и RS256.
Я начинаю с создания нового класса и добавляю в него вполне лаконичный комментарий:
/*
* Class to generate verify HS256 and RS256 JWT tokens, without using any external libraries.
*/
Обратите внимание, что я заранее указываю “without using any external libraries” (без использования каких-либо сторонних библиотек) — в моих экспериментах для более сложных задач, подобных этой, Copilot иногда предлагал решения, в рамках которых может понадобится какая-нибудь опенсорсная библиотека. Я хочу посмотреть, на что он способен сам по себе.
Сначала немного человеческого ввода — я знаю, что хочу использовать здесь кодировку base64 url-safe, поэтому я прописываю такие имена функций. Но приведенные ниже реализации на 100% написаны Copilot, и там не к чему придраться.
private function urlsafeB64Encode(string $input): string
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
private function urlsafeB64Decode(string $input): string
{
$remainder = strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= str_repeat('=', $padlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
Затем я перехожу к функции для создания HS256 JWT. Здесь мне пришлось внести некоторые очень незначительные изменения — ИИ неправильно определил порядок строк для кодировки base64, но все остальное он сделал правильно. Также он не догадался, что нужно использовать вышеперечисленные функции вместо встроенной base64_encode
.
private function generateHS256(array $payload, string $secret, int $expires = 0): string
{
$header = json_encode(['typ' => 'JWT', 'alg' => $this->algorithm]);
$payload = json_encode($payload);
$signature = hash_hmac('sha256', "$header.$payload", $secret, false);
$signature = $this->urlsafeB64Encode($signature);
$header = $this->urlsafeB64Encode($header);
$payload = $this->urlsafeB64Encode($payload);
return "$header.$payload.$signature";
}
Точно так же Copilot понял, какие функции OpenSSL использовать для подписи и проверки RS256. Еще раз, после некоторых незначительных ручных правок, мой финальный вариант класса выглядел так, как показано ниже. Copilot написал за меня около 92% этого кода.
<?php
namespace Gebler\Copilot;
/*
* Class to generate verify HS256 and RS256 JWT tokens, without using any external libraries.
*/
use RuntimeException;
use InvalidArgumentException;
class Jwt
{
private string $algorithm;
private array $config;
/**
* @throws RuntimeException|InvalidArgumentException
*/
public function __construct(string $algorithm = 'HS256')
{
if (in_array($algorithm, ['HS256', 'RS256'])) {
$this->algorithm = $algorithm;
} else {
throw new InvalidArgumentException('Invalid algorithm');
}
$this->config = [
'digest_alg' => "sha512",
'private_key_bits' => 2048,
'private_key_type' => \OPENSSL_KEYTYPE_RSA,
];
}
private function urlsafeB64Encode(string $input): string
{
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
private function urlsafeB64Decode(string $input): string
{
$remainder = strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= str_repeat('=', $padlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
private function generateHS256(array $payload, string $secret, int $expires = 0): string
{
$header = json_encode(['typ' => 'JWT', 'alg' => $this->algorithm]);
$payload = json_encode($payload);
$header = $this->urlsafeB64Encode($header);
$payload = $this->urlsafeB64Encode($payload);
$signature = hash_hmac('sha256', "$header.$payload", $secret, false);
$signature = $this->urlsafeB64Encode($signature);
return "$header.$payload.$signature";
}
private function generateRS256(array $payload, string $privateKey, int $expires = 0): string
{
$header = json_encode(['typ' => 'JWT', 'alg' => $this->algorithm]);
$payload = json_encode($payload);
$signature = '';
openssl_sign("$header.$payload", $signature, $privateKey, OPENSSL_ALGO_SHA256);
$header = $this->urlsafeB64Encode($header);
$signature = $this->urlsafeB64Encode($signature);
$payload = $this->urlsafeB64Encode($payload);
return "$header.$payload.$signature";
}
private function verifyHS256(string $jwt, string $secret): bool
{
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return false;
}
list($header, $payload, $signature) = $parts;
$jwtSignature = $this->urlsafeB64Decode($signature);
$signature = hash_hmac('sha256', "$header.$payload", $secret, false);
return $jwtSignature === $signature;
}
public function generateRS256PrivateKey(): string
{
$res = openssl_pkey_new($this->config);
openssl_pkey_export($res, $privateKey, null, $this->config);
return $privateKey;
}
private function verifyRS256(string $jwt, string $publicKey): bool
{
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return false;
}
list($header, $payload, $signature) = $parts;
$header = $this->urlsafeB64Decode($header);
$payload = $this->urlsafeB64Decode($payload);
$signature = $this->urlsafeB64Decode($signature);
$signature = openssl_verify("$header.$payload", $signature, $publicKey, OPENSSL_ALGO_SHA256);
return $signature === 1;
}
public function generateJwt(array $payload, string $secret, int $expires = 0): string
{
if ($this->algorithm === 'HS256') {
return $this->generateHS256($payload, $secret, $expires);
} else {
return $this->generateRS256($payload, $secret, $expires);
}
}
public function verifyJwt(string $jwt, string $secret): bool
{
if ($this->algorithm === 'HS256') {
return $this->verifyHS256($jwt, $secret);
} else {
return $this->verifyRS256($jwt, $secret);
}
}
public function getDecodedPayload(string $jwt): array
{
$parts = explode('.', $jwt);
if (count($parts) !== 3) {
return [];
}
list($header, $payload, $signature) = $parts;
$header = $this->urlsafeB64Decode($header);
$payload = $this->urlsafeB64Decode($payload);
return json_decode($payload, true);
}
public function getRS256PublicKey(string $privateKey)
{
$details = openssl_pkey_get_details(openssl_pkey_get_private($privateKey));
return $details['key'];
}
}
Заключение
Я не собираюсь что-либо преуменьшать. Copilot — самая крутая штука, которую я когда-либо видел! Это также самая сложная система искусственного интеллекта, которую я когда-либо встречал. Да, часть кода была не совсем идеальной, несколько строк тут и там нуждались в доработке. Но для программиста-ИИ, который еще даже не считается готовым к работе, уровень понимания PHP, помимо простых алгоритмов и стандартных задач, кода для конкретных систем и библиотек, был более чем впечатляющим.
Я проводил еще некоторые дополнительные тесты, которые я не описал в этом посте, чтобы проверить, как Copilot справляется с интеграцией с Symfony и Doctrine, несколькими CMS-системами и некоторыми популярными OS-библиотеками. Я также попробовал его на JavaScript, и как генерация кода, так и возможности прогнозирования были столь же впечатляющими.
Меня особенно поразил анализ естественного языка и возможность на его основании получить готовый код.
Что было особенно интересно, так это то, насколько специфичным я мог быть, описывая то, что мне нужно. Если я получал предложение с неопределенным классом или ссылкой на какую-то внешнюю библиотеку, я мог буквально просто написать “... без использования каких-либо внешних библиотек» в конце предложения, и Copilot был достаточно сообразителен, чтобы понять это сообщение.
Приведенный выше пример с JWT представляет собой код, который я, как человек, просматривающий документацию или пытающийся найти хорошие примеры, проверяя, что я делаю, по ходу дела, сомневаясь в себе... может быть, занял бы у меня около часа, прежде чем я бы получил окончательный результат. С Copilot у меня ушло около семи минут. Это внушительное повышение производительности.
Этот плагин — это просто глоток свежего воздуха с точки зрения эксплуатации. Просмотр возможных реализаций моей следующей строки или функции был одним удовольствием.
Copilot, похоже, немного лучше разбирается в JavaScript, чем в PHP. Если мой доступ к превью версии сохранится, я буду рад продолжить экспериментировать, чтобы увидеть, что он может сделать с Python, Java и Go.
Единственное, что меня беспокоит, — это еще одно десятилетие, и нам действительно может понадобиться гораздо меньше людей-программистов, задействованных в процессе создания программного обеспечения.
Может быть, наконец пришло время разобраться в алгоритмах машинного обучения?
Как проводить отладку и профилирование приложений на PHP? Разберемся сегодня в 20:00 на открытом уроке в рамках онлайн-курса "PHP Developer. Professional".
Детально разберем самый популярный инструмент отладки в экосистеме PHP — xDebug. Обсудим установку и настройку xDebug, поговорим о профилировании приложений на реальных примерах, с демонстрацией и разбором результатов.