[Перевод] Первый опыт работы с GitHub Copilot X: взгляд программиста
Будучи разработчиком ПО, я всегда нахожусь в поиске инструментов и технологий, которые могут повысить эффективность моей работы. Недавно у меня появилась возможность протестировать GitHub Copilot X, ИИ-ассистента для кодинга на базе машинного обучения. Мне не терпелось узнать, на что способна эта штука и как она впишется в мой рабочий процесс. В этой статье я поделюсь своим опытом первого использования GitHub Copilot X и изложу свои мысли о том, стоит ли включать его в свой набор инструментов.
Следует отметить, что при написании этой статьи я решил использовать только те функции, которые в настоящее время доступны в рамках публичного превью GitHub Copilot X. В качестве среды программирования я использовал Visual Studio Code, а писал на языке Go. Ваш опыт взаимодействия с Copilot X может отличаться в зависимости от используемой IDE и языка, кроме того, часть возможностей может не поддерживаться в полной мере в некоторых других IDE. Несмотря на наличие других экспериментальных и nightly-сборок, я хотел поделиться максимально реалистичной картиной использования этого инструмента в его текущем состоянии в связке с широко используемой IDE. Тем не менее, я изучил весь спектр функций, доступных в рамках публичной превью-версии, и смог получить неплохое представление о том, как они могут быть полезны в реальных сценариях программирования.
Интеграция с IDE
В целом, интеграция GitHub Copilot X в IDE прошла довольно быстро. Инсталляция расширения не вызвала никаких проблем, а вот процесс аутентификации показался мне немного сложным — в VS Code было не совсем понятно, как именно я должен авторизоваться в GitHub. Но я быстро разобрался, и дальше процесс аутентификации прошел сравнительно безболезненно. Здесь следует упомянуть, что перед использованием новых функций вам придется перезапустить VS Code, однако это скорее незначительное неудобство, чем серьезная проблема. В целом, процесс оказался не таким сложным, как мне представлялось, и я смог довольно быстро внедрить Copilot X в свои рабочие процессы.
Написание нового кода
Первая и самая очевидная особенность GitHub Copilot X — это его способность писать код с нуля. В этом разделе мы рассмотрим, как Copilot X может помочь вам в создании новых функций, классов и других структур, насколько хорошо этот инструмент адаптируется к существующей кодовой базе и может ли он улучшить процесс кодинга в целом.
Чтобы проверить, может ли Copilot генерировать новый код в соответствии с уже имеющимся стилем, я решил попросить его написать новую функцию для структуры Snowplow, с помощью которой я делаю запросы к хранилищу данных Snowflake, — я довольно часто использую ее в процессе написания кода. Итак, в качестве эксперимента я поручил ИИ-ассистенту создать новую функцию, которая будет извлекать финансовые активы, связанные с определенной датой. Я начал с описания функции, указав только сигнатуру и имя: GetAssetsForDate. К моему удивлению, GitHub Copilot X сразу же выдал такой код:
func (client *Snowplow) GetAssetsForDate(ctx context.Context, provider gopb.Provider, date *time.Time) ([]*snowflake.Asset, error) {
client.logger.Log("Getting assets data for %s provider...", provider)
// First, create the query from our table name and select clause
query:= orm.NewQuery ().Select (assetColumns…).From (client.fullName (Assets)).
Where (orm.And, orm.Equals («provider_id», provider, false), orm.Equals («last_updated_utc», date, false))
// Next, run the query and attempt to collect our results into a list of assets
assets, err:= orm.RunQuery[snowflake.Asset](ctx, query, client.db, client.logger)
if err!= nil {
return nil, err
}
// Finally, return the assets we retrieved
client.logger.Log («Retrieved %d assets for %s provider», len (assets), provider)
return assets, nil
}
Но что меня по-настоящему удивило, так это то, что Copilot смог реализовать полностью рабочий код, аналогичный тому, что я уже написал в том же файле. С комментариями к коду все тоже было хорошо.
Кроме того, Copilot X использовал те же пакеты, что и я в другом файле. Код был синтаксически корректен и делал ровно то, что я ожидал бы от функции с таким названием. Она принимает нужные типы данных и выдает нужный результат. Иными словами, я не обнаружил никаких очевидных логических проблем.
Более того, Copilot смог внедрить тип из стандартной библиотеки, входящей в состав Go, который я не импортировал явно. Это означает, что Copilot побывал за пределами конкретного файла, с которым я работаю. В целом, я был доволен качеством кода, который GitHub Copilot X смог сгенерировать по первоначальному запросу.
Чтобы показать, насколько этот результат близок к моему коду, я включил код из того же файла в качестве референса:
func (client *Snowplow) GetAssetsForProvider(ctx context.Context, provider gopb.Provider, enabled bool) ([]*snowflake.Asset, error) {
client.logger.Log("Getting assets data for %s provider...", provider)
// First, create the query from our table name and select clause
query:= orm.NewQuery ().Select (assetColumns…).From (client.fullName (Assets)).
Where (orm.And, orm.Equals («provider_id», provider, false), orm.Equals («download_enabled», enabled, false))
// Next, run the query and attempt to collect our results into a list of assets
assets, err:= orm.RunQuery[snowflake.Asset](ctx, query, client.db, client.logger)
if err!= nil {
return nil, err
}
// Finally, return the assets we retrieved
client.logger.Log («Retrieved %d assets for %s provider», len (assets), provider)
return assets, nil
}
Тем не менее, этот результат не идеален. Например, третий параметр, date
, является указателем на time.Time. Это было бы полезно, если бы я хотел сделать этот параметр необязательным. Однако код Copilot, учитывая используемый мной пакет ORM, не дает такой возможности.
Пользователь потенциально может передать этой функции null, что неизбежно приведет к ошибке. Еще одна потенциальная проблема, которую я заметил, заключалась в том, что сигнатура функции не была прокомментирована. Но, введя // в строке над сигнатурой, я получил от Copilot комментарии к ней, и они также были вполне корректными.
Рефакторинг кода
Рефакторинг является важной частью процесса разработки программного обеспечения. Он позволяет повысить производительность, расширить функциональность кода или просто сделать его более понятным. Далее мы попробуем разобраться, может ли Copilot X помочь вам с рефакторингом и сделает ли он это быстрее и лучше, чем вы делаете это сами. Также мы узнаем, насколько хорошо Copilot X умеет адаптироваться к вашему существующему стилю программирования и может ли он улучшить ваш рабочий процесс в целом.
Чтобы проверить упомянутую выше способность Copilot X, я решил поэкспериментировать с несколькими сценариями использования этого инструмента, включая преобразование данных, содержащихся в существующей переменной, из одного типа в другой, внесение изменений и исправление ошибок, возникших в результате обновления пакета.
Преобразование типов данных
В том же файле у меня есть список названий столбцов, которые должны возвращаться в результате запросов, связанных с активами:
var assetColumns = []string{"symbol", "provider_id", "asset_name", "asset_type", "market", "locale",
"currency_symbol", "currency_name", "base_currency_symbol", "base_currency_name", "delisted_utc",
"primary_exchange", "cik", "composite_figi", "share_class_figi", "last_updated_utc", "download_enabled"}
Теперь предположим, что я хочу преобразовать всё это в строку, разделенную запятыми. Справится ли Copilot X с этой задачей? Да, справится. Под этой строкой я начал вводить var assetColums = ""
. В этот момент Copilot выдал вот такой код:
var assetColumns = "symbol, provider_id, asset_name, asset_type, market, locale, currency_symbol, currency_name, base_currency_symbol, base_currency_name, delisted_utc, primary_exchange, cik, composite_figi, share_class_figi, last_updated_utc, download_enabled"
В целом, Copilot справился с этим заданием с небольшой подсказкой, быстро сгенерировав верный с синтаксической точки зрения вариант решения. Однако я заметил, что в некоторых случаях, когда длина полученной строки превышала установленные в других частях файла нормы, Copilot не делал перевод строки.
Если для небольших фрагментов кода это не проблема, то в крупных проектах это может привести к проблемам с читабельностью. При использовании Copilot X для рефакторинга важно помнить об этой особенности.
Обновление кода
Чтобы проверить способность Copilot X внедрять обновления пакетов, я попытался заменить сырые строковые SQL-запросы на запросы с использованием импортированного пакета ORM.
Сначала я вручную написал пример запроса с использованием пакета ORM, чтобы помочь Copilot X понять, что я вообще хотел сделать.
Затем я попросил Copilot X преобразовать исходный SQL-запрос в код ORM. Вот код, с которого я начал.
querier := func(offset int, size int) *Query {
return NewQuery(fmt.Sprintf("SELECT %s FROM %s WHERE provider_id = ? AND symbol = ? AND sip_timestamp >= ? "+
"AND sip_timestamp < ? ORDER BY sip_timestamp LIMIT ? OFFSET ?", quoteColumns, client.fullName(Quotes)),
provider, symbol, startTick, endTick, size, offset)
}
После ввода querier := func(o
Copilot X выдал следующий вывод с пятью табуляциями и энтерами.
querier := func(offset int, size int) *orm.Query {
return orm.NewQuery().Select(quoteColumns...).From(client.fullName(Quotes)).
Where(orm.And, orm.Equals("provider_id", provider), orm.Equals("symbol", symbol),
orm.Equals("sip_timestamp", startTick), orm.Equals("sip_timestamp", endTick)).
OrderBy("sip_timestamp").Limit(size).Offset(offset)
}
Несмотря на то, что результат был частично верным, несколько ошибок компиляции и логических неточностей все же пришлось исправить. Для сравнения, вот так должен выглядеть правильный код:
querier := func(offset int, size int) *orm.Query {
return orm.NewQuery().Select(quoteColumns...).From(client.fullName(Quotes)).
Where(orm.And, orm.Equals("provider_id", provider, false), orm.Equals("symbol", symbol, false),
orm.GreaterThanOrEqualTo("sip_timestamp", startTick, false), orm.LessThan("sip_timestamp", endTick, false)).
OrderBy("sip_timestamp").Limit(size, false).Offset(offset, false)
}
После устранения этих проблем я повторил попытку с другим запросом, и на этот раз Copilot X смог сгенерировать правильный код ORM без каких-либо затруднений.
Этот опыт показал мне, что Copilot X не до конца понимает, что такое внешние пакеты и как их можно использовать, но способен быстро выучить их синтаксис и даже понять значение отдельных функций и переменных.
Кстати, позже я попытался преобразовать запрос, в котором требовалось вызвать функцию Having
из того же пакета ORM, и Copilot смог предугадать существование этой функции и аргументы, которые она будет принимать, без моего участия.
Исправление Breaking Changes
Как разработчику, мне было интересно узнать, способен ли Copilot X автоматически исправлять ошибки компилятора или логические ошибки, возникшие в результате Breaking Changes в обновлении пакета, и может ли он системно исправлять их в нескольких файлах. Конечно, Copilot достаточно эффективен в задачах генерации нового кода, но неизвестно, распространяются ли его возможности на исправление Breaking Changes в пакетах.
В этом разделе я проверю эту гипотезу: специально внесу Breaking Changes и проверю, сможет ли Copilot определить и предложить исправления для поврежденного кода.
Это поможет понять возможности и ограничения инструмента и, вероятно, подскажет другим разработчикам, как интегрировать его в свои рабочие процессы.
В качестве эксперимента я добавил в интерфейс аргумент и одну новую функцию, после чего импортировал обновленный пакет в основной модуль. При попытке указать строковую константу для недостающего аргумента Copilot не смог предугадать значение этой строки или дать какие-либо предложения даже после двух попыток в разных файлах.
Это наводит меня на определенные размышления: либо Copilot не способен предвидеть подобные изменения без дополнительных примеров, либо сам тест был слишком ограниченным, чтобы в полной мере раскрыть потенциал Copilot. При этом вместо того, чтобы предлагать очевидно неправильные варианты, Copilot воздержался от каких-либо предложений вообще. Несмотря на то, что в Intellisense такое поведение меня раздражает, я уважаю Copilot за то, что он решил воздержаться от вывода мусорного результата. Только подробное тестирование поможет понять, где Copilot очевидно не справляется, а где выдает дельные предложения.
Генерация тестов
В этом разделе я попытаюсь разобраться в способности Copilot работать с тестами. В частности, я хочу определить, может ли он генерировать тест-сьюты, определять, как должны быть построены тесты с учетом библиотек тестирования или фреймворков, и генерировать достоверные, реалистичные тестовые данные.
Кроме того, я хочу узнать, может ли Copilot находить корнер-кейсы или общие точки отказа в зависимости от типа теста и определять ожидаемые ответы или значения генерируемых данных. Я надеюсь, что изучение этих аспектов работы Copilot позволит мне лучше понять, способен ли этот инструмент помочь в тестировании программного обеспечения и в чем заключаются его ограничения.
Итак, я попросил Copilot X попытаться сгенерировать юнит-тесты для функции, которую он создал ранее: GetAssetsForDate
.
Тесты должны учитывать несколько различных условий, в том числе передачу некорректных данных и возникновение ошибок в SQL. Кроме того, в случае возникновения ошибок, следует обработать их средствами внутреннего пакета. Он отформатирует ошибки и выведет дополнительную информацию, в том числе сведения о среде, пакете, классе (если он есть), функции, внутренней ошибке, а также собственно сообщение. Сможет ли Copilot сгенерировать тесты, соответствующие всем этим требованиям?
Итак, сначала я написал комментарий, описывающий желаемое поведение. После чего Copilot начал генерировать код теста, строка за строкой. В первую очередь он попытался сгенерировать тест для функции, включающей запрос с постраничным выводом, видимо, потому что я работал над этим типом теста до того, как написать эту статью. Тем не менее, он смог сгенерировать большую часть шаблонного кода и тестовые данные, которые соответствовали тем, что я использовал в тестах в том же файле. И если сначала Copilot создал несколько разных наборов тестовых данных, то, похоже, он понял, что я склонен использовать одни и те же данные в разных тестах в целях повышения эффективности.
Тем не менее, Copilot не смог сгенерировать точный SQL-запрос, который функция отправила бы на сервер, но эти нюансы было легко устранить. При генерации вызова функции Copilot пытался указать в качестве аргумента time.Time
строку, вместо того чтобы вызвать time.Parse
или time.Date
. Кроме того, ему потребовалась помощь в создании соответствующего кода верификации ошибки. После нескольких итераций он понял, чего я от него хочу. И даже генерировал правильные типы ошибок без какого-либо участия с моей стороны. После внесения некоторых изменений у меня получился следующий код:
// Tests the conditions determining which errors are returned from the GetAssetsForDate function when it fails
DescribeTable("GetAssetsForDate - Failures",
func(queryFails bool, rowErr bool, scanFails bool, verifier func(*utils.GError)) {
// First, setup the mock to return the rows
svc, mock:= createServiceMock («TEST_DATABASE», «TEST_SCHEMA»)
// Next, create the rows that will be returned by the query
rows:= mock.NewRows ([]string{«symbol», «provider_id», «asset_name», «asset_type», «market»,
«locale», «currency_symbol», «currency_name», «base_currency_symbol», «base_currency_name»,
«delisted_utc», «primary_exchange», «cik», «composite_figi», «share_class_figi»,
«last_updated_utc», «download_enabled»}).
AddRow («AAPL», 1, «Apple Inc.», 1, 4, 0, «USD»,»,»,»,», «XNAS»,
»0000320193», «BBG000B9XRY4», «BBG001S5N8V8»,»2022–06–20T00:00:00Z», false).
AddRow («AAP», 1, «ADVANCE AUTO PARTS INC», 1, 4, 0, «USD»,»,»,»,», «XNYS»,
»0001158449», «BBG000F7RCJ1», «BBG001SD2SB2»,»2022–06–20T00:00:00Z», false).
AddRow («AAUKF», 1, «ANGLO AMER PLC ORD.», 2, 5, 0, «USD»,»,»,»,»,»,
»,»,»,»2022–03–08T07:44:15.171Z», false).
AddRow («BA», 1, «Boeing Company», 1, 4, 0, «USD»,»,»,»,», «XNYS»,
»0000012927», «BBG000BCSST7», «BBG001S5P0V3»,»2022–06–20T00:00:00Z», false).
AddRow («BABAF», 1, «ALIBABA GROUP HOLDING LTD», 2, 5, 0, «USD»,»,»,»,»,»,
»,»,»,»2021–02–11T06:00:50.792Z», false).
AddRow («BAC», 1, «Bank of America Corporation», 0, 0, 0, «USD»,»,»,»,», «XNYS»,
»0000070858», «BBG000BCTLF6», «BBG001S5P0Y0»,»2022–06–20T00:00:00Z», false)
if rowErr {
rows.RowError (0, fmt.Errorf («row error»))
} else if scanFails {
rows.AddRow («BAC», «derp», «Bank of America Corporation», 0, 0, 0, «USD»,»,»,»,»,
«XNYS»,»0000070858», «BBG000BCTLF6», «BBG001S5P0Y0»,»2022–06–20T00:00:00Z», true)
}
// Setup the mock so we can verify the SQL query when it is submitted to the server
queryStmt:= mock.ExpectQuery (regexp.QuoteMeta («SELECT symbol, provider_id, asset_name,»+
«asset_type, market, locale, currency_symbol, currency_name, base_currency_symbol,»+
«base_currency_name, delisted_utc, primary_exchange, cik, composite_figi, share_class_figi,»+
«last_updated_utc, download_enabled FROM TEST_SCHEMA.assets WHERE provider_id = ? AND »+
«last_updated_utc = ?»)).WithArgs (gopb.Provider_Polygon, time.Date (2021, time.January, 1, 0, 0, 0, 0, time.UTC))
if queryFails {
queryStmt.WillReturnError (fmt.Errorf («query error»))
} else {
queryStmt.WillReturnRows (rows)
}
// Finally, call the function and verify the error
assets, err:= svc.GetAssetsForDate (context.Background (), gopb.Provider_Polygon,
time.Date (2021, time.January, 1, 0, 0, 0, 0, time.UTC))
// Verify the error and the returned assets
verifier (err.(*utils.GError))
Expect (assets).Should (BeEmpty ())
Expect (mock.ExpectationsWereMet ()).ShouldNot (HaveOccurred ())
},
Entry («Query fails», true, false, false,
testutils.ErrorVerifier («test», «snowplow»,»/xefino/quantum-core/pkg/snowplow/assets.go»,
«Snowplow», «GetAssetsForDate», 86, testutils.InnerErrorVerifier («Failed to query data from »+
«the TEST_DATABASE.TEST_SCHEMA.assets table»), «query error»,»[test] snowplow.Snowplow.GetAssetsForDate »+
» (/xefino/quantum-core/pkg/snowplow/assets.go 86): Failed to query data from the »+
«TEST_DATABASE.TEST_SCHEMA.assets table: query error»)),
Entry («rows.Err fails», false, true, false,
testutils.ErrorVerifier («test», «snowplow»,»/xefino/quantum-core/pkg/snowplow/assets.go»,
«Snowplow», «GetAssetsForDate», 86, testutils.InnerErrorVerifier («Failed to query data from »+
«the TEST_DATABASE.TEST_SCHEMA.assets table»), «row error»,»[test] snowplow.Snowplow.GetAssetsForDate »+
» (/xefino/quantum-core/pkg/snowplow/assets.go 86): Failed to query data from the »+
«TEST_DATABASE.TEST_SCHEMA.assets table: row error»)),
Entry («rows.Scan fails», false, false, true,
testutils.ErrorVerifier («test», «snowplow»,»/xefino/quantum-core/pkg/snowplow/assets.go»,
«Snowplow», «GetAssetsForDate», 86, testutils.InnerErrorVerifier («Failed to query data from »+
«the TEST_DATABASE.TEST_SCHEMA.assets table, error: sql: Scan error on column index 1, name »+
»\«provider_id\»: converting driver.Value type string (\«derp\») to a int64: invalid syntax»),
»[test] snowplow.Snowplow.GetAssetsForDate (/xefino/quantum-core/pkg/snowplow/assets.go 86):»+
«Failed to query data from the TEST_DATABASE.TEST_SCHEMA.assets table, error: sql: Scan error »+
«on column index 1, name \«provider_id\»: converting driver.Value type string (\«derp\») to a »+
«int64: invalid syntax»)))
И хотя Copilot смог сгенерировать некоторый шаблон тестовой функции, ему с трудом удалось определить структуру теста исходя из используемых библиотек и фреймворков. Кроме того, у него возникли определенные трудности с генерацией реалистичных тестовых данных и поиском корнер-кейсов или общих точек отказа.
Тем не менее, он смог извлечь уроки из предыдущих итераций и начал генерировать корректные типы ошибок без ввода данных. В целом, несмотря на то, что Copilot показывает хорошие результаты в генерации тестов, он еще не является полноценным инструментом для решения этой задачи. При использовании Copilot разработчики должны быть готовы потратить время на ручную проверку и исправление сгенерированного кода для обеспечения полного покрытия тестами.
Заключение
Copilot — это мощный инструмент, который может улучшить процесс разработки за счет генерации нового кода или использования шаблонов, примененных в уже существующем. Он особенно эффективен, когда у разработчика уже есть качественные примеры того, что нужно получить в результате. Однако, прежде чем генерировать код, который должен интегрироваться с чем-либо еще, необходимо потратить время на «обучение». Copilot генерирует синтаксически верный код примерно в 95% случаев, но у него все еще есть проблемы со связью между разными файлами или пакетами.
API по-прежнему довольно медленный (~500 мс на строку), есть небольшие проблемы с генерацией тестов. Несмотря на свои недостатки, Copilot — полезный инструмент для опытных разработчиков, которые хотят повысить эффективность своей работы. Однако младшим разработчикам и новичкам в программировании не стоит полагаться на него, ведь Copilot не способен привить характерные для языка best practices. В целом, я бы сказал, что Copilot претендует на роль ИИ-помощника программиста процентов на 50: он может генерировать код, пока вы занимаетесь рецензированием, но не способен выполнять роль рецензента.
Если вы — разработчик высокого уровня и хотели бы оптимизировать свой рабочий процесс, я настоятельно рекомендую вам протестировать Copilot. Он поможет быстро и эффективно генерировать новый код и писать шаблонные функции. Однако имейте в виду, что Copilot не может заменить программиста и не подойдет начинающим разработчиками и джунам. По мере развития ИИ и машинного обучения мы наверняка увидим еще больше интересных разработок в этой области. Так что не теряйте оптимизма, учитесь новому –, а в будущем мы снова встретимся, чтобы обсудить новинки ИИ в сфере программирования!