Ndifreke Ekott

Thoughts, stories, ideas and programming

05 Feb 2025

How I finally Came Around To Adopting And Pair Programming With Generative AI

It is very difficult to turn on the news or read anything on the news or internet today without seeing the phrases “ChatGPT”, “Generative AI”, “Open AI” crop up. We have businesses so hyped up about adopting AI and hoping for ways AI could replace Software Engineers and take over the coding tasks.

I am generally slow to jump on any hype or bandwagon. I tend to take my time, observe and see what interesting thing people are doing before venturing out myself. In the last two to three months, I have been using Generative AI as a coding companion, but I use it somewhat differently. Before I talk about how I use Generative AI as a coding companion, I will first highlight the different ways people use Generative AI or I have seen people use Generative AI.

How Do Software Developers Use Generative AI?

AI Coding Assistant

Software developers have integrated AI coding assistants like GitHub Copilot and Claude into their daily workflows, using them to accelerate coding tasks and improve productivity. These tools help generate boilerplate code, suggest function implementations, and assist with code refactoring. Developers prompt the AI with natural language descriptions or partial code, and the AI responds with relevant code suggestions they can review, modify, and integrate.

Testing and Documentation Generation

Documentation and testing have become more efficient with AI assistance. Developers use AI to generate comprehensive docstrings, write unit tests, and create technical documentation. The AI can analyse existing code to suggest test cases and produce clear explanations of complex functionality, though developers still need to verify the accuracy and completeness of AI-generated content.

Debugging and Error Message Explainer

Beyond coding, developers leverage AI for code review, debugging, and learning. They paste error messages to get explanations and potential fixes, use AI to understand unfamiliar codebases, and get recommendations for best practices. The AI serves as an always-available programming partner for brainstorming solutions and exploring different approaches to technical problems. However, developers maintain critical oversight - verifying AI outputs, ensuring security best practices, and making final decisions about implementation details.

How am I using AI?

Enquire about features of a Library

In our field of software engineering, there are endless list of software libraries out there. I have recently been working on a hobby project and playing with SqlAlchemy after being away from SqlAlchemy for years and living the database live in Django land. So it is only natural that the saying “You don’t use it, you lose it”, which means I have forgotten much about SqlAlchemy. It also doesn’t help that my last experience of SqlAlchemy was version 1 and not the latest version 2.

While building my project, there are times I would turn to AI to ask about how a certain feature of the ORM works. For example, I have been trying to adopt Domain Driven Design practices and have been using ValueObjects and Types. An example, rather than use int or uuid as types for my domain identity fields, I turn to type alias like UserId. Below is code in python for type alias for UserId.

UserId = int
EventId = UUID

This allows us to type hint functions explicitly with what the types are to help improve readability. Compare the two functions below:

def get_user(id: int):
    ....
vs

def get_user(id: UserId):
    ....

To store these values in the database, I needed a way to convert them to types that the SqlAlchemy ORM supports. I could go the naive way and always convert the type alias into the native types the ORM easily supports but this will require me doing this everywhere in the code base. What about D.R.Y principle?

Fortunately, SqlAlchemy supports a feature for encoding custom types to Types native to the ORM. And this is the point, I ask GenAI to tell what features in the ORM will meet this goal and better still show an example.

from sqlalchemy.types import TypeDecorator, CHAR
from sqlalchemy.dialects.postgresql import UUID
import uuid

class UserId(TypeDecorator):
   impl = CHAR(32)
   cache_ok = True

   def load_dialect_impl(self, dialect):
       if dialect.name == 'postgresql':
           return dialect.type_descriptor(UUID())
       return dialect.type_descriptor(CHAR(32))

   def process_bind_param(self, value, dialect):
       if value is None:
           return value
       if dialect.name == 'postgresql':
           return str(value)
       return value.hex

   def process_result_value(self, value, dialect):
       if value is None:
           return value
       if not isinstance(value, uuid.UUID):
           value = uuid.UUID(value)
       return value

Obviously, I updated the code to my liking and also cross referenced the documentation to make sure this is right and also remove overrides I didn’t need. I also asked about other features like, “What is the difference between session.add and session.merge, and when do I use each one?”.

As a typing assistant

For coding, I use the Pycharm IDE and after going through the trial period, I decided to try a month’s subscription and give pair programming with AI a go. Let us say, there are more upsides than downsides, with a few quirks in the middle.

I must confess, I am resistant to change, I also don’t like magic, I love to understand how things work. Herefore, I don’t blindly copy and paste. So even though AI can generate a lot of code for me, it just works isn’t enough for me. I prefer to understand, see if I like the coding style and may tweak the code accordingly.

A good thing about using an integrated AI experience within the IDE is that Generative AI can read your source code and adopt your coding style, which is a plus in my book.

Knowing the code I would like to write next, I find myself palming that off to AI to do the writing. I could have written it anyway, but instead, let’s get the code generated. I can focus on prompting the constraints I would love the new code to have and what the input parameters should be.

Many times, Generative AI has written code in ways I haven’t seen or written, and in the process, I learn a new pattern moving forward.

Taking on a simple example. Assuming you were given the task - _“You have a list of unique non-repeating numbers, and you are tasked with removing a specific number specified from the list”. I will start with a typical approach to the problem and later show the output from the AI.

data_set = [1, 2, 3, 4, 5, 6, 7]
number_to_remove = 4

result = []

for num in data_set:
    if num != number_to_remove:
       result.append(num)

# As an experience dev and one with a few more tricks, I could have also written it as.
result = list(filter(lambda x: x != number_to_remove, data_set))

Though I like my experience developer approach (”Aren’t one line solutions awesome?”), it may be difficult to read at first glance and requires squinting. Even worse, developers unaware of how the filter function works may struggle with the code. So, what approach did Generative AI take?

data_set = [1, 2, 3, 4, 5, 6, 7]
number_to_remove = 4
result = [num for num in data_set if num != number_to_remove]

Agreed, it is three lines, but you could easily read the for comprehension as plain English: “For every number in data_set, if the number isn’t number_to_remove, add it to the list and continue.”

Quality Assurance Agent a.k.a Test Engineer

You could go the lazy route by asking AI to scan your code and generate a bunch of test code to exercise your application. You could walk away feeling well accomplished, however, don’t do it. You still need to be aware of all the test generated and what each test is exercising.

I find myself instructing GenAI to write specific tests for me. Since I mentioned I am applying a bit of DDD, I find it fascinating that it follows the appropriate convention I would expect to mutate my domain models. Rather than accessing properties directly, it uses the appropriate methods provided.

I have a class Event, an Event can have Participant, ignoring the details of participants, you can immediately see that we should have a participants field on the Event class. You could easily write the code event.participants = [participant] and you will be fine. We however frown upon that approach but instead provide event.add_participant(participant) method. This approach allows us to perform various validation logic and trigger Domain events. Imagine how impressed I was to see that GenAI used the appropriate methods for adding Participants to an Event.

Identify Edge Cases

This is following on from the previous section, in a different context, rather than continue the path of generating more tests codes, I turned to have a conversation with the AI and ask it to evaluate my test file and generate a list of edge cases I may have missed or not considered. Here is a screenshot of part of its response.

Screenshot 2025-02-05 at 00.57.12.png

Reading through the suggestions, I picked each cases and had it generate an appropriate test case to cover those concerns. I found this way of thinking about test really important as it unlocks more test coverage for your application.

I also went a step further and asked it to “Identify redundant tests”. This was important as I believe having many test doesn’t equate to good tests. Below is a screenshot of the response from Gen AI.

Screenshot 2025-02-05 at 01.00.09.png

Generate Configuration or Specification Files

If you ever delve into setting up Server infrastructure, you would have gotten use to writing a lot of infrastructure code or config files. For example, when setting up a web application, these days, we typically have a reverse proxy in front of the web app, and a popular one is Nginx.

AI is really good at generating code from well-understood standards. So generating an Nginx config file to match your web server setup is no big deal.

Another use case is generating specification files. For my project, I was also writing OpenAPI specification files for my backend API. Numerous times, I can get GenAI to write out the specification for the endpoint saving me some typing.

Conclusion

In conclusion, adopting and pair programming with Generative AI has transformed my coding journey in unexpected and beneficial ways. From accelerating mundane coding tasks to enhancing my learning with new patterns and ideas, Generative AI has proven to be an invaluable companion. Despite initial reservations, I’ve found that by integrating AI thoughtfully and leveraging its strengths, I can focus more on the creative and complex aspects of software development. While critical oversight and understanding remain crucial, the synergy between human ingenuity and AI assistance is a powerful combination. As technology evolves, I look forward to further exploring and refining this partnership, continually enhancing my productivity and expanding my capabilities.