Nim Programming Language v0.15.0 released
nim-lang.org
nim-lang.org
We also have a BountySource campaign[3], so if you would like to support us please consider making a donation.
Finally, please feel free to AMA!
1 - https://manning.com/books/nim-in-action?a_aid=niminaction&a_...
2 - https://manning.com/books/nim-in-action?a_aid=niminaction&a_...
Also how does the gc trace variables on the stack? How do you determine if a value is a pointer to an object or an integer?
As far as I know, variables are only traced if their type is `ref`. So the type system helps the GC a lot here I think.
Since version 0.14 (iirc) however, Nim uses a traditional mark&sweep backup GC to collect cycles. Reasons: Trial deletion in practice is much slower.
For all versions of the GC is stack is scanned conservatively as precise stack scanning is simply too expensive when C is your main target.
But to ask a pointed question, doesn't that mean Nim gets the worst of both worlds? You have both the overhead of updating reference counts and the (relatively long) garbage collection pauses. I guess if the programmer codes in such a way that no cyclic garbage is created it is not a problem because the gc will never be triggered. But how common is that in practice? Does the language make it easy to avoid cycles?
There's a patch of the collector to make the M&S incremental, to avoid pauses for this step too. Of course, whether a deferred RC'ing GC with incremental cycle collector works better in practice than a more conventional generational incremental GC (or just an incremental GC) is an open question. :-)
Nim has garbage collected pointers (ref) and raw pointers (ptr). You can use 'ptr' to break up cycles manually and disable the M&S. I suppose it's very much comparable to Swift except that Nim's RC overhead is much lower since it's deferred RC.
This is great! There are a lot of little things like this in the nim standard library that make life a lot easier. I should write more nim.
but what is the return type? The multisync example shows separate generated methods that return `string` and `Future[string]` depending on the method argument.
If that's the case then blocking and non-blocking operations need to be treated separately since operations defined on a result of `Future[string]` are not the same as those available on `string`.
HKTs not only allow for both method argument and return type to be polymorphic, but also allow one to define the same operations (like map, flatMap/bind, fold, etc.) on the wrapped type.
Seems like Nim is the answer :)
Are Ada's range types similar to D's, or quite different?
https://dlang.org/phobos/std_range.html
The above page links to the one below - the one below is about the concepts, by Andrei Alexandrescu (who joined Walter Bright in developing D).
However, once I started a reasonably sized project, getting Nim to compile it became difficult. Lots of small little quirks that weren't greatly documented.
However, when it did build, it was reasonably fast, and allowed me to tweak the C code when I disagreed with the compiler.
Macros were still getting developed, last I played with Nim, but I'm not sure comparing with Lisp is fair.
I adore Lisp, and its macros. (Both CL style and Scheme).
But most languages are designed to constrain the programmer to a way of thinking, a series of patterns.
Lisp was the opposite.
Even in languages that try to give the programmer flexibility, they miss the power of Lisp's homoiconity.
That being said, I think Nim is trying to find a middle ground between Python's "only one way to do it" and Lisp's "get out of the programmers way".
Its a decent language, albeit with a tiny, rather opinionated, development team.
1 - https://github.com/nim-lang/Nim/issues
2 - irc.freenode.net #nim
Snappy responses to Github Issues, with really clear, really polite responses. Even when I was an idiot.
For instance?
> But most languages are designed to constrain the programmer to a way of thinking, a series of patterns. Lisp was the opposite.
Lisp is still the opposite. Exactly this is the problem of Lisp: It is too powerful. I admire Lisp for its power, and there actually is no other language with such a freedom. Everyone can write his own DSL to simplify his task. Writing DSLs is extremely easy in Lisp.
However, this doesn't profit maintainability, and it makes sharing code with others difficult. You actually need a lot of discipline that you code in such a way that it remains maintainable. In teams you need to constrain the way of thinking otherwise you can quickly run into chaos.
Java is the opposite of Lisp. You cannot code productively in Java unless you use a sophisticated framework. Such frameworks force you to a certain style of coding which profits maintainability. That is one factor which explains the huge success of Java in business.
Nim and Rust combine both paradigms. They provide a certain degree of freedom, and they require a certain degree of discipline. Haskell requires an extreme amount of discipline. In case of Haskell's freedom I am not convinced.
> For instance?
Nimble file naming [0]. It must end in '.nimble', and must have at least character in front of that suffix, but the prefix doesn't actually matter.
I'm not convinced on the maintainability argument. Many, many projects have a coding style guide. LISPs require one too. Once you have one, maintaining the code becomes as difficult as any other language. I've encountered unmaintainable Python code [1], and insanely well documented Scheme code [2]. I'm not sure the language has much to do with it, apart from supporting a wide range of paradigms and patterns.
I'm not sure black-boxing through use of a sophisticated framework is any better than the way you work in more flexible languages.
Here's my take:
* Nim is great. The use of the 'auto' keyword let me be productive using it.
* Scheme is great. It lets me do insane things, and the compiler will optimise it to work, and well. (Rather than breaking three similar function into pieces, I can make one function that generates all three functions using their commonalities).
* Java is the smart kid at college who always answers questions in the form of a thesis. He isn't wrong, but you don't really remember what he was saying.
[0] https://github.com/nim-lang/nimble/issues/166
[1] Hence why things like this exist: http://docs.python-guide.org/en/latest/writing/structure/
[2] http://cvs.savannah.gnu.org/viewvc/slib/slib/alist.scm?revis...
It does now. The new NimScript .nimble format[1] uses this prefix to determine the name of the package.
1 - https://github.com/nim-lang/nimble#the-new-nimscript-format
Does it? With the type system guiding me if feels like it requires less discipline.
As far as freedom is concerned, I can't offhand think of something I'd want to do but can't in Haskell.
It's easy to add code in most languages, even for unexpected features which were not considered at first place. In Haskell it can be painful to do that.
I sort of find the idea refreshing myself. As a full time Clojure developer, I consider Nim to be the closest thing I can find to what I want for a great experience coding closer to the bare metal.
Nim developed from the bottom up, being closely related to C which is nice and makes porting to other platforms easy. The Nim development team adds only features which really make sense. If they just remove those immature ugly features like strong spaces and the redundancy of underscores then Nim 2.0 could be a really great language.
Obviously this is not only my problem because there are only sparely applications written in Haskell. It is possible to write real world applications in Haskell, as proven by Leksah and the window manager xmonad. However, would you write an MS Office clone in Haskell? I wouldn't. I am still honestly interested in Haskell but I cannot understand why the cabal hell has not been fixed yet. Sandboxes and VMs are no option for me. Rust, Nim, and many other languages don't have this problem which points out that the cabal hell is due to Haskell's nature.
Rust is another story. It is way more practical than Haskell, and I would use it for safety and systems programming. Nim however has become my favorite after a long journey of languages because I am really productive with it. Only Lisp has a similar productivity, and sometimes I still use it. Nim has the advantage of being very close to C which makes porting to other platforms extremely easy -- and hence also all my Nim code.
However, there are three points which annoy me. First, the Rust compiler is huge (LLVM based). Second, it requires a native Rust compiler for bootstrap. Third, it doesn't compile to C which makes porting to other platforms difficult. Nim doesn't have these problems. It is small, self-hosting, and easily portable.
Not compiling to C is a feature. It insulates us from the undefined behavior of C (signed overflow never results in UB in Rust, for example) and allows us to actually get proper debug info.
(I agree that it's a tradeoff generally, I'm just not sure this example is particularly motivating.)
No, because the interface is
int stat(const char *path, struct stat *buf);?
(Which is how stuff like https://docs.rs/libc/0.2.16/libc/fn.stat.html is built)
I still think that, given Cargo, the one-time cost makes it worth it, but after seeing my error, think that the point makes more sense. Thank you for being patient. :)
Furthermore, if your frontend doesn't know about structure layout then you forgo some important systems language features. For example, you can't implement sizeof as a compile time constant.
Sure and you only need to call libclang for each different platform. Or is it on every platform? ;-)
Strong spaces are already gone.
I thought Lisp code was its own AST, although I don't really know much about Lisp.
That's wrong. Nim has both macros and templates.
> Lisp macros are not necessarily shorter, but it's far easier to see what's going on.
Even Lisp macros are not perfect. You always have to care for new symbol names where the lexical scope could be affected.
As for hygene problems, Nim's system has the same issues.
Here's an example of the problem:
This is a simple Nim macro from the tutorial:
macro debug(n: varargs[expr]): stmt =
result = newNimNode(nnkStmtList, n)
for x in n:
result.add(newCall("write", newIdentNode("stdout"), toStrLit(x)))
result.add(newCall("write", newIdentNode("stdout"), newStrLitNode(": ")))
result.add(newCall("writeLine", newIdentNode("stdout"), x))
What the heck is that doing? Now let's compare to my lisp of choice, Chicken Scheme (although this macro would be much the same in any Lisp with imperative macros) (define-syntax debug
(ir-macro-transformer
(lambda (x i c)
(let ((exprs (cdr x)))
(cons 'begin
(map (lambda (expr)
`(begin
(write (quote ,expr))
(display " : ")
(display ,expr)
(display "\n")))
exprs))))))
Now see, that's much clearer. And before you ask, yes, this macro is completely hygenic, because it uses ir-macro-transformer. Don't worry too much about it if you're not familliar with Chicken Scheme. It's not the important part.You can write the same macro with less verbosity:
macro debug(n: varargs[typed]): typed =
result = newNimNode(nnkStmtList, n)
for x in n:
let xRepr = toStrLit(x)
result.add(quote do: writeLine(stdout, `xRepr` & ": " & $`x`))It seems Nim has a code templating system, which is nice, but I find a bit more confusing and less pleasant than the Lisp equivalent. As I said before, it just feels clumsy, especially compared to Lisp.
(defmacro debug-it (&body expressions)
`(progn
,@(mapcar (lambda (expression)
`(format t "~%~a: ~a" ',expression ,expression))
expressions)))
Example: CL-USER 8 > (pprint (macroexpand-1 '(debug-it (+ 1 2) (* 3 4))))
(PROGN
(FORMAT T "~%~a: ~a" '(+ 1 2) (+ 1 2))
(FORMAT T "~%~a: ~a" '(* 3 4) (* 3 4)))
CL-USER 9 > (debug-it (+ 1 2) (* 3 4))
(+ 1 2): 3
(* 3 4): 12
NILThere is one key difference between my code and yours: in mine, the functions, values, and symbols referenced in the macro are automatically renamed, thus guaranteeing no namespacing conflicts. Due to the way the CL package system works (IIRC), CL provides almost the same guarantees, at least in this case. But it is an important semantic difference.
And it is the Common Lisp. Despite how you may feel, Scheme, Clojure, PicoLisp, NewLisp, Racket, Interlisp, LeLisp, EuLisp, and others are as much a Lisp as CL.
> and the splicing unquote is less efficient than cons in this case
Not sure why that should be the case and why it even matters.
AFAICT, splicing unquote has to traverse the entire result list. Not super relevant in this case.
I don't know why I picked cons, honestly. I guess it just fit ny mental model of what was happening better.
Especially since it is usually done once at compile time.
> Some Lisps don't even have them.
Even most Scheme implementations have them.
And yes, most schemes have them. I was talking about Picolisp, Newlisp, and Kernel, which don't.
My five MIPS Lisp Machine did not care. My Lisp implementations on a several hundred times faster processor don't care either.
These languages you list don't have macros.
And I didn't say it was a legitimate reason to use cons. I said it technically had better performance. As I stated above, the real reason I used it is because it fit my mental model of what was happening.
Trivially.
Anyway, I don't consider them to be Lisp dialects anyway. They are new languages, Scheme dialects, scripting languages with parentheses, whatever. The name 'new'LISP says it already.
> I said it technically had better performance.
You thought it had, without actually knowing it.
CL-USER 36 > (let* ((bar '(1 2 3))
(baz `(foo ,@bar)))
(eq bar (rest baz)))
T
So it's not copied and no traverse is needed.What actually is traversing the code is your IR-macro mechanism. Twice. -> ir-macro-transformer. Which makes it slower both in the interpreter and the compiler.
https://github.com/bnoordhuis/chicken-core/blob/master/expan...
Traverses the code, then calls the handler, traverses it again...
Btw., the code won't win any beauty contests.
It's good to know that splicing unquote optimizes for that case in Lisp. I wasn't sure, and so assumed the general case. In any case, that's not the reason I didn't use it, as I've explained multiple times now.
Yes, I know ir-macro does code traversal. Yes, sc-macros are more elegant. But that's the mechanism we picked, And I have no issue with it. Furthermore, it doesn't traverse all the same code twice. It traverses the inputs and the outputs.
>Btw., the code won't win any beauty contests.
...Says the CL user. Actually, I agree, it won't. But it works, it's reasonably clear, and it doesn't do anything obviously stupid. It's okay.
...Unless you're talking about my code. You want me to clean that up? Okay. I will.
(define-syntax debug
(ir-macro-transformer
(lambda (x i c)
(let ((exprs (cdr x)))
`(begin
,@(map (lambda (expr)
`(printf "~A: ~A~%" ,'expr ,expr))
exprs))))))
We still have to bind our args by hand, because it's Scheme, but it's not too bad.Calling them to be Lisp is wrong.
> Not considering them to be Lisp is ridiculous. Picolisp is closer to Lisp 1.5 than CL is.
How so? CL runs a lot Lisp 1.5 code unchanged.
Picolisp not. The Picolisp evaluator is not compatible with Lisp 1.5. It doesn't even have LAMBDA.
>CL took many ideas from Scheme, and vice versa.
Sure not. The main idea CL took from Scheme was 'lexical binding by default'. Other than that the Scheme influence was minor.
CL is based on Lisp Machine Lisp, NIL, S1 Lisp and Spice Lisp. All coming from Maclisp, which was developed out of Lisp 1.5.
> The claim that CL is The One True Lisp is tenuous at best, and absurdly ridiculous at worst.
I never said that. But it is the most widely used Lisp, and the one I mostly use - minus some minor use of Emacs Lisp.
> It's good to know that splicing unquote optimizes for that case in Lisp. I wasn't sure, and so assumed the general case.
You could have looked it up or tried it, before claiming it. I did it for you.
> Yes, I know ir-macro does code traversal. Yes, sc-macros are more elegant. But that's the mechanism we picked, And I have no issue with it. Furthermore, it doesn't traverse all the same code twice. It traverses the inputs and the outputs.
Yeah, but claiming that the CL code was less efficient. Great move.
I looked that up from the Chicken Scheme sources, to actually see what it does. It traverses inputs and outputs during macro execution. You could have mentioned that.
Sorry, I don't trust your judgements, your claims are simply not backed up by the source and how things actually work.
> Says the CL user
I can't remember seeing such ugly code for macro expansion in a CL implementation.
Take make-er/ir-transformer . That function code is fully obfuscated. It bundles several utility functions, which don't belong there as sub-functions. Some are using access to lexical variables defined several dozen lines above, others don't. The result code is over hundred lines long, even though the basic mechanism could be written down much more compact. Each subfunction can only be understood by referring to the surrounding code, which is above or below. Then it takes a parameter for using two different expansion mechanism. From that, two new closures are created, which then are given to the user in, again, two differently named versions. The code itself contains lots of debug code, which simply outputs intermediate results, and which will overwhelm any human user for any non-trivial macro expansion.
However, it keeps a lot of ideas from 1.5 that were later dropped by CL and others. Names don't matter: ideas do.
>You could have looked it up or tried it, before claiming it. I did it for you.
It wasn't entirely relevant to the present situation, until I mentioned that I thought cons was more performant. Thanks for trying it. I don't have an excuse, but thanks.
>Yeah, but claiming that the CL code was less efficient. Great move.
I didn't. I said that splicing unquote had to traverse the resultant list, making it slower than cons, which was pretty much irrelevant in this case. I then explained the real reason I used cons.
>I looked that up from the Chicken Scheme sources, to actually see what it does. It traverses inputs and outputs during macro execution. You could have mentioned that.
Quite honestly, I didn't see how it was relevant. We weren't discussing macro system internals until just now. It's not great, but it gets the job done, and that wasn't the point.
I'm starting to get really really frustrated here. You seem to miss every point I make, to the point that I'm very nearly wondering if it's deliberate.
That's what I say: vague ideas don't matter much when forming language families. Code does. Books. Libraries. Communities.
What were those ideas that were dropped? Fexprs would be one. That was dropped when compilers were used and Fexprs were found not to be compilable. That happened in the 70s before CL existed. Pitman published his paper on macros in 1980, which summarized the view of the Maclisp / LML developers. What else?
The Lisp 1.5 manual gives an extended example: the Wang algorithm.
The code still runs in Common Lisp.
http://www.informatimago.com/develop/lisp/com/informatimago/...
What were the 'ideas' that were dropped, even though somehow old code still runs?
> We weren't discussing macro system internals until just now. It's not great, but it gets the job done, and that wasn't the point.
The point was, claiming a 'slower compilation process' due to splicing backquote usage, while in fact the whole compilation of the example you gave was the really slower one, because use used a slower macro system which traverses code for renaming and re-renaming.
> You seem to miss every point I make
EVERY POINT? Are you really sure I miss EVERY POINT you make?
Personally I would only claim that you miss SOME of my points, not every. In some cases I would claim that we have different opinions, for example what makes a language and its dialect.
But I would not claim that you miss all my points.
Maybe you should check again.
>What were the 'ideas' that were dropped, even though somehow old code still runs?
Well, fexprs and dynamic scope by default are the big ones, but also the idea of functions as lists, which are why it doesn't have lambda.
>The point was, claiming a 'slower compilation process' due to splicing backquote usage, while in fact the whole compilation of the example you gave was the really slower one, because use used a slower macro system which traverses code for renaming and re-renaming.
I appreciate the irony, but as I've now said several times, that wasn't my justification for using cons. I even said that they hypothetical speed increase would be negligible, and unlikely to be noticed, before you showed that the speed increase wasn't even there. This is one of the things it seems like you missed.
>That's what I say: vague ideas don't matter much when forming language families. Code does. Books. Libraries. Communities.
That's not entirely true. Sure, code matters a bit, but Java definitely comes from the C family, and the code doesn't transfer at all. As for communities, see for yourself: Scheme was born from the MACLisp community, and retains strong ties the modern equivalent: Common Lisp.
> From the perspective of someone who knows Lisp, perhaps.
For what it's worth, as a perpetual novice programmer who knows Lisp a little and Nim not at all, I also found qwertyuiop924 (https://news.ycombinator.com/item?id=12617350 )'s example much clearer. (EDIT: Of course it's totally anecdotal, but I thought that it might be worthwhile to have a data point from someone who is not an expert in either language.)
(defmacro debug-it (&body expressions)
`(progn
,@(mapcar (lambda (expression)
`(format t "~%~a: ~a" ',expression ,expression))
expressions)))
That's Common Lisp. I probably could have made mine similarly clean, but I was too lazy to do so :-).Anyways, hats off to lispm, because he's the one who actually did it.
template debugImpl(varName, value: typed) =
stdout.write(varName)
stdout.write(": ")
stdout.writeLine(x)
macro debug(n: varargs[expr]): stmt =
result = newNimNode(nnkStmtList, n)
for x in n:
result.add(getAst(debugImpl(toStrLit(x), x)))
Whether the Lisp version is clearer or not is very subjective, and you obviously have a lot more experience with it so of course it's clearer for you. To me, the Nim version is far clearer.Edit: flyx's version is even better!
Even conditional logic constructs (if/else) should be an expression (i.e. return a value).
The language doesn't even have to be pure or hard core FP to follow this philosophy.
Together with immutable "variables" (let) this encourages a pretty functional style. I'd just wish their support for lambdas and list comprehensions would be a bit better.
let x = 5;
(That said, very nearly everything is an expression, yes)SML is explicit about it and its syntax makes it clear what type of binding is going on.
val x = 5
fun f = fn x => x * x
datatype weekend = Sat | Sun
So while on top level not everything is an expression, everything on the RHS is an expression.
Yeah, looking at it from the perspective an utopical pure functional world this seems as much of an abomination as "out arguments" in C# ...but in real world code things look different :)
Links: https://www.manning.com/books/nim-in-action and (for the free chapter) https://manning-content.s3.amazonaws.com/download/1/f2a2c55-... .
But in the specific case of Nim - AFAIK there aren't that many languages around with a similar set of characteristics. For me the killer is the combination of GC (I am quite fine with not having to bother with memory management), readability, reasonable high levelness including closures etc, nice OO support, extremely good C and C++ interop and really good performance.