The `with` syntax is new to me, but it looks useful for dealing with lots of subfields. Its also a good point that while Nim looks like Python, it very much isn't.
Nim is overall much better designed from a computer science perspective with saner scoping rules, etc, IMHO. That means that while Nim as a language is relatively complex, overall its easier to work with than C++ or even Python which have lots of special cases.
Nim is heavily inspired by Wirth languages, where you have the same capabilities to do low level programming, without the traps of C derived languages.
Citation required. How can Wirth languages (e.g. Pascal, Modula) have "the same capabilities for low level programming", but without the traps, of C derived languages (e.g. C, C++)? The idea suggests itself that you can have one or the other, but not both.
Personally from trying it out, I like the speed resulting from the stricter module system. And there might be a little additional protection against bad builds resulting from it. But I assume it comes at a price in flexibility as well - I can't really tell since I quit after working 6 months in it.
Undefined behavior and null terminated strings with the attendant buffer overflows are not fundamental constants of low level programming; they’re implementation details of C. Rusty (and Nim, Modula, Pascal, Ada, etc.) show that it’s perfectly possible to write low-level code with zero-cost abstractions without “unsafe by default” programming languages like C.
UB in C has some unfortunate historic problems, and is _probably_ way too complicated. But UB per se is needed for a number of really important optimizations - for example, for array indexing using 32-bit integers you need to be able to assume that index-scaling operations (implemented as multiplications/shifts) don't overflow.
> null terminated strings
null terminated strings are like 10-50% space savers compared to string + length tuples for realistic strings (which are small). They're also easy to pass around and require no bikeshedding what's the best string type.
You can easily make your own type if you want (struct String { const char *buffer; int length; }) or you can just use C++ and one of the more terrible full featured string types with all sorts of badly matching built-in memory management and what not.
(By the way, the issue with SetLength() that I described in my other comment applies to Pascal Strings, too)
(Oh, and Pascal/Delphi also wants you to choose between PChar, String, AnsiString, WideString and what not, and for every little thing you're doing you need to locate and use the proper API for whatever string type you've chosen. Or you need to do conversions before the calls, with all the necessary implicit memory allocations. Enjoy!)
(Oh, this is basically how I achieved a 100x-1000x speedup in a module of a business application in a short Delphi stint. I converted the string data to chunk-allocated bytes with manually computed indexing - instead of allocating thousands of little strings and throwing them back and forth. That reduced the total application startup time (including a lot of code that I never touched) from minutes (in some cases) to something bearable (1-5 secs).
> with the attendant buffer overflows are not fundamental constants of low level programming
Of course they are. If you're not controlling bytes directly, and deciding about data layout and tradeoffs, I wouldn't call that low-level programming. YMMV. But - while my code is never fuzzed - it's been years since I've been bitten by a string bug. The importance of this stuff is way overblown, and knowledge and experience how to program with simple data structures is only found in more obscure corners of the internet. Also, very little in systems programming is about strings.
Optimization war stories aside (which is cool, thank you!), this reads like the classic apologetics "C is fine if you just use it correctly". Whereas in practice large C and C++ projects and libraries regularly suffer from severe security vulnerabilities exploited in the wild specifically from buffer overflows and use-after-free bugs.
There is also "Rust is fine once you know how to not get burnt by the type system, and if you carefully choose your dependencies it will maybe still be able to start a build of your project a month from now, and if you're lucky it will build in less than a lifetime".
Or "Java is fine if your fingers aren't sore yet and if you don't mind the latency spikes for your interactive application or if you're basically programming like C with training wheels and less performance".
As for the latter: Or you know, most applications don’t need manual memory management because more often than not, programmers WILL get it wrong.
Unless you really need that level of control over the hardware, you ARE better off with a good runtime env
Nim, with the new ARC garbage collector works well for those situations now (well within a few percent usually). Especially with the `move` and `sink` semantics.
I'm using it in embedded devices and needed a few tight loops a week or two back. With a bit of work and a few odd contortions, I got it to compile to almost the exact type of C code I'd write by hand to run a fast inner loop using preallocated arrays, etc. It turns out that code wasn't noticeably faster than more idiomatic Nim code with a few tweaks to ensure move's were able to be used. So I ended up going back to the more readable/idiomatic version while still roughly doubling the overall speed.
The new `move` semantics with Nim's ARC system really do help make it easier to write fast code. Now I understand why the C++ ecosystem is so bullish on `move` in the C++ stdlib now. It really can take code which looks like straightforward C code but performs closer to painstakingly optimized C code. Mainly since it result in less copying of objects/structs and reducing the number of malloc's. Overall it means less effort to design a fast system.
Has answered multiple times, including to your previous comments.
- bounds checking enabled by default for arrays and strings
- real enumerations that aren't implicit converted into numeric types (fixed in C++11, if one bothers to use enum classes)
- enumerations can be used as indexes (C++ can work around this with enum classes and some boilerplate templates)
- most data conversions must be done explicitly
- ability to actually define subtypes that are enforced by the type system (C++ offers this, provided one does the required boilerplate class/template code)
- memory allocation by default doesn't require doing math with data sizes (C++ as well, yet there is too much malloc()/free() pollution in C++ code bases)
- no need to deal with pointers for out parameters, thanks reference parameters, which even in C++ there isn't any guarantee they aren't null, even though that would actually trigger UB)
- in some of the dialects, namely Modula-2 and Oberon and the languages derived from them, unsafe code requires explicit import of SYSTEM package
- if one wants to do crazy C like code, all the necessary gear is available, pragmas to turn off bounds checking, pointer arithmetic, unchecked casts, unions, mapping variables to explicit memory addresses, pointers to callbacks, it is everything there in the package. The original bare bones ISO Pascal wasn't no longer relevant in the mid-80's, unless the teacher didn't knew any better.
In fact, IBM i, z/OS and Unisys ClearPath have done pretty well with PL/S, PL.8 and NEWP, only adding support for C and C++ after the market started to care about POSIX support on mainframes.
> bounds checking enabled by default for arrays and strings
You can make your own explicit bounds checks, or you can use valgrind (which, just like any type system, can understand only simple situations).
You can use C++ which actually does allow you to do bounds checking by default, if you're into such things.
> - real enumerations that aren't implicit converted into numeric types (fixed in C++11, if one bothers to use enum classes)
> - enumerations can be used as indexes (C++ can work around this with enum classes and some boilerplate templates)
> - ability to actually define subtypes that are enforced by the type system (C++ offers this, provided one does the required boilerplate class/template code)
These are total non-issues in practice, since treating them as integers is usually what you want (you almost always want to support some light arithmetic on them, you want to store sentinel values sometimes...).
Furthermore (as you say) C++ allows you to treat them as separate types.
> most data conversions must be done explicitly
This is the same with C, except for integral types where though you need to turn on warnings to avoid implicit downcasts of integral types with some (most?) compilers. For upcasts, implicit is what you want.
> memory allocation by default doesn't require doing math with data sizes (C++ as well, yet there is too much malloc()/free() pollution in C++ code bases)
That's FUD, if you do memory allocation - of course you need to compute the number of bytes you need.
If you do "new Foo[count]" you're likely doing it wrong anyway, just like if you're calling malloc directly.
To equate memory allocation with malloc (or syntax sugar in fancy object languages) is WRONG.
malloc() is a stupid default allocator, and you don't know
what allocator you're getting if you don't provide your own. Good system performance often requires writing (simple)
custom allocators instead of using malloc which has to deal with random allocation sizes, allocation patterns,
and multiple threads.
Whether using your own allocator or not, it shouldn't be too much to expect you to write a single line like:
Now tell me, why exactly is "new Foo[count]" safer than "ALLOCATE(Foo, count)"? Or just do this minimal boilerplate by hand, this is not a practical issue unless your
code is very badly factored (good code requires only few allocations).
> no need to deal with pointers for out parameters, thanks reference parameters, which even in C++ there isn't any guarantee they aren't null, even though that would actually trigger UB)
This is not a problem at all in practice, and anyway, if you need a lot of that you're doing something wrong. I very much like that C keeps it simple and you always see what happens. This is unlike Pascal and its Objects extensions, where it's impossible to see which data you're actually mutating because there are like 10 different "adressing modes". For example, dynamic arrays in Pascal can be assigned to other variables (or passed through a function parameter), and they will share memory, but only until one calls SetLength() on one "reference" at which point a Copy-on-Write is happening and they split lifes. This is not only not useful but extremely confusing. And the whole object extensions to Pascal (I can speak for Delphi) are built in that way - trying to not let the user see the complexity of what they're doing - which works until it doesn't, which is when you're dealing with extremely difficult to debug problems. Frankly Delphi ecosystem is a mess because it's a set of layers where each time they tried to be clever and add another philosophy-driven set of classes with implicit behaviours on top t...
> You can make your own explicit bounds checks, or you can use valgrind (which, just like any type system, can understand only simple situations). You can use C++ which actually does allow you to do bounds checking by default, if you're into such things.
Yeah, apparently Google, Apple, Microsoft think otherwise.
> Now tell me, why exactly is "new Foo[count]" safer than "ALLOCATE(Foo, count)"? Or just do this minimal boilerplate by hand, this is not a practical issue unless your code is very badly factored (good code requires only few allocations).
1 - Everyone needs to create their ALLOCATE macro, and not everyone does it right
2 - Actually it fails with certain kinds of type given as parameter
> I very much like that C keeps it simple and you always see what happens.
That is what people that never read ISO C and the myriad of UB cases think they do.
That is why I get such a kick of C Pub Quizzes, everyone is so full of themselves in regards to their C knowledge.
> This is why I took issue - you can't have both low-level bit twidding and "memory safe" programming. At most you can have a language that lets you choose between one or the other in a rather integrated system, but then you're still dealing with this abstraction gap, and having to traverse it constantly is not pleasant.
That is the whole point, dangerous code that can trigger memory corruption code should only be written when there is no way around to do it in a safer way.
And most of the time, it is an optimiser bug that is even the case to start with, unless one is writing drivers or kernel code.
> 1- Everyone needs to create their ALLOCATE macro
This is not an issue at all. It's one (!!) line. And the (huge) advantage is: Everybody can create their own allocation macros. Not all allocations are born equal: Some need specific contexts or arenas/pools where to allocate from, some want a specific alignment, some want zeroed memory...
And again, you don't even need this macro, it's just cosmetic. Better invest in some structure that reduces the number of required allocations to a small constant (per type or subsystem).
> and not everyone does it right
This is like saying, don't write code because not everyone is doing it right. This is almost the simplest type of code you can imagine.
> 2 - Actually it fails with certain kinds of type given as parameter
Which ones, then?
---
For the rest, trying to de-flame this and not to reply, and maybe be actually productive for an hour this afternoon :-)
I'm not sure why any "citation" is needed here. C is widely accepted as being deeply unsafe language, and C++ has inherited those traits, and is still working on plugging the weaknesses some 35 years later.
Of course you can do things like bounds checking manually in C, and run with all sorts of warnings enabled, and you can use static code analyzers that uncover entie classes of bugs, but the whole point is to never get that far in the first place.
I worked with Borland's Pascal dialect for 10+ years, and it offered a great deal of protections while still allowing you to use unsafe constructs when you needed to get close to the metal (e.g. pointer arithmetic and even embedded assembly code). But "safe by default" solves a lot of problems.
Pascal's low-level semantics are comparable to C's. That's why I dispute that it doesn't have the same traps.
I agree that if you program C by emulating higher-level object systems, it becomes unmanageable and a lot of bugs around object lifetimes appear, and they're very hard to fix.
I personally don't have a great interest in such object systems, but fail to see how C++ should be any worse than Pascal in terms of perceived safety resulting from the higher level of abstraction.
Not at all. You could apply the same reasoning for assembly: just because you can write insecure assembly it does not mean that all assembly generated by compiler is using vulnerable coding patterns.
> The `with` syntax is new to me, but it looks useful for dealing with lots of subfields.
It's not syntax, but rather a third-party macro[0], which it should be noted is inspired by a rather horrible feature of the same name in javascript. Though I guess the devil would be in the details.
Thanks @masklin for pointing that out -- sloppy language on my side, but I also didn't realize it was a third party library. Thanks @zevv for the macro!
There's also stdlib's with (https://nim-lang.org/docs/with.html), though for some reasons I'm not aware of it doesn't seem listed in the stdlib's ToC :/
D also has withs. Definitely in the "not necessary but nice to have" category of features, particularly when implementing state machines (i.e. statment soup)
It's an interesting pattern to test language designs with. On the one hand expressive macros can be disastrous in big codebases, on the other hand a more "monadic" (implemented via passing some kind of object through a chain rather than algebraically) way is very template-intensive.
At least one (very bottom of [1]) of the 5+ variants makes you list which fields the code gets access to (in a tiny 6 line Nim macro). That is an alternate, perhaps more keystroke-heavy solution to the problem bothering @simias.
I seem to be in the minority but I really dislike this type of sugar. I find that it always makes code harder to read because you can't disambiguate what the code is doing without some heavy context, and changes to the class can profoundly change the way the code is behaving without triggering as much as a warning.
For this same reason I really dislike that in C++ you can just implicitly drop the "this->" to target class members. You can never tell at a glance what "foo = blah" does if you don't know whether "foo" is a member of "this" or not.
I think your page demonstrates what I mean in the "nested WithStatement" example:
Foo foo;
Bar bar;
Baz baz;
f(); // prints "f"
with(foo)
{
f(); // prints "Foo.f"
with(bar)
{
f(); // prints "Bar.f"
with(baz)
{
f(); // prints "Bar.f". `Baz` does not implement `f()` so
// resolution is forwarded to `with(bar)`'s scope
}
}
with(baz)
{
f(); // prints "Foo.f". `Baz` does not implement `f()` so
// resolution is forwarded to `with(foo)`'s scope
}
}
with(baz)
{
f(); // prints "f". `Baz` does not implement `f()` so
// resolution is forwarded to `main`'s scope. `f()` is
// not implemented in `main`'s scope, so resolution is
// subsequently forward to module scope.
}
Wi
I get that sometimes it can cut on a lot of repetition, but I think I would be fine if the syntax was more explicit while still avoiding repetition, for instance:
And beyond that make it non nestable (i.e. only the first level of "with" is taken into account) to avoid the situation above with complicated overloading.
In my experience that would account for 99% of uses of `with` while making the code a lot more readable without requiring a lot of context to make sense of it.
Although frankly even that might arguably overkill, for dynamic languages or ones with type inference you might as well just do something like:
{
let v = &mut some_annoyingly.long.thing();
v.foo = bar;
v.baz();
}
It's almost the same amount of typing and you don't need any magic.
This is true however with is fairly uncommon, so as with any feature of languages that add syntax like this it's up to your judgement as to when to use it.
That implies a very good "smart" highlighter that has in-depth knowledge of the language, something I don't like to rely upon because it's not always available and/or working properly.
The Nim `with` macro by @zevv doesn't modify functions or methods (that'd require explicit handling in the macro to do so and would be a nightmare to do correctly). Which means that example from D is not applicable to the Nim version at all.
My preference is Nim over D primarily since Nim compiles to C, but also overall the community appreciates syntax sugar but also has a decent sense as to when and how to use it. Most "sugar" in Nim needs to be explicitly imported. Literally `import sugar` is one of my first lines in many files.
let v = &mut some_annoyingly.long.thing();
v.foo = bar;
v.baz();
In the example you list (is that Rust, or D?), if it was in C or Nim and those were structs-within-structs and not pointers/refs then `.thing` would be copied to a new `v` (if I recall the semantics correctly). So you'd end up with a weird "logic" bug where updates would disappear. The Nim `with` macro solves that by modifying the generated code,
You describe the events, transitions and even interrupts and this compiles to optimized goto statements with no allocation, no indirect function calls, no switch statement.
There are compile-time checks to make sure you don't have state with no transition from and it also produce a visual graph of your state and transitions.
It's been tuned to be used as the core of my high performance multithreading runtime so zero overhead compared to handwritten.
It is suitable for any state machine with zero alloc constraint and state machines can be composed (they are just a function).
I hope in the future to be able to add model checking using Nim Z3 integration so that we can reach Ada/Sparks formal verification prowess: https://github.com/nim-lang/RFCs/issues/222
I've seen people do similar things in D (although the "D way" of doing it would probably be to parse some kind of textual description and turn it into code at compile time). The cost/benefit of making a metaprogramming library just to write a lexer is fairly bad, however.
wrt to the SMT integration, it's something that I would very much like to try in D, but it's quite difficult to work out exactly where to start (and means bolting a fairly big dependency on the compiler). There also seems to be a big gap in the literature between "practical" verification done "in" the widget being verified (e.g. SystemVerilog code) and more academic work where, effectively, a parallel structure is built which represents the semantics of the widget.
Nim is a huge language, but it is very well designed, and unlike e.g. C++, you can use and discover its feature gradually - including when using 3rd party libraries. It has lispy macros (except reader macros - you start with a parsed-to-ast text), closures, effect tracking, C/C++ level performance &interop and a lot more - but somehow, somewhat by language design and somewhat by community standards, you don't need to fully understand these things (and hardly to be aware of them) when writing your own Nim code or when using 3rd party libraries.
(Whereas in C++, you can pick your own subset for your own code, but as soon as you pull in a 3rd party library, you HAVE to understand the preprocessor, templates, often template metaprogramming, lambda capture rules if that library is built for them, etc).
> Unless if you’re one of those NodeJS wielding kids who thinks Electron is a good idea and the web is the ultimate application development platform… Ignorance is bliss!
MIC DROP!!
Wonderful article. Nim is a great programming language. I am using nim and c these days as my primary languages.
Electron can certainly be a bad idea, but my main gripe there is that everyone embeds the runner. It has an amazing feature set, everyone can agree, no?
You can do pure Windows with wNim or Winim and its few helpers.
If you are looking for cross platform, very graphical, not necessarily accessible, there's NimX. I think there's also a port of Dear ImGui called Nimgui.
If you are ok with webby interface, there's a Nim port of zserge's webview.
Nim is fantastic! I've been using it for a small project of mine which involves web scraping and some computations, and it has just been a joy to work with. I was actually running into some issues with the binary size (not a real problem, but I wanted it to be a bit smaller) since I do some compile time array generation, these tips are much appreciated!
I've been working with golang and Nim for side projects for a few months, and well... I really like both a lot. Having coded in Python for what seems like forever, Nim is feels like the fast Python without training wheels I've always wanted. Golang, nothing bad to say there, either, but I like reading Nim code more.
I really despise the 'common' module approach because it scatters code that should be in the same file.
One workaround I use to avoid this is to break the cycle of type dependencies by replacing one of the dependencies with a generic parameter. This works great if the type that has a generic parameter only deals with opaque objects of that type.
Once concepts are fully supported (they may already be now), this pattern will be able to describe more complex relationships.
> I really despise the 'common' module approach because it scatters code that should be in the same file.
If you have circular dependencies then, in my opinion and experience, it unifies code that should be in the same file, and which was wrongly scattered among different ones.
I remember reading the release notes on Turbo Pascal 6 (or was it 5? or Borland Pascal 7) with a weird half-assed support for circular type dependencies among different units, and trying to find a case where putting all the definitions in one file (and avoiding all these issues) wouldn't make more sense to me. It's been 30 years since, and I haven't seen an example as such yet.
I acknowledge there's no accounting for taste, of course.
58 comments
[ 0.23 ms ] story [ 115 ms ] threadThe `with` syntax is new to me, but it looks useful for dealing with lots of subfields. Its also a good point that while Nim looks like Python, it very much isn't.
Nim is overall much better designed from a computer science perspective with saner scoping rules, etc, IMHO. That means that while Nim as a language is relatively complex, overall its easier to work with than C++ or even Python which have lots of special cases.
Personally from trying it out, I like the speed resulting from the stricter module system. And there might be a little additional protection against bad builds resulting from it. But I assume it comes at a price in flexibility as well - I can't really tell since I quit after working 6 months in it.
UB in C has some unfortunate historic problems, and is _probably_ way too complicated. But UB per se is needed for a number of really important optimizations - for example, for array indexing using 32-bit integers you need to be able to assume that index-scaling operations (implemented as multiplications/shifts) don't overflow.
> null terminated strings
null terminated strings are like 10-50% space savers compared to string + length tuples for realistic strings (which are small). They're also easy to pass around and require no bikeshedding what's the best string type.
You can easily make your own type if you want (struct String { const char *buffer; int length; }) or you can just use C++ and one of the more terrible full featured string types with all sorts of badly matching built-in memory management and what not.
(By the way, the issue with SetLength() that I described in my other comment applies to Pascal Strings, too)
(Oh, and Pascal/Delphi also wants you to choose between PChar, String, AnsiString, WideString and what not, and for every little thing you're doing you need to locate and use the proper API for whatever string type you've chosen. Or you need to do conversions before the calls, with all the necessary implicit memory allocations. Enjoy!)
(Oh, this is basically how I achieved a 100x-1000x speedup in a module of a business application in a short Delphi stint. I converted the string data to chunk-allocated bytes with manually computed indexing - instead of allocating thousands of little strings and throwing them back and forth. That reduced the total application startup time (including a lot of code that I never touched) from minutes (in some cases) to something bearable (1-5 secs).
> with the attendant buffer overflows are not fundamental constants of low level programming
Of course they are. If you're not controlling bytes directly, and deciding about data layout and tradeoffs, I wouldn't call that low-level programming. YMMV. But - while my code is never fuzzed - it's been years since I've been bitten by a string bug. The importance of this stuff is way overblown, and knowledge and experience how to program with simple data structures is only found in more obscure corners of the internet. Also, very little in systems programming is about strings.
Or "Java is fine if your fingers aren't sore yet and if you don't mind the latency spikes for your interactive application or if you're basically programming like C with training wheels and less performance".
;-)
I'm using it in embedded devices and needed a few tight loops a week or two back. With a bit of work and a few odd contortions, I got it to compile to almost the exact type of C code I'd write by hand to run a fast inner loop using preallocated arrays, etc. It turns out that code wasn't noticeably faster than more idiomatic Nim code with a few tweaks to ensure move's were able to be used. So I ended up going back to the more readable/idiomatic version while still roughly doubling the overall speed.
The new `move` semantics with Nim's ARC system really do help make it easier to write fast code. Now I understand why the C++ ecosystem is so bullish on `move` in the C++ stdlib now. It really can take code which looks like straightforward C code but performs closer to painstakingly optimized C code. Mainly since it result in less copying of objects/structs and reducing the number of malloc's. Overall it means less effort to design a fast system.
- bounds checking enabled by default for arrays and strings
- real enumerations that aren't implicit converted into numeric types (fixed in C++11, if one bothers to use enum classes)
- enumerations can be used as indexes (C++ can work around this with enum classes and some boilerplate templates)
- most data conversions must be done explicitly
- ability to actually define subtypes that are enforced by the type system (C++ offers this, provided one does the required boilerplate class/template code)
- memory allocation by default doesn't require doing math with data sizes (C++ as well, yet there is too much malloc()/free() pollution in C++ code bases)
- no need to deal with pointers for out parameters, thanks reference parameters, which even in C++ there isn't any guarantee they aren't null, even though that would actually trigger UB)
- in some of the dialects, namely Modula-2 and Oberon and the languages derived from them, unsafe code requires explicit import of SYSTEM package
- if one wants to do crazy C like code, all the necessary gear is available, pragmas to turn off bounds checking, pointer arithmetic, unchecked casts, unions, mapping variables to explicit memory addresses, pointers to callbacks, it is everything there in the package. The original bare bones ISO Pascal wasn't no longer relevant in the mid-80's, unless the teacher didn't knew any better.
In fact, IBM i, z/OS and Unisys ClearPath have done pretty well with PL/S, PL.8 and NEWP, only adding support for C and C++ after the market started to care about POSIX support on mainframes.
You can make your own explicit bounds checks, or you can use valgrind (which, just like any type system, can understand only simple situations). You can use C++ which actually does allow you to do bounds checking by default, if you're into such things.
> - real enumerations that aren't implicit converted into numeric types (fixed in C++11, if one bothers to use enum classes)
> - enumerations can be used as indexes (C++ can work around this with enum classes and some boilerplate templates)
> - ability to actually define subtypes that are enforced by the type system (C++ offers this, provided one does the required boilerplate class/template code)
These are total non-issues in practice, since treating them as integers is usually what you want (you almost always want to support some light arithmetic on them, you want to store sentinel values sometimes...). Furthermore (as you say) C++ allows you to treat them as separate types.
> most data conversions must be done explicitly
This is the same with C, except for integral types where though you need to turn on warnings to avoid implicit downcasts of integral types with some (most?) compilers. For upcasts, implicit is what you want.
> memory allocation by default doesn't require doing math with data sizes (C++ as well, yet there is too much malloc()/free() pollution in C++ code bases)
That's FUD, if you do memory allocation - of course you need to compute the number of bytes you need.
If you do "new Foo[count]" you're likely doing it wrong anyway, just like if you're calling malloc directly. To equate memory allocation with malloc (or syntax sugar in fancy object languages) is WRONG. malloc() is a stupid default allocator, and you don't know what allocator you're getting if you don't provide your own. Good system performance often requires writing (simple) custom allocators instead of using malloc which has to deal with random allocation sizes, allocation patterns, and multiple threads.
Whether using your own allocator or not, it shouldn't be too much to expect you to write a single line like:
Or even Now tell me, why exactly is "new Foo[count]" safer than "ALLOCATE(Foo, count)"? Or just do this minimal boilerplate by hand, this is not a practical issue unless your code is very badly factored (good code requires only few allocations).> no need to deal with pointers for out parameters, thanks reference parameters, which even in C++ there isn't any guarantee they aren't null, even though that would actually trigger UB)
This is not a problem at all in practice, and anyway, if you need a lot of that you're doing something wrong. I very much like that C keeps it simple and you always see what happens. This is unlike Pascal and its Objects extensions, where it's impossible to see which data you're actually mutating because there are like 10 different "adressing modes". For example, dynamic arrays in Pascal can be assigned to other variables (or passed through a function parameter), and they will share memory, but only until one calls SetLength() on one "reference" at which point a Copy-on-Write is happening and they split lifes. This is not only not useful but extremely confusing. And the whole object extensions to Pascal (I can speak for Delphi) are built in that way - trying to not let the user see the complexity of what they're doing - which works until it doesn't, which is when you're dealing with extremely difficult to debug problems. Frankly Delphi ecosystem is a mess because it's a set of layers where each time they tried to be clever and add another philosophy-driven set of classes with implicit behaviours on top t...
Yeah, apparently Google, Apple, Microsoft think otherwise.
> Now tell me, why exactly is "new Foo[count]" safer than "ALLOCATE(Foo, count)"? Or just do this minimal boilerplate by hand, this is not a practical issue unless your code is very badly factored (good code requires only few allocations).
1 - Everyone needs to create their ALLOCATE macro, and not everyone does it right
2 - Actually it fails with certain kinds of type given as parameter
> I very much like that C keeps it simple and you always see what happens.
That is what people that never read ISO C and the myriad of UB cases think they do.
That is why I get such a kick of C Pub Quizzes, everyone is so full of themselves in regards to their C knowledge.
> This is why I took issue - you can't have both low-level bit twidding and "memory safe" programming. At most you can have a language that lets you choose between one or the other in a rather integrated system, but then you're still dealing with this abstraction gap, and having to traverse it constantly is not pleasant.
That is the whole point, dangerous code that can trigger memory corruption code should only be written when there is no way around to do it in a safer way.
And most of the time, it is an optimiser bug that is even the case to start with, unless one is writing drivers or kernel code.
This is not an issue at all. It's one (!!) line. And the (huge) advantage is: Everybody can create their own allocation macros. Not all allocations are born equal: Some need specific contexts or arenas/pools where to allocate from, some want a specific alignment, some want zeroed memory...
And again, you don't even need this macro, it's just cosmetic. Better invest in some structure that reduces the number of required allocations to a small constant (per type or subsystem).
> and not everyone does it right
This is like saying, don't write code because not everyone is doing it right. This is almost the simplest type of code you can imagine.
> 2 - Actually it fails with certain kinds of type given as parameter
Which ones, then?
--- For the rest, trying to de-flame this and not to reply, and maybe be actually productive for an hour this afternoon :-)
Of course you can do things like bounds checking manually in C, and run with all sorts of warnings enabled, and you can use static code analyzers that uncover entie classes of bugs, but the whole point is to never get that far in the first place.
I worked with Borland's Pascal dialect for 10+ years, and it offered a great deal of protections while still allowing you to use unsafe constructs when you needed to get close to the metal (e.g. pointer arithmetic and even embedded assembly code). But "safe by default" solves a lot of problems.
I agree that if you program C by emulating higher-level object systems, it becomes unmanageable and a lot of bugs around object lifetimes appear, and they're very hard to fix.
I personally don't have a great interest in such object systems, but fail to see how C++ should be any worse than Pascal in terms of perceived safety resulting from the higher level of abstraction.
So, does it mean, that Nim can inherit these unsafe behaviors of C? Or did the Nim language creators safely avoid the pitfalls of C?
Whereas a language like Zig and Rust, compiles its code down to LLVM.
Why? C has been the best choice for Nim so far.
Compiling with Nim+GCC is much faster than Rust, it generates applications with comparable performance, and it supports way more architectures.
Not at all. You could apply the same reasoning for assembly: just because you can write insecure assembly it does not mean that all assembly generated by compiler is using vulnerable coding patterns.
It's not syntax, but rather a third-party macro[0], which it should be noted is inspired by a rather horrible feature of the same name in javascript. Though I guess the devil would be in the details.
[0] https://github.com/zevv/with
D also has withs. Definitely in the "not necessary but nice to have" category of features, particularly when implementing state machines (i.e. statment soup)
It's an interesting pattern to test language designs with. On the one hand expressive macros can be disastrous in big codebases, on the other hand a more "monadic" (implemented via passing some kind of object through a chain rather than algebraically) way is very template-intensive.
[1] https://github.com/c-blake/cligen/blob/master/cligen/macUt.n...
For this same reason I really dislike that in C++ you can just implicitly drop the "this->" to target class members. You can never tell at a glance what "foo = blah" does if you don't know whether "foo" is a member of "this" or not.
I think your page demonstrates what I mean in the "nested WithStatement" example:
WiI get that sometimes it can cut on a lot of repetition, but I think I would be fine if the syntax was more explicit while still avoiding repetition, for instance:
And beyond that make it non nestable (i.e. only the first level of "with" is taken into account) to avoid the situation above with complicated overloading.In my experience that would account for 99% of uses of `with` while making the code a lot more readable without requiring a lot of context to make sense of it.
Although frankly even that might arguably overkill, for dynamic languages or ones with type inference you might as well just do something like:
It's almost the same amount of typing and you don't need any magic.My preference is Nim over D primarily since Nim compiles to C, but also overall the community appreciates syntax sugar but also has a decent sense as to when and how to use it. Most "sugar" in Nim needs to be explicitly imported. Literally `import sugar` is one of my first lines in many files.
In the example you list (is that Rust, or D?), if it was in C or Nim and those were structs-within-structs and not pointers/refs then `.thing` would be copied to a new `v` (if I recall the semantics correctly). So you'd end up with a weird "logic" bug where updates would disappear. The Nim `with` macro solves that by modifying the generated code,You describe the events, transitions and even interrupts and this compiles to optimized goto statements with no allocation, no indirect function calls, no switch statement.
There are compile-time checks to make sure you don't have state with no transition from and it also produce a visual graph of your state and transitions.
It's been tuned to be used as the core of my high performance multithreading runtime so zero overhead compared to handwritten.
It is suitable for any state machine with zero alloc constraint and state machines can be composed (they are just a function).
I hope in the future to be able to add model checking using Nim Z3 integration so that we can reach Ada/Sparks formal verification prowess: https://github.com/nim-lang/RFCs/issues/222
wrt to the SMT integration, it's something that I would very much like to try in D, but it's quite difficult to work out exactly where to start (and means bolting a fairly big dependency on the compiler). There also seems to be a big gap in the literature between "practical" verification done "in" the widget being verified (e.g. SystemVerilog code) and more academic work where, effectively, a parallel structure is built which represents the semantics of the widget.
(Whereas in C++, you can pick your own subset for your own code, but as soon as you pull in a 3rd party library, you HAVE to understand the preprocessor, templates, often template metaprogramming, lambda capture rules if that library is built for them, etc).
MIC DROP!!
Wonderful article. Nim is a great programming language. I am using nim and c these days as my primary languages.
(Saying this as a fan of Nim and lean desktop apps in general, though also someone who really doesn't get web technologies.)
If you are looking for cross platform, very graphical, not necessarily accessible, there's NimX. I think there's also a port of Dear ImGui called Nimgui.
If you are ok with webby interface, there's a Nim port of zserge's webview.
There's a discussion on the Nim forum you might find interesting: https://forum.nim-lang.org/t/5632
I really despise the 'common' module approach because it scatters code that should be in the same file.
One workaround I use to avoid this is to break the cycle of type dependencies by replacing one of the dependencies with a generic parameter. This works great if the type that has a generic parameter only deals with opaque objects of that type.
Once concepts are fully supported (they may already be now), this pattern will be able to describe more complex relationships.
If you have circular dependencies then, in my opinion and experience, it unifies code that should be in the same file, and which was wrongly scattered among different ones.
I remember reading the release notes on Turbo Pascal 6 (or was it 5? or Borland Pascal 7) with a weird half-assed support for circular type dependencies among different units, and trying to find a case where putting all the definitions in one file (and avoiding all these issues) wouldn't make more sense to me. It's been 30 years since, and I haven't seen an example as such yet.
I acknowledge there's no accounting for taste, of course.