Hacker News new | ask | show | jobs
by FridgeSeal 1647 days ago
Nim is everything Python and Go should be/want to be, as far as language features and semantics go (haha).

It has their easy-to-learn properties, but has a better type system, the generics system and macros are arguably more useful, and more usable than what is offered in either language.

It deserves far more attention than it currently gets.

2 comments

I like Nim, and probably will learn it at some point.

But no, Nim is not everything "python should/want to be".

The name case insensitivity and the implicit imports on the global namespaces are perfect examples of stuff that are better in Python.

I've been using nim for a few years. I wouldn't have made either design choice (i always do qualified imports, it's just a few extra characters of typing). That said, there is a reason these superficial things always come up in HN threads. If you haven't used a language before, syntax is the only thing you can talk about. It's the equivalent of low effort political banter and the dopamine hit you get from junk food or watching a tiktok. These things melted away after my first 30 minutes with the language, and that's about when the real work began.
> If you haven't used a language before

This just feels like gatekeeping. I like nim, I've used it, talked to Dom and the author a few times in chat, filed some small bug reports. But even if I hadn't, I'd still hate the case insensitivity; I'm an experienced developer and I don't need to have used a language to know I will like or dislike some aspect of it.

To me it feels more like gate opening: if you feel discouraged to try a language by this specific feature that you do not like, do not fear, come in and try it since as far as we know no people actually using the language suffered from this feature
I don't think Nim has anything that I'd call a "global namespace". By default importing a module will include its exported symbols into the local module, but that doesn't impact anything globally.

In my opinion, this is the correct default, at least for Nim. Operator overloading and UFCS (https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax) don't really work if I have to prefix everything with a module name, and the static nature of Nim means that I'll get a compiler error if there's any ambiguity. There are downsides as well, but language design is all about tradeoffs, and I think Nim got this one right.

Regarding the case insensitivity, I was initially put off by this as well, but in 2 years of using Nim as my primary language, I have never, ever, encountered a real-life issue with it. I've never seen a Nim code base that uses mixed casing, and never encountered or heard of a bug caused by this behaviour. However, it has allowed my own code bases to stay 100% consistent, regardless of the code style of my dependencies, even when those dependencies written in other languages. Contrast this with Python where the `logging` module uses different casing than everything else, so you're forced to use an inconsistent style if you want to consume it. This type of thing is a non-issue in Nim. I think a case sensitive Nim would still be a fine language, but in my experience the pros of being mostly insensitive outweigh the cons.

In the end you may still disagree with both of these decisions, which is fine. Just understand that it's a nuanced discussion, and there's some solid reasoning behind their choices.

Implicit imports on the global namespaces? What do you mean?

Case insensitivity is somewhat complex topic in Nim, as there are some places where the case actually matters, plus there are some more rules for equivalence of identifiers (related to `_` IIRC). In any case, it was never a problem for me in practice; nimsuggest works quite well.

> Implicit imports on the global namespaces? What do you mean?

Not OP, but they are referring to `import std/strformat` (from homepage). That just imported things into the global namespace. Maybe that isn't required and its just for short examples, I don't know.

Go and Python can both import into the global namespace too (`from thing import *` and `import . somepackage`), but they are pretty frowned upon in most projects.

> Not OP, but they are referring to `import std/strformat` (from homepage).

> That just imported things into the global namespace.

That's what tripped me up: there's definitely no such thing as importing into GLOBAL namespace. That's an import into a top-level (true) namespace of a MODULE. In Python, if you put something into the dict returned from `globals()`, you are indeed importing into global namespace, but I've never seen it actually done. Anyway, that might seem like nitpicking, but it's an important distinction. [EDIT: C `#include`s are an example of actually importing into global namespace]

With that out of the way: the wildcard imports are frowned upon for a reason. What reason? Basically, it's hard to tell a) what identifiers were imported, b) if any of them are still used in the module (ie. if it's safe to remove the import). With multiple wildcard imports in one file you also get c) hard to tell where a given identifier comes from.

Now, none of these problems apply to Nim. Nim is statically typed. The compiler and the tooling knows exactly which identifier comes from where. If you delete all the places you used identifiers from a wildcard import, the compiler and the tooling will tell you that it's safe to delete the import. Another problem with wildcard imports is that the imported modules can change, leading to disasters like silently shadowing an identifier imported from somewhere else. Again, this cannot happen in Nim - it simply won't compile.

Every single linguistic feature has its pros and cons, but you have to remember to only weigh those in the particular context they're used in. What I mean is that wildcard imports in Python and Nim are very, very different beasts, and shouldn't be directly compared, if at all. It would make more sense to compare Nim to Elixir or Scala in this regard.

Note: obviously, Nim also has selective imports and whole module imports, too.

Cool thanks for the info.

Nim having selective imports solves any annoyances I would have.

But you mentioned all the reasons it was ok in Nim. But the answer is always that the compiler knows. That isn’t why I personally dislike this type of import. I dislike it as a user. I find it super annoying not knowing where an identifier is coming from.

I love having ‘fmt.Printf()’ instead of ‘Printf()’. Trivial example I know. I’m sure everything becomes fine once you are familiar with the ecosystem though.

There's a reason why wildcard/selective[1] imports are favored in Nim. It's because of the uniform function call syntax[2]. In short, this:

    split("a string", " ")
is exactly the same thing as this:

    "a string".split(" ")
`split` here is a normal function, not any kind of method. This means that you can chain normal functions as if they were methods on objects:

    "a string".split(" ").join("\n")
now, if you imported modules instead of functions themselves, how would that look like?

    sequtils.join(strutils.split("a string", " ") "\n")
or, if you wanted to keep uniform call syntax, you'd have to invent some new syntax:

    "a string".split<strutils>(" ").join<sequtils>("\n")
Which also doesn't look very appealing.

So, there are trade-offs here, and you obviously might not like the choices made by Nim, which is fine! But, there are reasons for the design decisions made by the language implementers, and they are almost never predicated on a single feature: all the features of a language interact with each other, sometimes in very strange and unpredictable ways. So it makes sense to consider the features your interested in the proper context :)

[1] `import modname` and `from modname import fun1, fun2`

[2] https://en.wikipedia.org/wiki/Uniform_Function_Call_Syntax

IMO people really should stop comparing it to Python, it's a very different language.
I don't mind the comparison but do wish the comparison wouldn't so often be "it's a better Python."

I've been programming Python 14+ years and I've dabbled in Nim and I agree it's a very different language. Yes, there are similarities, but my expectations about it being a lot like Python from the community and docs actually led to some significant frustration and discouragement.

"Inspired by"..."borrows concepts from" are better descriptors IMO. But it would be better if an unqualified "it's like Python" disappeared.

Your opinion contradicts the official description of Nim on the language's home page:

> Nim is a statically typed compiled systems programming language. It combines successful concepts from mature languages like Python, Ada and Modula.

Also, the "Features" page contains "Inspired by Python" twice, and "concepts from Ada and Modula" once.

If you have 'myvar' and 'myVar' in some code there are three things you could do.

1) The compiler gives you an error, this would be my preferred solution though I'm not aware of any language that does that.

2) These end up referring to the same variable with no error. Nim's solution.

3) These are actually different variables with no error. The common solution.

Again, I would prefer (1) but I think that (2) is a safer way of handling the situation than (3).

As much as I hate it for correctness, 2 is better than 3 indeed. No sane developer encodes meaning in the case of the name of the variables, in the overwhelming majority of cases it's a typo.
In the C world ALL_CAPS very often signify preprocessor macro identifiers (or some other "class" of identifier), but, yeah..they may all be "insane". ;-)
OMG, what if C did significant case because of that? I mean, it's fine to write "#import", why didn't K&R use a special syntax for macros, like $MACRO or something and left the uppercase as a convention?
I am no C historian, but I believe part of the idea was to be able to have macro wrappers around proper language symbols like "#define sin(x) real_sin(x, 1e-7)" or some such, for example. The "warning character" (what you used `$` for) was thus only for introduction (#define) not for reference. Whether the initial intent or not, this idea certainly came to become an application, also invoked via compiler command line "-Dsin=real_sin" style definition. With this you want the case conventions/sensitivity to be the same in the preproc/macro language as the base language.

FWIW, I do think that the late 1960s saw a broad "new prog lang" evolution away from case insensitive (often ALL CAPS due to low resolution dot matrix printers/display devices) stylistic tendencies in, e.g. LISP and FORTRAN, towards case sensitivity and lowercase. So, I suspect this was the salient driver - "We have lowercase now/text is more legible! Let's use it!". (Yes, mechanical typewriters/typesetting had it for decades to centuries, but programming was not done on those, but with printers and punch cards and just starting to be on CRTs.) There are analogies with Unicode in PLs these days (though there are obviously also other motivations these days/decades with the compute world i18n).

player = Player()

all the time in Python.

date = new Date()

all the time in JS.

Those are just the two languages I spend the most time in, but, but I'm sure it's trivial to find numerous examples of cases carying meaning in case-sensitive languages. Especially ones which encourage camel casing.

`player` and `Player` are different in Nim. Ditto for `date` and `Date`.
There is a difference between variables and types.

For instance, in C#, with capital-case convention for variables, it is common to give a variable the same name as the type.

eg `public Player Player;`

The 3rd solution is terrible, because:

    ForMid = 4 # padding for middle box in px
And:

    FormId = 3 # ID of the form in the user process
Should definitely not be resolved as the same variable.
The only thing worse than 3 is 2.
I don’t remember the exact options, but there’s a way to make Nim emit warnings (and perhaps errors) for inconsistent capitalization.

It isn’t the default, though.

The options are nim c --styleCheck:hint --styleCheck:usages (you need both). Though not on by default you could put it in your config.nims or nim.cfg's to make it on by default for you or for your code.
The global namespacing is what allows UFCS to work. You can rewrite parseInt("6") to "6".parseInt, but you can't rewrite strutils.parseInt("6") to anything.
Yes, you have listed the two things that have stopped me from giving Nim a chance. I especially dislike the global namespaces, it makes it very hard to understand where a function is defined.
Use go-to-definition in your editor.
As someone learning programming, should I try nim for my first compiled language, instead of C++? I know it's likely harder to find answers in the Internet, but at the same time it looks easier, coming from Python and JS.

If nim picks up pace in a few years and I already have a sense of it, it may be a good for prospects too.

I'd recommend it. The syntax should make you feel at home if know python, though the type system (and std lib) does make it feel like a very different language once you use actually use it.

One advantage over C++ is that you don't need to learn make, IDEs or a complex build system. The nim compiler and the nimble package manager just takes care of building for you. "nim c main.nim" and off you go, ending up with a single executable, no matter how many imports, modules, etc you have.

I do recommend this as a gentle introduction: https://nim-by-example.github.io/ and recommend you use VS Code with the Nim extension by saem.

Not OP but will definitely recommend Nim if you are intimidated by C++. Nim first compiles to C and then that C code is compiled to binary. If you know python already, you will feel right at home and you get better performance and a great type system and metaprogramming. But the eco-system is definitely not as mature as Python or JS.

The documentation is good enough that I don't need to google most of the stuff while writing Nim, having types definitely help with that.

If you already know python and JS I think you probably won't have too much trouble with Nim, but why not add something new like memory management to learn about? Rust is complicated, but it has training wheels, and its wasm support would integrate with your JS knowledge. Alternatively modern C is still used everywhere and will teach you a lot about hardware.
> but why not add something new like memory management to learn about?

You can learn that with Nim too. It supports manual memory management like C. The new ARC/ORC GC also works more like modern C++ than a traditional GC.

> its wasm support would integrate with your JS knowledge

Nim can also compile to JS. Or you could compile it first to C and then compile that to wasm, although that's not supported out of the box.

Those are good points. I have found that languages that don't force me to use a feature don't teach aa well as ones that do. I usually don't want to use those languages later though ( java and oop or Haskell and functional) because I like having options. Nim gives you lots of options it sounds like.
Consider either Rust or Go of you want something easier than C++, but that still has a large community and lots of learning resources.
Go yes. Rust? I wouldn’t say that Rust is easier although I may be biased with my long C/C++ background. The ownership model is a pretty complex thing to learn. I guess maybe if you Box a lot?

It does have an easier on-ramp story for getting started/adding dependencies and that may be important for getting started.

> The ownership model is a pretty complex thing to learn

It is, but if you want to write C or C++ that doesn't crash, or just silently work incorrectly then you have to internalise these rules anyway. And having a compiler that gives you a helpful error message when you get it wrong is much easier than getting a segfault at runtime that may not even occur in a proximate part of the code. Let me put it this way: in 5 years of using Rust I am yet to need a debugger, and only need even println debugging rarely.

That’s not strictly true IMO. There are things that are trivially expressible in C++ that require a lot of complexity on the Rust side to prove to the compiler the code is safe.

I don’t disagree with you on the safety aspects* and 100% real production code should really be starting in Rust. From a learning perspective though… not as sold yet. There’s a lot of things nicer (the stdlib is 100x better and more ergonomic and more pythonic in terms of “batteries included”). There’s a different set of edges though and none of the ownership concepts you learn really transfer anywhere else so you’re learning Rust’isms but not general systems programming things (just like goroutines teach you Go’isms and not what high performance thread safety means).

* The debugger claim feels specious because segfaults aren’t the only source of bugs that need debugging. How do you deal with an unexpected panic or logic bugs?

As someone who properly learned C++ only some years ago, I can confidently state that Rust is harder to learn than C++. You can effectively code in C++ by learning only 20% of the language, but Rust has a far larger mental overhead to start coding. You have to do everything "The Rust Special Sauce Way".
> I wouldn’t say that Rust is easier

Rust is easier than C/C++, but not at first. After you learn the semantics of it, is very smooth sailing (at least until you get into the most exotic needs like build your own async runtime).

I mean perhaps if you’re writing 20 line toy programs, but to do anything non-trivial in either language you’re going to want to use a library, and that’s already hard in C++
I would recommend just vanilla C for maximum learning value. Learning about stack vs heap, pointers, memory allocation, etc. lets you learn more about what the computer is actually doing. It will make the computer seem less like a magic box, and you will see the things other languages abstract away.