AI found a bug in my code
joel.tools
joel.tools
Yes, it's helpful that the system shows bugs, but it does this, not through careful analysis of the control flow or subtle type analysis, but by "probability of each token appearing given the previous tokens".
If such a large proportion of our code follows common patterns, are we not wasting huge amounts of time writing and testing the same functions across thousands or millions of pieces of code? If we (almost) always follow a certain pattern, should not that pattern be embedded in a library or language, so vastly reducing the opportunity for errors or bugs?
A lot of programming languages have a lot of room for improvement though.
The skill is in selecting the optimal amount and form of redundancy.
For example, the typical ; statement terminator is redundant. People often ask, since it is redundant, why not remove it? People have tried that, and found out that the redundancy makes for far better error detection.
I think a better example is CoffeeScript, where almost any string is a valid program but probably not the one you wanted.
They usually have another statement terminator instead, like a newline.
Of course, the downside is that now you have way more tokens you need to know to understand some code, similarly to how Haskell code tends to have tons of mega-abstract function combinators or whatever whereas Go code is very simple. Which one is more readable depends on the reader, because a language like Go requires the reader to sift through more details, whereas Haskell is much terser but also requires much more pre-existing knowledge to understand.
I'm wondering if we can use these code-generation models to find "low entropy code" that is a prime target for turning into libraries.
A()
B()
C()
So someone thinks, hey, lets extract that bit, and create a function that does all three things def ABC():
A()
B()
C()
Now the calls site is much simpler and there is no more repetition: ABC()
Except you realise that in some call sites the B() call was missing, so to fix that you add an argument: def ABC(doB):
A()
if(doB):
B()
C()
The call site now looks a bit less clean, but still no repetition! ABC(True)
or ABC(False)
Then you realise in some cases the C() call is not necessary, so just add another argument: def ABC(doB, doC):
A()
if (doB):
B()
if (doC):
C()
And your call sites look like this: ABC(True, False)
etc.At that point the repetition in the original is definitely preferrable...
def handleNewEmail(isInboxDisplayed, areNotificationsEnabled):
AddToInbox();
if (isInboxDisplayed):
RefreshInboxView();
if (areNotificationsEnabled):
SendSystemNotification();
Not defending needless refactoring in all cases, it's definitely a judgement call.(Edit: formatting)
I'm talking about the case where the conditionals are only needed because of the refactoring.
To be honest, I find it hard to discuss readibility without a real-world example. The algebraic placeholders feel too terse.
def func1():
...
A()
B()
C()
...
def func2():
...
A()
B()
C()
...
Which is then refactored to def func1():
...
ABC()
...
def func2():
...
ABC()
...
def ABC():
A()
B()
C()
Which is sensible, but then other functions show up: def func3()
...
A()
C()
...
And you refactor to: def func1():
...
ABC(True)
...
def func2():
...
ABC(True)
...
def func3()
...
ABC(False)
...
def ABC(doB):
A()
if (doB):
B()
C()
I'll leave the final example to your imagination, since I've already used so much screen space.I'm currently in the process of undoing a lot of such refactorings in my codebase because it has become unmanageable.
I really think repetitive code is much easier to work with than overabstracted code.
My personal approach to combat this is better data structures that model the problem (and are checked by the compiler). Once this is in place I try to "flatten" the calls, so that things are mostly at the top level, or few levels deep, which usually comes up naturally once the data structure is consciously defined.
I try to have as much code as possible be "structure in - structure out" (pure functions) and to concentrate stateful code to work on the structure's fields/values. This is surprisingly easy once the data structures match the problem, instead of only growing organically.
Both options can be valid. DRY is a tangential topic, the main goal should be to keep your code as close to your mental model as possible (also keeping mental models in sync between team members, which is quite hard).
ABC being a sequence could be a pattern, or it could be a coincidence. You can't know which one it is just by looking at the code. The knowledge whether it's a sequence or not is in the business domain and in your mental model of that domain.
You might think you're disagreeing on DRY with another team member, but in reality you two have different mental models, and one of you is using DRY to justify a his.
"You seem to be using ifs and fors very much. These should be abstracted into a function."
This 90% of code is not genuine word-for-word boilerplate (copy/pasted from a known good source). This code is typically constructed fresh each time; or worse, copied from somewhere similar and quickly tweaked for names/types! (I do it, and I see it done all the time.)
I expect that the remaining 10% non-boilerplate code, taking 80% of the effort, is much more carefully considered, and less likely to contain those clumsy/forgetful off-by-one or buffer-overflow bugs.
Most people don't like writing that boilerplate once they know how to do it and have done it a few times, and would rather just call a function do_that_thing_need_done_on(input1, input2).
If it can't be factored out like that and is actual language boilerplate beyond a few lines, that's a failure of the language.
If these AI models are suggesting the code that could be called in a library/module instead of the the code to actually include and call a well known and trusted library or module, I'm not sure that's progress. At least when someone notices a bug or better way to do it and updates that module or library, consumers of that module can update and benefit from it, or at a minimum see that there were bugs in the version they're running they might want to address at some point.
I think the judgement call of when to use a library & what library to use is quite subjective, even for humans to get right.
If I'm doing JSON deserialisation it might suggest I use Gson library which would be much better than rolling your own. But the original authors are saying that you should prefer Moshi over Gson — I think it'd be hard for an AI to reach that conclusion though (though maybe not if it's doing something like tracking migrations in OS projects from Gson->Moshi).
With something a little more trivial — I don't want it to add in a dependency on left-pad, even though it has 2.5M weekly downloads so is arguably both well-known and trusted :)
You could probably set a threshold for how complex code is before it's suggested to be swapped out for a lib, but then is my code simple because I'm ignoring edge cases I should support, or because I've trimmed the fat on what I'm choosing to support (e.g. i18n, date handling, email validation etc.)
The only thing worse than using a module that has a bug/security problem for a function that's just a few lines and not used again in the codebase is when the content of that function is copied in place instead of being included and nobody has an easy way of knowing whether that's the code that was suggested and included in their project. Worst of both worlds.
Since this is HN I'm sure someone will say that the answer is Lisp. But can we do better?
Lisp may be an improvement.
Another issue is consistency. Take C, Javascript, or Go. Many loops are of the form
for (var i = 0; i < n; i++) { ... }
You could argue that "for i < n" provides the same information, but then you'd have to find a way to start the loop at a different offset, use a different end condition, or different "increment".But the situation is not that bad, is it? A few characters too many, so be it. I find reusability a much larger problem.
Yes programming language design is a social science (imo)
Sounds like tokenizer -> Markov chain? Surely something trained on a TPU is more sophisticated than something we could have done in the middle of the 20th century?
At some point the variations mean that more abstractions don't really help.
A string of length N is vastly more likely to be a valid Perl program than a valid Python program. Ultimately this meant that Perl programs, while easier to type, were much harder to read, and extremely easy to misinterpret.
Perl is not only hard to read because there are many shortcuts that might look like line noise for the inexperienced (hell, Rust have a bunch of those too), it's because there is a bunch of the ways to do anything.
Like take humble array
Perl> @a
$VAR1 = 1;
$VAR2 = 2;
$VAR3 = 3;
$VAR4 = 4;
$VAR5 = 5;
Perl> @a + 2
$VAR1 = 7;
Why adding a number to array results in number ? Because array in scalar context returns its length. It leads to some very compact code if (@a > 3) {print "big array"}
but, uh, what you do if you want to see length of string ? well length($string).Will that also work for arrays ? Nope, if you want to force scalar context you're supposed to do scalar(@array). So how to add 2 arrays ?
@c = (@a, @b) #
obviously. But wait, what we really typed after variable expansion is ((1,2),(2,3)) and in other languages irb(main):001:0> a = [1,2]
=> [1, 2]
irb(main):002:0> b=[2,3]
=> [2, 3]
irb(main):003:0> c = [a,b]
=> [[1, 2], [2, 3]]
>>> a = [1,2]
>>> b = [2,3]
>>> c = [a,b]
>>> c
[[1, 2], [2, 3]]
that's exactly what we get. Confusing ? Sure. But it saves few characters!The language models try, by statistical means, to derive what should be there. Given enough data they will start to have sone (statistical) grasp on the intention.
I am not entirely sure about the boilerplate though. Often you need some minor variation of an already existing pattern. Trying to unify those slightly divergent patterns into one schema can very easily lead to very hard to understand code. Another thing is thar boilerplate is fairly easy to write and to test, because it is familiar, reducing the actual effort which goes into it. Sometimes it is just better to not reuse code.
And then if it's buried deep enough somebody will add another layer and fix the cases that were found - there.
And that's how the disgusting legacy code happens.
KISS, please. Unnecessary abstraction is the root of almost all problems in programming.
Hell, just helpers/utilities functions within an organisation aren't always used, devs end up reimplementing stuff all the time simply because there's no easy way to know about it (documentation is only one part of this).
Although, I feel like very often, the idea of resucing boilerplate should __not__ to hide the boilerplate one layer below- it should be to __try not to write the boilerplate__ …
Take this very specific example at hand.. What is the meaning of this “JoelEvent” class? Why is it there?
It appears that is wraps a list of functions, with methods to push and pop from it. Why is it necessary to write them?
“Dispatch” reimplements function application , apparently? Btw, it is beyond me why one would loop over this.listeners and the check if the element is in this.listeners .
In a reasonable language or framework, I cannot in any way see a reason why this code needs to be written.. This is the idea of removing boilerplate, to me!
Sure, most code is boilerplate, except for that one thing, and that one thing can be anywhere. For example, let's say you want to write a function that returns the checksum of a bunch of data. That's a very common thing to do, there are plenty of libraries that do that, and I have seen the CRC32 lookup table in many places, sometimes I am the one who put it there.
Now, why rewrite such a function?
- Ignore some part of the message
- Use different constants
- Fetch data in a special way (i.e. not a file or memory buffer)
- Have some kind of a progress meter
- The library you may want to use is not available (can be for technical, legal or policy reasons)
- Some in-loop operation is needed (ex: byte swapping)
- Have a specific termination condition (ex: end-of-message marker)
- And many others, including combinations of the above
If you ignore all these points and only see the generic checksum function, yes, it is boilerplate and can be factorized. But these special cases are the reason why it may not be the case, and the reason why there are so many coding jobs.
It is also the reason why we don't have real (Lv5) self driving cars yet, why there are pilots in the cockpit, why MS Office and the like have so many "useless" features, why so many attempts to make software cleaner and simpler fail, etc...
As for legal or policy reasons, those still aren't reasons to write boilerplate code. Your reimplementation can be tight and reuse other abstractions or include their own.
In reality, few people need to write their own checksumming function, but sometimes, it is the best thing to do. And it is just an example, there are many other instance where an off the shelf solution is not appropriate because of some detail: string manipulation, parsing, data structures (especially the "intrusive" kind), etc... And since you are probably going to have several of these in your project, it will result in a lot of boilerplate. If it was so generic not to require boilerplate, it probably has been developed already and you would be working on something else.
Abstractions are almost invariably more complex, slower, more error-prone and generally worse than the direct equivalent. They are, however, reusable, that's the entire point. So one person goes through the pain of writing a nice library, and it makes life a little easier for the thousands of people who use it, generally, that's a win. But if you write an abstraction for a single use case, it is generally worse than boilerplate.
Any language with no boilerplate at all is a black box of incomprehensibility. Java has, I think, more boilerplate than average, while some other languages have less boilerplate than average.
IDEs can help with some of this, which is why I finally stopped writing all code in vim.
Observation: many comments explaining what the code is doing don't match what the code is doing after a few check-ins. E.G., I've seen variations on
//Add 1 to x
x+=2;
too many times to count.Comments that have the same content as the code but written in English will have code drift problems. But most comments aren't like this; they can provide context or explain what's happening at a higher level, ideally.
But ideally the comments should be executable, as unit tests, making you read them if and only if you break them.
For this to be a tolerable development experience, test as much as you can while keeping your tests away from slow dependencies like networking, DB, disk I/O..., and try to keep tests relevant to what you're modifying executable locally in a few seconds.
Maybe even refactor your app to have dependencies at the top, so that most code doesn't have access to them.
For the kinds of comments that answer the "why", if we could do that, we wouldn't need the actual code in the first place.
> making you read them if and only if you break them.
A good "why" comment is supposed to inform you beforehand, so you can make changes effectively and without introducing extra bugs in the process. Unit tests are more of a safety net.
// in case this is malformed, fix formatting so it will still parse
input = fixInputFormatting(input);
and had a code reviewer ask, “why are you calling fixInputFormatting”?Nothing to raise the blood pressure like a code review question that is literally answered by a comment on the line immediately preceding where they left the question.
input = fixInputFormattingIfMalformed(input);
or even if (isMalformed(input)) input = fixInputFormatting(input);Which makes me to think that the AI models should be trained on the code evolution of commit chains and not just on isolated snippets of code. That way, the AI could analyze your own commits to detect when a comment becomes outdated.
And once or twice at the top of a weird class/task file/section/..., don't be afraid of being a bit verbose and explain it until it's obvious, and then one more level. Stuff tends to be obvious while you have all the context uploaded in your mental caches - but a year down the line, it'll be rather confusing. Still, having such long comments too much in straight line code tends to make it harder to read.
AddsFiveToNum(num) {
return num - 5
}A bunch too. So I don't think comments are solely at fault. Self documenting code is only as good as the person who wrote it, and the people who approved it. Sometimes a comment is warranted, sometimes it's not.
// This code is weird. I tried doing it the obvious way, but that doesn't work because .. reasons ..
Sometimes, if the code is short, I'll even leave the old/obvious code there for future reference when I look at the weird code and say to myself:
"This is weird! Obviously it should work in this much simpler way.."
# High value orders need to be approved before refund
# Similar logic is also applied elsewhere, this is here
# as a failsafe.
if ticketValue > 500:
emailCustomerSupport(ticket)
else
refundSome programmers use comments (correctly) to explain reasoning and context, some use them to redundantly say what the code already says and some, apparently, use them to apologise.
“Don’t get suckered in by the comments-they terribly misleading. Debug only the code. Dave Storer Cedar Rapids, Iowa
Each segment of code should have a diagram, a flow model, psuedo code, and comments. More the better.
If every refactor involves redrawing a bunch of fancy ASCII art, you'll either get less refactors, or outdated comments
"If loop does something" #rev1
"If loop did do something, it now does something twice" #rev2
"If loop doesn't do something, it does something three times" #rev3
"we loop three times because we processing supervariables" #rev4
And you have an wrong illusion of documentation. You don't need ascii diagrams. Why not a scribble in a sketch book? Whiteboards and photography exist. And the method above doesn't require you too redraw. You've already got the first and last revision. Besides, during an documentation cycle of your projects life-cycle is where you update all documentation.> you'll either get less refactors, or outdated comments
If so, you're not disciplined enough. If your project is to be handed over down the line, more documentation is better than any and any documentation is better than none.
That's probably the only useful comment I've ever written.
Or in your case not to write comments at all. That's obviously a terrible idea.
I mean, I still won't want "add 1 to x" comments of course.
It's actually completely achievable with today's models to look at the comment and the code immediately after it and see how surprising it is then note the comment could be incorrect.
(I don't mean the DocBlock type comments for describing functions and class interfaces, that get compiled into docs.)
To all you downvoters: please do respond with examples of comments that are are counterexamples to what I said!
Sure, sometimes the code is right and the comment is wrong -- but sometimes the comment is right and the code is wrong, in which case the comment just saved me a lot of time.
Comments informs us what are the stuff that the programmer cares enough to write down. When we see a seemingly trivial comment, we may ask: why did they took time to write down that? Did they think there was any subtlety we aren't aware of? Or perhaps they were inexperienced with the language, to the point of having a hard time reading the code they themselves wrote? (if I put this comment on Google, will I find they copy-pasted from Stack Overflow? -- in this case, the comment may be very helpful, if only to track down that [0])
[0] but even better would be an IDE that highlighted code copy-pasted from Stack Overflow, Github repositories, etc
I agree with that, but it gets to the point where people police all the comments in a codebase deeming them unuseful. I think, especially in a huge codebase, explaining why there is a certain block of code is very helpful to transfer knowledge.
(Although two would be useful as a signal that something has gone wrong!)
But if you're using comments to explain the code, 9/10 you just wrote it in too unreadable way.
Sure, some algorithms are complex enough that some comments are needed to explain the how (that's the 1/10) but in most cases the comments should explain why, not how. So instead it should be
// Add the calibrated skew to compensate for latency
x+=2;
or whatever is the reason for the code existence.A single line of Python data analysis code is often worth 20 lines of C++. If you would be willing to add one comment per 20 lines in C++, then nearly every line of your pandas gobbledygook is worth commenting.
Terse code is good, but that doesn’t necessarily mean the comments should be terse (or absent).
We solved it by making test suites independently runnable easily.
I'd love to see a future in which we specify the constraints and the ai writes the code to fulfill them.
Obviously it will work better if there's descriptions of what the code is supposed to do (as this blog post shows!) but that's true for humans too.
I'm definitely going to try this on my code.
> a precise rule (or set of rules) specifying how to solve some problem
The only difference is the rules are adjusted (trained) over time, rather than being written down once by a programmer.
It's a general trend in AI that:
1. Some problem in the AI-domain is solved with a new method (say, barcode scanning or handwriting recognition as historical examples).
2. This new technique is referred to as AI and not algorithmic.
3. Over time the ability of AI is pushed further.
4. At some point the method shifts from being understood as AI to being considered algorithmic.
It has to use/learn data within a defined scope, so even with dozens of algos, they are all well within defined ranges of variables.
Still within reason to be called well written software that performs the tasks that it was designed to accomplish.
Since there isn't a plain sequence of steps that can be followed to explain the output, i'd say a different term is justified. Whether you call that "intelligence" is debatable.
There is a sequence of steps, it's just very long and unintelligible to humans.
I'm not even sure I disagree with you: Quantity has a quality of its own.
You wrote the code that tries random directions, but you are not choosing which directions it takes when executed.
When thinking about this it's a bit like if I submit an application on fiver to write code for me that takes me from A to Z. I get the code back, I don't understand it and I didn't write it, is it still an algorithm? All of the same can be applied to a neural network.
Is natural selection an algorithm?
Is how the universe works an algorithm?
I think it's useful in every day life to distinguish what humans do and what something out of human control does. It can also be useful to be a bit philosophical and lump definitions together, but this only works if everyone agrees that they're doing this in a discussion.
(Because once we do know how to do it, it gets a more specific name like "machine learning")
Past questions: is it really compressive sensing? Is it really superresolution? Is it really expectation maximization?
My experience is it’s typically 50-50.
- if B was added after A, it will not receive the current event
- if A was added after B, then B has already received the event by the time it is removed.
If I encountered that during a code-review, I would be very suspicious.
The right thing to do here is probably the more simple thing that the AI also seems to be suggesting :
for (const listener of this.listeners) listener(arg);Object A emits events.
Object B subscribes to A, and manages the lifetime of Object C
Object C subscribes to A in its constructor, and unsubscribes in its dispose function.
With the common event listener model, Object C could have its methods called after dispose was called, even though it appeared to clean up its event listener!
You can check out the DOM spec for the correct behavior, which both clones the event listeners arrray and sets a removed flag on them in case they were removed after cloning.
I disagree. Most bugs in code are entirely valid code. They're things where the developer has written great code that does the wrong thing. Those bugs will be impossible to catch with AI until the AI can understand the requirements, and that can't happen if the requirements aren't clear, and unclear requirements are the source of the bug in the first place. That can't be solved with AI. AI can only ever be as good as the input data. In software development the input data is usually a pile of crap.
If you choose to defer to AI rather than think about the code you write then you will write buggy code, but the AI won't tell you it's buggy because it'll look fine.
The way to build high quality software is to build things with thought and rigour, with good processes like analysing requirements and building tests to cover what the requirements state the code should do.
1. I write code
2. Copilot gives me good suggestions about potential improvements on my code
3. I asses the suggestions
4. Back to 1
My understanding was you just give copilot stubs of code and it auto-completes.
At Codium.ai, we are trying to tackle this problem. We are developing a new code integrity product that intends to do something quite similar. Codium will mark problematic parts of the code (a.k.a bugs), via auto-generated tests, that isn't inline with the developer's intent. The tricky part is to have high accuracy. We don't want to annoy any folks with false positives.
We would love to get your feedback about what we are working on! We are developers with ML background, excited about exploiting ML for software development, so developers can code fast with confidence.
I think this website is in dire need of a <noscript> warning somewhere.
Too bad it's exactly what we need to progress the field.
TLDR; printing press copied book that could have been trivially transcribed by hand.
TLDR; farm produced food that could have been trivially foraged.
Scenarios in your examples are beneficial because of increased amount of
items moved by steam engine
books printed by press
food farmed
We don't need more code, we need better code.The driving case is in the comment: "Check that the listener is still there in case one is removed during dispatch".
The failing TDD test could use a this.listeners with a single listener, where that listener is not present. Or two listeners where the first is present and the second is not. (Or a list of any size where the last listeners are not present.)
That would drive the "if (!this.listeners.has(listener))" test, but not be enough to drive the choice of "continue", "break", or "return" - all of which would make that failing test go green.
How would you handle this situation in classic TDD? Indeed, one of my complaints about classic TDD is that it doesn't do enough testing away from the happy path of meeting the developer's preconceptions.
BTW, this is one of those cases where 100% statement coverage isn't enough - nor 100% branch coverage.
Mutation testing could detect it, by mutating the "break" to a "continue" and complaining when all the tests still pass. I have yet to use mutation testing in my projects.
While people argue (incorrectly IMO) that TDD naturally results in 100% statement coverage, I've never seen a TDD advocate argue that it's naturally results in code which correctly identifies all mutations.
oldListeners
.stream()
.filter(newListeners::contains)
.forEach(l -> l.accept(args));... is the "AI" actually just a markov chain?
You generate text by sampling from that likelihood distribution.