Hello!
I just had a Mini-Idea for Nim I'd like to discuss.
I really enjoy using let for everything as in
let foo = computeFoo()
foo = "foo" # compiler error
because I know the compiler will help me to avoid all sorts of bugs. Old happy common story.
But every once and a while I need to spread the assignment of an otherwise not-to-be-changed value over several lines and I end up using var.
var foo = initFoo()
computeFoo(foo)
foo = "foo" # whoops, silent bug
This is always slightly annoying me. And while I have long chucked my usage of Ruby on to the dung pile, I have kept their idea that I should be happy! Therefore, since I am worthy of programming while not under constant slight irritation, I must find a way to "let" a variable.
Of course I could do this:
var tmp = initFoo()
computeFoo(tmp)
let foo = tmp
foo = "foo" # compiler error, good
var tmp = initBar() # whoops, unwanted compiler error, scope pollution
The best I came up with is this:
let foo = block:
var tmp = initFoo()
computeFoo(tmp)
foo = "foo" # compiler error, good
# pretty good but a bit heavy
To those who are concerned with such things, is this how you are doing it?
What I would really like would be this:
var foo = initFoo()
computeFoo(foo)
let foo # proposed let usage: without argument, on previously declared as var, prevents future modification
That's a lot less mental overhead both to write and read. I am going to be a clueless end user here and suggest that it might be reasonably easy to implement in the compiler as well.
I would love to hear about this idea. Preferably that someone else had it first and it's already beeing worked on ;) But really any thoughts about it.
You can also do something like this:
let foo = (var tmp = initFoo(); computeFoo(tmp))
foo = "foo" # compiler error, good
Depending on the situation it might fit better.
I use this pattern a lot, although @dom96 either forgot to return tmp in his expression, or suggests computeFoo returns, and not just modifies its argument, in which case you don't need the tmp at all.
So, there's two cases:
let foo = (var tmp = initFoo(); frombnicate(tmp); tmp) # in-place frobnicate
let foo = frobnicate(initFoo()) # frobnicate returns a value
Which shows (and confirms my experience), that the reason this pattern is so frequent and annoying, is that there's a considerable amount (may be just too many) of procs which modify a variable in-place, instead of returning the modified value. The main reason for this, as far as I understand, is performance. However, in case your proc consumes your argument (i.e. it's being move d in), compiler should perform the required optimisations and produce the same code. On the other hand, procedures (in the classic Pascal meaning of statement subroutines) prevent chaining and unnecessarily grow code vertically, in comparison to functions.
I use this pattern a lot, although @dom96 either forgot to return tmp in his expression, or suggests computeFoo returns, and not just modifies its argument
For what it's worth I just followed @cmc's block example and translated it into an expression instead of a block statement. In that example it only makes sense for computeFoo to return a value.
Well a similar hack can be done in that case as well but it's still dependent on scopes.
import std/[macros, genasts]
macro immut(n: typed) =
if n.kind notin {nnkSym, nnkHiddenDeref}:
error("Expected a variable", n)
let
(n, name) =
if n.kind == nnkHiddenDeref:
(n[0], ident($n[0]))
else:
(n, ident($n))
typ = block:
let typ = n.getType
if typ[0].eqident"var":
typ[^1]
else:
typ
result =
genast(name, n, typ):
template name(): typ = cast[typ](n)
proc doThing(i: var int) =
immut i
i = i + 2
var a = 100
doThing(a)
That's awesome! Played around with it, and it can be as simple as this:
https://play.nim-lang.org/#ix=3ZjH
import std / [macros, genasts]
macro immut(n: typed) =
result = genAst(name = ident(n.repr), n):
let name = n
proc doThing(i: var int) =
immut i
i = i + 1
echo i+i
var a = 100
doThing(a)
Nevermind, you can shadow a var in a proc, but you can't do it at the top level.j
proc test(i: var int) =
let i = i # works in a proc
#i = i + 1
echo i
var a = 100
test(a)
let a = a # doesn't work here
Using a view https://nim-lang.github.io/Nim/manual_experimental.html#view-types the generated code boils down to one pointer copy.
import std / [macros, genasts]
{.experimental: "views"}
macro immut(n: typed) =
if n.kind notin {nnkSym, nnkHiddenDeref}:
error("Expected a variable", n)
let
(n, name) =
if n.kind == nnkHiddenDeref:
(n[0], ident($n[0]))
else:
(n, ident($n))
typ = block:
let typ = n.getType
if typ[0].eqident"var":
typ[^1]
else:
typ
result = genast(typ, name = ident(n.repr), n):
let name: lent typ = n
proc doThing(i: var int) =
immut i
echo i
N_LIB_PRIVATE N_NIMCALL(void, doThing__immut_49)(NI* i) {
NI* i_2;
tyArray__nHXaesL0DJZHyVS07ARPRA T1_;
i_2 = i;
nimZeroMem((void*)T1_, sizeof(tyArray__nHXaesL0DJZHyVS07ARPRA));
T1_[0] = dollar___systemZdollars_3((*i_2));
echoBinSafe(T1_, 1);
}
I had to google for this, but at least in C casting in this case probably doesn't have any runtime cost because the fundamental type isn't changing. It's hard to say for sure without looking at the assembly.