Artificial Intelligence
ChatGPT and other AI tools have become essential for developers, but their true potential goes beyond answering prompts. How can these tools help junior developers grow into senior roles? The answer lies in prompt engineering, understanding LLM capabilities, and leveraging them effectively.
How ChatGPT Works
ChatGPT is a conversational AI that processes natural language to generate responses. It applies trained rules without reasoning independently. This means quality input yields quality output, while poor input leads to poor results. Additionally, its training data limits its knowledge to specific timeframes, making it unreliable for new or niche topics.
Effective Use of ChatGPT
- Understanding Code
LLMs can analyze unfamiliar code, making it easier for developers—especially juniors—to understand and modify. For instance, asking ChatGPT to explain a function often provides a clear breakdown. - Code Suggestions
Tools like GitHub Copilot excel in contextualizing code, analyzing your editor and repository to provide relevant suggestions. These tools surpass generic LLMs in specificity. - Writing Tests
Test creation is a critical skill that takes time to master. LLMs can generate initial test cases, saving time and ensuring better code coverage. - Improving Security
ChatGPT can identify potential vulnerabilities and suggest improvements, such as adding parameter validations or null checks. - Refactoring Code
Refactoring suggestions from LLMs improve readability and maintainability. Although not always performance-optimized, they often provide a clearer structure. - Generating Boilerplate Code
Starting new projects or functionalities often involves repetitive tasks. ChatGPT can generate boilerplate code tailored to your needs, reducing setup time. - Adding Comments
LLMs can automatically add detailed comments to your code, making it easier to understand and maintain, especially for undocumented or legacy systems. - Code Reviews
GitHub Actions like ChatGPT Code Reviewer automate pull request reviews, providing actionable insights and improving the quality of contributions. - Documentation and Translation
LLM-powered tools simplify documentation and even automate translations, making collaboration across teams seamless.
LLMs Are Not Designed for Problem Solving or Riddles
LLMs are built to answer questions, write articles, and generate code, not to solve problems or riddles. If you present an unsolved problem to an LLM, it won’t be able to solve it. However, if you provide a solved problem, the model can explain or reproduce the solution.
Watching someone present a riddle to ChatGPT and then being surprised when it fails to solve it is akin to asking a 5-year-old to tackle quantum physics—such tests ignore the tool’s design and capabilities. They don’t prove the model is inadequate but rather highlight the user’s misunderstanding of how to utilize it.
Knowledge Limitations
LLMs have finite knowledge restricted to the data in their training sets. If you ask about a topic not included in its dataset, the model can’t provide an accurate answer.
When does this limitation become obvious?
- When asking about products, technologies, or events that didn’t exist or were newly introduced during the model’s training phase.
While mitigations like real-time web queries and continuous training are emerging, these are partial solutions. Models may still produce low-quality or fabricated responses about recent developments.
Don’t expect reliable answers from ChatGPT on newly released products, technologies, or recent events.
Where Results Improve
Some models, like GitHub Copilot, surpass prompt-only LLMs by leveraging additional context, such as analyzing the code around the cursor, open files, and linked GitHub repositories. This broader context allows Copilot to produce more accurate and relevant suggestions.
Example: Contextual Code Suggestion
Given the following code:
java<code>public static final int UNO = 1;
public static final int DUE = 2;
// Other static variables
public static void main(String[] args) {
// Find a match for the parameter in the static values
}
</code>
Code language: PHP (php)
This vague comment wouldn’t yield useful results with ChatGPT. However, Copilot can infer its purpose and suggest a complete implementation:
java<code>int parametro = Integer.parseInt(args[0]);
switch (parametro) {
case UNO:
System.out.println("UNO");
break;
case DUE:
System.out.println("DUE");
break;
// Other cases
default:
System.out.println("No match");
}
</code>
Code language: JavaScript (javascript)
Although the logic is basic, the key point is Copilot’s ability to generate context-aware solutions with minimal input.
Code Understanding and Contextualization
Analyzing unfamiliar code can be daunting, particularly for junior developers or when venturing outside one’s comfort zone. LLMs assist by explaining what code does and how it works.
For instance, given the following function:
java<code>public static boolean matchesURI(URI uri1, URI uri2) {
String scheme = uri1.getScheme();
if (scheme.equalsIgnoreCase(uri2.getScheme())) {
if (uri1.getHost().equalsIgnoreCase(uri2.getHost())) {
int thisPort = HttpClient.normalizePort(scheme, uri1.getPort());
int thatPort = HttpClient.normalizePort(scheme, uri2.getPort());
if (thisPort == thatPort) {
return uri2.getPath().startsWith(uri1.getPath());
}
}
}
return false;
}
</code>
Code language: JavaScript (javascript)
When asked to describe the function, an LLM might respond:
- Compares the schemes (protocols) of two URIs.
- Matches hosts and normalized ports.
- Checks if one URI’s path starts with the other’s path.
This concise explanation helps developers understand complex or poorly documented code.
Test Creation
Writing tests can be challenging, especially for juniors. LLMs can generate test cases for a function, providing a foundation for robust testing. For example:
java<code>@Test
public void testMatchingURIs() {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("http://example.com/path/subpath");
assertTrue(URIMatcher.matchesURI(uri1, uri2));
}
@Test
public void testNonMatchingScheme() {
URI uri1 = new URI("http://example.com/path");
URI uri2 = new URI("https://example.com/path");
assertFalse(URIMatcher.matchesURI(uri1, uri2));
}
</code>
Code language: JavaScript (javascript)
Refactoring for Clarity
LLMs can also help refactor code for readability and maintainability:
Original code:
java
<code>if (uri1.getHost().equalsIgnoreCase(uri2.getHost())) {
if (port1 == port2) {
if (path2.startsWith(path1)) {
return true;
}
}
}
return false;
</code>
Code language: JavaScript (javascript)
Refactored:
java
<code>if (!uri1.getHost().equalsIgnoreCase(uri2.getHost())) return false;
if (port1 != port2) return false;
return path2.startsWith(path1);
</code>
Code language: JavaScript (javascript)
Though the logic remains the same, the refactored version is easier to read and debug.
Writing Effective Prompts
The key to using LLMs effectively is crafting clear and precise prompts. But what does that actually mean? Writing a good prompt involves providing explicit instructions on what you expect from the model and what outcome you want.
Here are some basic but essential guidelines:
- Be clear and concise.
- Be specific.
- Include context and necessary details.
- Define input and output expectations.
When applied to programming, these principles may seem straightforward, but creating clear, specific, and concise instructions for a software project is challenging and requires experience with best practices. The more complex a project becomes, the harder it is to distill it into a well-defined prompt.
Real-World Challenges
Have you ever encountered project specifications that changed over time, were unclear, or lacked the necessary details? Or written code without fully understanding the client’s expectations? In such cases, asking an LLM to deliver exactly what the client wants is almost impossible.
What Makes a Good Prompt?
A giant, overly detailed prompt is impractical, but a prompt that’s too short or vague won’t yield the desired results. The solution is to craft prompts that are:
- As specific as possible.
- Contextually rich yet concise.
Example: A Clear and Focused Prompt
“Write a C# function to calculate the factorial of a given number.”
This prompt specifies:
- The programming language.
- The functionality required.
- The input and output expectations.
Such a prompt allows the model to produce an accurate and effective response.
Context Matters
LLMs rely on context to improve results. For example, entering a problem commonly encountered by other programmers increases the chances of obtaining a good solution. However, for highly specific or niche problems, even with sufficient details, the model may struggle.
In such cases, tools like GitHub Copilot, which analyze surrounding code and provide tailored suggestions, might be a better fit.
Commenting Code
How many times have you written code without adding comments? Or had to comment someone else’s code but struggled to understand it?
LLMs can help generate comments by starting with a generic prompt like:
“Add comments to the following code:”
For example, given this code:
private String readFile(String fileName) {
BufferedReader br;
try {
br = new BufferedReader(new FileReader(fileName));
final StringBuilder sb = new StringBuilder();
String line = br.readLine();
while (line != null) {
sb.append(line);
sb.append(System.lineSeparator());
line = br.readLine();
}
br.close();
return sb.toString();
} catch (IOException e) {
e.printStackTrace();
}
return "";
}
Code language: JavaScript (javascript)
An LLM could generate the following detailed comments:
/**
* Reads the content of a file and returns it as a string.
* @param fileName The name of the file to read.
* @return The file content as a string, or an empty string if an error occurs.
*/
private String readFile(String fileName) {
BufferedReader br;
try {
// Initialize a BufferedReader to read the file.
br = new BufferedReader(new FileReader(fileName));
// Create a StringBuilder to construct the file content.
final StringBuilder sb = new StringBuilder();
// Read the first line of the file.
String line = br.readLine();
// Continue reading until there are no more lines.
while (line != null) {
// Add the current line to the StringBuilder.
sb.append(line);
// Add a system-specific line separator.
sb.append(System.lineSeparator());
// Read the next line.
line = br.readLine();
}
// Close the BufferedReader.
br.close();
// Return the file content as a string.
return sb.toString();
} catch (IOException e) {
// Handle any I/O errors.
e.printStackTrace();
}
// Return an empty string if an error occurs.
return "";
}
Code language: PHP (php)
This process transforms unstructured, undocumented code into something much clearer and more maintainable. Even for simple examples, LLMs can provide significant value in improving code readability and helping developers manage unfamiliar codebases.
Translations
One of the most annoying tasks a programmer can face is translating project documentation into another language. It’s a tedious job that requires time and attention but it is necessary to make the project understandable to those who don’t speak our language.
For a few years, I’ve been producing all the material I need in markdown format. Markdown is a lightweight writing format that allows you to create documents simply and quickly, without worrying too much about formatting.
This approach lets me write documentation faster and quickly extract slides from it using tools like Marp. Additionally, it’s very easy to track differences between documentation versions and see what has changed over time.
A few months ago, I found myself needing to translate a project’s documentation into English. Doing this manually by copy-pasting text into an LLM and back again is tedious, time-consuming, and requires a lot of focus.
I was about to write a program myself when I discovered extensions for GitHub that can automate documentation translation using simple Actions. I’m referring to GTP-Translate: https://github.com/3ru/gpt-translate.
I’m sure there are other similar extensions, but this one integrated perfectly into the project lifecycle I manage and helped speed up the documentation translation process.
Using a simple issue, you can activate the GTP-Translate Action, and specify which markdown document to translate, and the target language. The result is a pull request with the translated document, ready for review and approval.
Conclusions
When approaching an LLM, it’s important to understand that it’s not a magic wand that solves all problems. Instead, it’s a tool that can help speed up work and improve code quality. Writing a prompt and expecting it to magically transform your thoughts into perfectly tailored code is unrealistic.
Similarly, thinking that a single AI tool can solve all problems is a misconception, at least for now.
The best way for a programmer to approach an LLM is to understand its limitations and identify the contexts where it can be used successfully. Start mentally cataloging these scenarios and associate each with a technique or tool that can be effectively employed.
Likewise, it’s unthinkable to encapsulate an entire project in a single prompt. It’s much more effective to break the project into parts and ask the model to work on these individual components to achieve better, more precise results.
Even in contexts where a model cannot generate code, it can likely document, comment, translate, or perform many other tasks that can speed up our work.
We’re not yet at a point where these tools can do everything automatically, but a programmer who knows how to use them can achieve excellent results and enhance the quality of their work.
Improving your quality means becoming more effective and gradually shedding the “junior” label to don the heavier and well-worn hat of a senior programmer.