I've noticed that the Uninit warning is on by default in devel. I understand it's just a warning, but since it is on, I assume nim devs want to steer users away from certain code patterns. However I'm a bit confused which alternatives should be used for APIs like
var x: SomePayload
if ch.tryRecv(x):
# do stuff
Or when working with arrays
var cornersOnCrop: array[4, array[2, float]]
for i in 0..<4:
cornersOnCrop[i][0] = fx*(obj.corners[i][0] - xmin)
cornersOnCrop[i][1] = fy*(obj.corners[i][1] - ymin)
Pushing and popping warning:Uninit:off adds clutter and is annoying to do. Assigning some dummy value is actually working against clarity, as it is completely overwritten. What do?
This is the idiom to use:
var cornersOnCrop = default(array[4, array[2, float]])
If speed is a concern, use .uninit. However, you didn't get the speed before either as the compiler injected the default call for you.
Pushing and popping warning:Uninit:off adds clutter and is annoying to do. Assigning some dummy value is actually working against clarity, as it is completely overwritten. What do?
It may not be obvious to old timers, but the =default(..) scales to multiple variables such as var a, b, c = default(int).
Instead of "per site" which is in both annoying but also specific, you can also just put {.warning[Uninit]:off warning[ProveInit]:off.} at the top of your module or in your nim.cfg (module/project/user/whatever). That is what I'm doing as I already made the mistake of adapting a bunch of code to BareExcept/CatchableError warnings. This makes even more false positives for me than that did. Unfortunately, the way generics & templates are instantiated, warnings will still fire in client code unless they also do that.
I am just one vote, but I think they should both move to verbosity:2 rather than functioning as stealth advertising for the new let x: Type; #[ assign x eventually ]# feature. ProveInit doesn't even analyze the result.every = 1; result.field = 2 situation (very explicit) correctly and the warning text sounds like the worst-of-the-worst mistakes in C. People can always turn them on surgically.
As a more quantitative argument for my verbosity:2 vote: I may have missed it, and I don't know if he kept a journal of if his strictdefs fixes were "real" or not, but I didn't even see a single real bug in many dozens of fixes xflywind/ringabout recently made to the stdlib. Applying https://en.wikipedia.org/wiki/Rule_of_three_(statistics) , I estimate for my own code a true-positive rate at something like below 1/300 in my own code (So, over 99.7% false) which in my view is far too hair trigger to justify being in verbosity:1. Maybe I am just more used to newer prog.lang's initting by default or inittin'g to zero in general. If you find any real bugs you can use https://github.com/c-blake/spfun/blob/main/spfun/binom.nim to estimate a binomial proportion.
Personally, I think any warning/lint message with community realizations of false positives over 50% is too chatty to be in verbosity:1. These two new ones are probably over 99%. I realize that estimate is code-base-biased and any false positive rate threshold is subjective. I know people who always compile with gcc -Wall -pedantic -ansic -Wextra -Werror, but even for that compiler, it's hardly the default. I genuinely doubt these two new warnings do not come within an order of magnitude of that for "most" Nim users, though. I also disagree the style is more clear than simply knowing "vars init by default to default" (lol - "for default", j/k w/apologies to Abe Lincoln), especially since in Nim one can, theoretically, control what that default even is (a rare/nice superpower). I think as more of the nimbleverse hits this, more will complain.
TL;DR - Please let's not make nim c foo.nim with default configs behave like gcc -Wgo-crazy foo.c.
FWIW, all missing initialization bugs I've written were combined with the use of implicit result - i.e. forgetting to return something in some branch. It's especially awful when you use sum types for error handling, a la nim-results:
proc flushCurlyExpand(ctx: var UnquoteContext, word: string):
Result[void, string] =
case ctx.subChar
of '-':
# [long case statement...]
ctx.subChar = '\0'
ctx.hasColon = false
# whoops, forgot to return ok(). the code would always return an empty
# error string.
In contrast, implicit zero-initialization for "var" seems safe enough. By now I've adjusted most of my code to adhere to the new rules, but pretty much all implicit inits I've replaced looked like:
var found: bool
for it in s:
if it == needle:
found = true
break
if found:
# [...]
# or
var s: seq[int]
for it in xs:
if it > 3:
s.add(it)
# etc.
i.e. I hadn't initialized the variables because I knew the compiler would do it for me.
So at least for my code, a rule like "implicit result must be explicitly initialized" would have been enough to prevent bugs, and porting existing code would have taken much less effort.
Even if what you say is true, I think there is nothing particularly stylish about:
var s: seq[int]
for it in xs:
if it > 3:
s.add(it)
It's just not worth the inconsistency when we require result to be initialized explicitly.
I have always assumed Nim zero-inits values, so I'm not sure what the difference is between this
var cornersOnCrop: array[4, array[2, float]]
and
var cornersOnCrop = default(array[4, array[2, float]])
other than more typing? Is it just a new preference for explicitness?
What are these bugs people are finding? I am not sure how explicitly setting an var i = default(int) will prevent bugs. Is this a different facet of the nil problem people have, or is this new check more for structs / objects?
If I do var s = newSeqint do I need to iterate through 0..9 and set them to default(int)? If not, why is that implicit but the initial var s: seq[int] not and must be var s = default(seq[int])?
I assume this implies we will now be expected to explicitly init result even for simple value types that zero-init as expected?
Is it expected that we explicitly init result even if it is not used? I'm thinking / hoping not but I will need to test if that is allowed to be skipped.
Honestly don't reply, I think I'm just resistant to a new change I don't initially like. I'll get over it!
I’m coming to this discussion a bit late but I think @c-blake and @Vindaar made some excellent points. I thought that nim’s variables were zeroed out by default so this feels a bit of a syntax change. This feeling is reinforced by the fact that now the cases in which you could use the “traditional” var thename: thetype (without a value) syntax would become rare. Plus I don’t love the way the code looks when you use the warning “fix”. It seems quite verbose and duplicated to me. Maybe if you could just do var thename: thetype = default() (without repeating the type) in all cases it would be fine.
Wouldn’t it be possible to make var thename: thetype be equivalent to the “fix syntax” and have users explicitly mark variables as “uninit” if that’s what they want? And if in some cases the two syntaxes are not equivalent then nim could use the new heuristic to display a warning or an error. Or maybe this could be an opt-in check that users can enable to check when there are unintended uninitialized variables.
Anyway, as a small data point, a couple of days ago (before I knew about this small controversy) I updated to nim level and compiled an existing nim program that uses Arraymancer and which compiles with no warnings on nim 2.2.0. When I recompiled I got a lot of new warnings, which was puzzled me a bit.
Part of the problem is that the same warnings (on the same lines) were repeated over and over again. Perhaps nim could avoid repeating the same exact warnings multiple times?
In any case adding so many new warnings to the whole nimbleverse seems a bit counterproductive. I tend to judge (perhaps unfairly) the quality of a library by the number of warnings I get from it. I think others do the same. IMHO this would make the (relatively small) nim ecosystem look less refined than it really is.
In my opinion what’s most annoying about this is that you get warnings from code that you don’t own and thus cannot do anything about it. If it only applied to your _own code it might be fine (specially if the syntax were improved). Would it be possible to only show these warnings for your own code, and not for library code that you import from libraries that you installed via nimble or atlas?
I recall there was an intent to change not nil feature to or nil, which implies that refs and ptrs types are never nil by default, but one can annotate them with or nil to opt out. What has happened to this idea?
IMO it would solve the argument in this forum thread in a much better way, arguably requiring less changes in existing code. For the most lazy of us it would be smth like
s/(.*) = ref (.*)/\1 = ref \2 or nil/g
The regex above is untested pseudocode to just get the point across :) so relax, we'll not butcher everybody's code in a sterile quest for purity
I'd like to relax, but this conversation doesn't make me feel very relaxed. It should be uncontentious that initializing result and any var is much more frequent than varargs. I (and evidently at least a few others) consider working around a hard error ProveInit-Uninit "butchering", but that's ultimately pretty subjective. "Purity" here has already been brought up Re: func (by @Araq & @Guzba). I don't think programmers in the 2020s expect garbage initialization as @Vindaar mentioned.
How to decide "sterility"? "cost" - "benefit", of course. False positive rates are not "meaningless". At the least, they reflect cost to programmers to adapt which is why it has been some focus here. They also crudely measure popularity of syntax being phased out, at least if the alternative was always available which it more|less has been. I also tried already to help make non-ptr|ref-bug power "benefit" assessment "more" meaningful { 4th non-quoting paragraph } for "well tested code", though I grant automating that would need a lot of NLP. Even rough work, though, would still be a better idea than non-quantitative evidence invisible to others which is unconvincing by construction.
For me, I was never confused about zero/empty-init a la @nrk & @Guzba, get no bug power a la @yglukov & @Vindaar & @xrfez1, don't like the new syntax like at least @yglukov & @didlybom, and adapting is 100s of places (and seems needed for any lib I might dream others may want to use) - lose, lose, lose, lose - (all of the above, though admittedly the first two are about the same and sorry if I left anyone out & @planetis is a bit more pro-bug-power/syntax-flexible-minded).
As for the last, sure - I can add a nim.cfg with legacy=noStrictDefs, but that doesn't now (and may never?) help client programs for library code as @didlybom observes. All clients must also do it (maybe with a side condition of uses any generics/template/macros). Same for warnings as said by me & him. Maybe besides "type bound operations" Nim also needs "type bound compiler directives"? Not sure if that would solve it or help other problems or even be remotely possible. Just brainstorming to try to be helpful.
Also, @yglukov, I also raised that in case Araq's brief reply helps at all. I didn't fully follow. I think auto-init result is great with ready-to-go types and I also think those are the best types. The present approach makes them harder to use and then even spills over into all var decl syntax.
Anyway, I look forward to the promised documentation providing some assessment of the non-purity benefit that doesn't involve garbage-init expectations or invisible evidence of pointer-only examples.
Well at this point I cannot imagine writing an article that will you make your mind. So ok, you win, the feature is dead:
https://github.com/nim-lang/RFCs/issues/556
Maybe we will introduce a module-wide {.edition: 2025.} switch collecting these things but existing code can be left untouched.
Maybe it wasn't obvious from my comments, but at least for result I find strictDefs very useful, without a distinction for ref/ptr. If you look at my first example (a real bug), then you see it's a value type too.
I've also encountered cases like
proc foo() =
# [blah...]
if x:
return # early return
# [...blah]
# then I add a return value
proc foo(): string =
# [blah...]
if x:
return # whoops, forgot about this case, and the compiler didn't tell me
# [...blah]
return "blah"
# Or just
proc bar(): string =
if x:
if y:
if z:
return "xyz"
else:
return "abc"
else:
return "x"
# what if xy?
My defense against the first issue is to make procs small; against the second one, to always structure returns such that every indentation level ends with one. But these are just imperfect workarounds to a very real footgun in Nim, which is implicit result + default zero init.
Also note that requiresInit isn't quite the same as strict defs either, and that object default init currently has footguns:
type A = object
a: int
b: int = 2
type B {.requiresInit.} = object
a: int
b: int = 2
var x: A # strictDefs complains
assert x.b == 0 # wait what?
var x = A() # strictDefs doesn't mind
assert x.a == 0 and x.b == 2 # OK
var x: B # requiresInit complains
var x = B() # requiresInit complains
var x = B(a: 0) # OK
Not sure what all the fuss is about, result is mostly useless anyway ;)
Here's a fun style to get the equivalent of all this complex and fancy init analysis, without the actual feature - as a bonus it also protects against another common source of bugs, namely that of initializing twice by accident (which, just like initializing not at all, is bad):
proc f(b: bool): int = return block:
if b:
53
else:
42
Regarding requiresInit, we've tried to use it on occasion but it is poorly integrated with the rest of the language leading to a signifificantly sub-par experience - for example, it's really difficult to combine it with Table since Table has no API that caters to it, which blocks its use beyond toy examples - this in turn is due to the lack of solid "singe-owner" and "move-only" types and a lot of nim code assuming that zero-init is always valid.
It was a nice start and nice idea, but 80% of the work is left todo to integrate it with the rest of the language and std lib before it actually can be used.
How is this better than
proc f(b: bool): int =
if b:
53
else:
42
? How is this better than
It forces you to write an expression after block no matter what - ie as long as return block: stays there, you can't by accident refactor the function into something that doesn't return a value.