102 comments

[ 3.3 ms ] story [ 192 ms ] thread
Nim continues to impress.
With C++20, the C++ example can actually be written as

    constexpr std::string get_fizzbuzz(int number) {
        if (number % 15 == 0) {
            return "FizzBuzz";
        } else if (number % 3 == 0) {
            return "Fizz";
        } else if (number % 5 == 0) {
            return "Buzz";
        }
        return std::to_string(number); // convert to string
    }
No, it can't. std::to_string isn't constexpr. And even if it was, it still wouldn't work because the std::string needs to be destroyed inside the constexpr context.

The article is also wrong because std::to_chars isn't constexpr so you can't use that.

Compiler Explorer is your friend, https://godbolt.org/z/94hzK8svE

In one thing you're actually right, I should have used a string_view for the return value instead.

    #include <vector>
    #include <string>
    #include <algorithm>
    #include <string>
    #include <cstdio>

    constexpr std::string_view get_fizzbuzz(int number) {
        if (number % 15 == 0) {
            return "FizzBuzz";
        } else if (number % 3 == 0) {
            return "Fizz";
        } else if (number % 5 == 0) {
            return "Buzz";
        }
        return std::to_string(number); // convert to string
    }

    int main() {
        static constexpr auto value = get_fizzbuzz(15);
        puts(value.data());
    }
The template metaprogramming to expand all values for get_fizzbuzz() is left as exercise.
Your example only works because the optimizer eliminates your call to std::to_string.

Call get_fizzbuzz(11) and you'll see the error.

I stand corrected, however I have a couple of ideas to try later on.
You’re gonna have a bad time if you return a string_view to a locally created string.
It is compile time, so the expectation is that the compiler replaces it anyway, although you might be right.
`constexpr` isn't "compile time". It's potentially compile time at best. Debug builds in particular will go out of their way to evaluate things at runtime, presumably so you can set breakpoints and step through code, even when it's completely pointless. I have seen the following function show up in a profiler:

    template < bool value >
    bool constexpr IsEnabled() { return value; }
This was used to silence warnings about unconditional branches in a macro. There are ways to force such functions to be evaluated at compile time, but they're pretty awkward, and limited to integral types (you can't use them on string_view s):

    #define EVAL_AT_COMPILE_TIME(x) std::integral_constant<decltype(x), x>::value
    const bool x = EVAL_AT_COMPILE_TIME(IsEnabled<true>());
That is why main() has a static constexpr, to force its execution at compile time. A trick I learned from Jason's screencasts.

Although, you're right. I tried it back at home on VC++ and its static analyser wasn't happy.

My idea was to make use of template metaprogramming to generate the string buffer, there are a couple of examples of such attempts.

However, I guess with such amount of additional code, I should declare defeat on the original comment.

Seems to me the good news is that your compiler told you this doesn't work.

In languages where you can't tell the compiler you think this is constant, there's a risk you delude yourself, especially because computers are very fast, and you think you've got O(1) when it's actually O(N) or even O(N^2) and one day N gets big enough and you're in real trouble.

Yeah, because I am one of those persons that when I use languages like C and C++, and have the last word, the seatbelt and metal gloves are always turned on.

Those examples for HN recreation naturally weren't written with such care.

String literals are specified to have static storage duration. That means references are valid at least until main returns. In practice string literals are immortal and references to them are always valid.
I didn’t say string literal. I said “locally created string” i.e. std::string

In particular, my comment was referring to “return std::to_string(number);”

>The template metaprogramming to expand all values for get_fizzbuzz() is left as exercise.

There's some joke here about authors using "left as exercise to the reader" to skip debugging their broken code.

Or like I don't need to fully implement it for the next interview.
Have to defer conversion to std string to runtime https://godbolt.org/z/rWKEr4P6h
Yeah, I was wrong on that regard.

Although I still think with a mix of template metaprogramming and constexpr might be possible, however it would be a very low ROI on such example.

And it would still be worse than the other languages, so defeat accepted.

I'm not sure deferred conversion is a bad thing in this toy instance.

I agree that being able to easily tradeoff between time and space is an important property of compile time evaluation and C++ doesn't make that easy with the no escaped allocations requirement.

On the other hand, I shudder to think what will happen to binary size. It can't be any worse than codegenning all the things with reckless abandon... Right?

Based on the other comments to this, isn't it a problem that we're even debating the "correct" way to implement FizzBuzz in C++20? Like, does no one else see a problem with that?
Like, no. Have you never programmed Python?
I've seen many debates about whether or not a given Python code snippet was "best" or "Pythonic" but nowhere near the level of discourse (compared to C++) about whether or not it's correct. Perfect forwarding, anyone?
Oh I see, I thought you were referring to language features rather than code correctness.
(comment deleted)
The compile time in zig and nim is breath of fresh air, while rust seems similar spaghetti mess as c++
I know its super hip to hate on C++, but how is adding one keyword to make a function compiletime evaluated "spaghetti mess"? Because it's C++ and its hard?
constexpr should just try to evaluate stuff at compile time. In D, most sensible code works at compile time automatically, I don't think about it.

Having to mark everything as evaluatable at compile time is a stupid, stupid, decision that only C++ could think was a good idea.

D should worry less how great language it is, focus on fixing long standing DIPs and compiler bugs, and actually have an ecosystem that makes it worthwhile using in the industries where C++ is the first choice.
Somehow it feels like languages are clashing here.

>D should worry less how great language it is,

You are just replying to some guy expressing an opinion on internet about how to do something better, and using D as example.

Mostly because unless I am going crazy, the comment was different when I posted my remark.
The HN guidelines state:

> Eschew flamebait. Avoid unrelated controversies and generic tangents.

You responded to

> Having to mark everything as evaluatable at compile time is a stupid, stupid, decision that only C++ could think was a good idea.

with

> D should worry less how great language it is, focus on fixing long standing DIPs and compiler bugs, and actually have an ecosystem that makes it worthwhile using in the industries where C++ is the first choice.

Definitely qualifies as both "flamebait" and "unrelated controversies".

The comment was edited, it was about the virtues of static if in D versus C++.
My comment?
If I recall correctly it had something about not doing if constexpr if being lesser than static if, and how greater it makes D over C++, overlooking the fact that since Andrei published its book that has mattered very little.

Now if I happened to reply to the wrong comment, sorry about that, and I should pay more attention before replying.

You have replied to the wrong comment.
One downside to implicit constexpr is its harder to know when an API intends it as a user and harder to enforce it works as an author. Seems like it could be easy to break compatibility.
At least users would notice at compile time when it breaks. With both implicit and explicit constexpr, authors who care could use the explicit form, and any implicit constexpr would be “use at your own risk”, but would still be guaranteed to work as long as you don’t upgrade the dependency.
That doesn't really happen in D because basically everything is "constexpr" unless it does something like inline ASM or unions with pointers in them
(comment deleted)
It may seem stupid to you, because you only consider saving a few keystrokes. The keyword exist to guarantee stability/contract of compile-time-compatible APIs.

A change of an implementation detail in some leaf function (that happens to add a run-time dependency) should not accidentally break execution of another function elsewhere (possibly in a different downstream project) that happened to work as compile-time before that.

Note that compilers are still free to execute pure functions opportunistically at compile time, but it's just not guaranteed.

By the same logic it also means everything has to be marked as constexpr, i.e. you have to type everywhere, people who don't make their code less pleasant to use. The end game is that everything reasonable will be constexpr anyway, so there's no point.
> Having to mark everything as evaluatable at compile time is a stupid, stupid, decision that only C++ could think was a good idea.

What is stupid about it? It makes a lot of sense given how programming languages work.

What sense is that? I've worked on roughly 3 compilers with CTFE
I did some experimenting recently and was disappointed by how limited it is. It's a great solution for generics but things like file access don't work in const expressions and the errors are horribly opaque with no hint that it's not allowed.

I was hoping to be able to do things like generate type safe classes from a database schema but the current limitations mean you have to fall back to shell scripts, which zig-build also appears to not support.

Even in this thread everyone is code golfing fizzbuzz instead of something more practical.

It's funny how something like Zig's comptime of C++'s constexpr get 90% of the way to solving most compile time issues, but can't knock out that last 10%. Compile time database types sound like one of those cases. F# had a feature for compile time types via "type providers". It seemed _really_ cool at first, but in practice is was a bit fragile.

Though on the other hand I'm using compile Nim code to parse CMake files and provide static types for configuration values. It's super easy in Nim between macros and const's. Here's a ~170 lines of code where I'm compile time checking that my Nim code can compile time check against the current build configuration of Zephyr RTOS https://github.com/EmbeddedNim/nephyr/blob/main/src/zephyr_c...

Isn't that somewhat possible with @embedFile? Then you get the content of the file as a null terminated byte array / normal zig string. I would think you could do further comptime things to it after that no? I haven't tried it myself so maybe I am missing something.

I get that this differs from normal file io, but I could argue that is in line with Zig motto of clarity.

Also you would of course suddenly have to have the file content as part of your binary. Maybe you were hoping to be able to throw that away after you generated what you wanted...?

In the case the file was only intended to be there for a temporary step, I wanted that working before involving the database, which I believe also won't work because you can't do system calls at comptime.

Ultimately if you go down that path, whatever would be supplying the file could just as easily (or perhaps easier) be supplying zig code and you're still reliant on some sort of per-processor.

Nim is an odd language insofar that every time it's pitted against other languages, it performs brilliantly on nearly every level. Yet almost nobody uses it. Really weird juxtaposition.
Quickly scanning Nim page, there is a couple of things that bothers me in Nim. The python style blocks making multi line lambda awkward, Garbage collection, "Identifier equality" (some_variable == someVariable)...

So because of the quirkiness I would understand why it is not a universally adopted language.

Do you have pointers to the comparison with other languages?

I think the biggest issue is that Nim took a long time to “find its feet”. There was a lot of experimentation before the current sweet spot. Due to its new GC system ‘ARC’ it’s now broadly useable as a systems language, which wasn’t true before with a regular GC. ARC is non-atomic/non-locking reference count based, meaning it can be used to as a systems language (no GC pauses) or for system libraries (C programs can use it).

There’s a few awkward-ish parts of the syntax, but in practice those items aren’t big issues (compared to say Rust async syntax issue). For example multi-line lambdas can be done with ‘do’ blocks or just wrapping parens. Identifier equality has actually saved me from a few bugs by accidentally creating a new similarly named variable, say an ‘isDone’ when I already had an ‘is_done’ following the style from C FFI. Though the compiler warns by default when mix naming styles, and can be set as an error if desired.

Iterators usage can be a bit annoying though. I also really enjoy UFCS: https://en.m.wikipedia.org/wiki/Uniform_Function_Call_Syntax

Python comparison example: https://narimiran.github.io//2018/05/10/python-numpy-nim.htm...

ARC is not new, it was used by Apple in their languages for at least 10 years now.
Obviously parent is not claiming that Nim invented the concept of reference counting. They're saying that it's a recent addition to Nim, and Nim's other memory management strategies have been part of the compiler for longer.
That and that Nim's ARC system extends the concept a bit further to include move semantics and limited ownership rules. Nim had a traditional RC garbage collector before, but ARC is case where "the whole is greater than the sum of parts".

Swift and Obj-C's ARC system relies on atomics and their associated issues (explained well here: https://news.ycombinator.com/item?id=15046481).

Nim's ARC system relies on ownership rules that require a single thread to "own" any ARC-managed memory which removes the need for locks or atomics in the reference counts. That eliminates the non-determinism of locks & atomics when doing the RC accounting, overall improving latency and performance. Move semantics increase performance and enables "giving" ownership of ARC-managed memory to another thread.

What's so awkward about multi line lambdas? You can achieve them in the same way you can in JavaScript, can't get easier than that.

    import sequtils

    let x = [1,2,3,4,5]
    echo x.map(
      proc (num: int): int =
        num*2
    )
The lambdas are terrible in nim, you currently have to `import sugar` for bandaid fix.

Nim is in progress to fix them, see here: https://github.com/nim-lang/Nim/pull/11992

I don't see why people can't admit to shortcomings in languages? I like Nim a lot, doesn't mean I'm blind to where it falls short or needs improvement...

I just started to (very slowly) learn Nim, and it's quite nice for the moment. Of course there are some points of friction, but which language doesn't have ? The documentation page has comparison with C, python and typescript/JavaScript https://nim-lang.org/documentation.html
>Identifier equality

This is a very surface-level complaint, and it is no surprise that almost everyone who complains about this hasn't actually used the language. I will admit it is indeed a peculiar feature, I wouldn't be able to name another language that did this before, but as it turns out, it has yet to be a problem in practice.

Do you fear some crazy person might mix identifier styles? They could do that in any language, Nim has nothing to do with that. The pro of this feature is that you can write your library in whichever style you prefer, and people can use your library and still use their preferred style, I always hated how in python you often end up having to use libraries written in camelCase for your snake_case project (or vice versa).

Once again, it is a rather peculiar feature, like who asked for this? I don't know, but then again, why not? it did make me raise an eyebrow at first, but in practice, Nim gets it just right. The one valid scenario where it could get in the way is when searching your source files for an identifier, but even then Nim has solutions for that (there's nimgrep, and mainstream editors/IDEs are already nim-aware anyways).

(comment deleted)
May I ask what exactly do you dislikes about Nim's garbage collection? You said you "quickly" scanned Nim's page, so I assume you just don't want a GC period? If you're dealing with hard-realtime constraints then I understand. But otherwise, as long as you aren't dealing with cyclic data structures, Nim's GC is just reference counting, which is very widespread in even C++ and Rust code, and if you really want to you can just not use the cycle collector and break the cycles yourself.

The way I see it, the GC is just nice to have for productivity, and for those cases where you really need to optimize your program, there is nothing stopping you from managing your memory yourself, you can write your own types with their own constructors/destructors (Nim has "scope-based" memory management akin to RAII in C++), of course, you must be aware that using pointers will lead to potentially unsafe code, Nim can't hold your hand there.

Because comparing grammars and semantics is meaningless without an ecosystem and killer use case to come along with the language.
I say this as a fellow language developer: Nim is impressive as hell!

I'm particularly excited about their region isolation [0]. A lot of new languages are exploring it, and it has a lot of potential to change the way we program over the next decade.

[0] https://github.com/nim-lang/RFCs/issues/244

It lacks the branding of something like Rust, which it self tried to piggy back on Go years ago, until it got traction.

C++, for better or worse continues to be a safe choice. A reasonably large user base, and it will be around for a long time. My co-worker joked that if you want your software to be able to run in the future, pick Windows and C++ as your platform. Then you’ll be good for a few decades.

Patiently waiting for the right moment to try and introduce it at $JOB, but it's kind of hard so many developers like only the familiar...
People don't use Nim because the ecosystem is small, and the ecosystem is small because people don't use Nim.

The solution is to make the ecosystem bigger by contributing to it. Good news! The ecosystem is small, so there's a lot of low-hanging fruit.

I'm a "random guy" writing Nim in my spare time, and I develop & maintain a handful of Nimble packages. Someone might actually be using them, too. None of my packages require regular maintenance because Nim's tooling is dead simple and the language is really easy to work with. It's a great opportunity for an enthusiastic hobbyist to be a big fish in a small pond.

It's been a while since I last played around with Nim (back when it was still called Nimrod). But doesn't Nim compile to C and therefore it can interact with the entire C ecosystem? And afaik it is also camel-case-insensitive, so that you can call C functions without having to worry about different naming conventions?
> But doesn't Nim compile to C and therefore it can interact with the entire C ecosystem?

You can directly call C code, yes. You can even work with macros! It's a lot nicer when you can import a package that provides an abstraction layer over the C code, though (for example, my `ruby' package)

> call C functions without having to worry about different naming conventions

Yes. In fact, if you hand-write your C bindings instead of using something like c2nim, you can completely change the name of the function. This is a snippet from my aforementioned `ruby' package:

  proc evalString*(code: cstring): RawValue {.importc: "rb_eval_string".}
The ecosystem might be small but as you said it can grow and I think there's enough people out there willing to give a new language a try if it seems appealing enough, it's quite common to try a new language for Advent of Code for example.

That being said, honestly the thing that stops me from trying/using Nim that the compiler enforces a style and that style is just wrong (it enforces spaces). Same goes for Go which enforces the opening brace on same line style. The people writing those languages can have whatever style they like for their code, but forcing me to use it is very off-putting to say the least. I get that this is probably a minor issue for most people but I can't deal with change that well and things like this are super annoying, especially since there is not one single reason for forcing a certain style on everyone.

I'm aware that `#? replace(sub = "\t", by = " ")` works, but it's a hack and I'd have to inject it everywhere, not a good solution. But at least it's better than Go in this regard.

> That being said, honestly the thing that stops me from trying/using Nim that the compiler enforces a style and that style is just wrong (it enforces spaces)

I agree that the spaces thing is weird. Personally, I don't care what style anyone uses in any language. Nim doesn't care either (besides tabs, for some reason). All I ask is that your style is readable and consistent.

It seems like every developer has hot takes about code style. With Nim, no matter what hot takes you have about code style, I can import your module without you forcing your style into my code.

I'm a firmware engineer by day, and it seems like every embedded C codebase on earth uses a different style. For me, it's refreshing to be able to write code that's styled consistently, regardless of the styles used by dependencies.

Exactly, as long as it's readable and consistent I don't care what style other people write their code in, all I ask is that they and the compiler give me the same freedom and Nim and Go don't do that.

> It seems like every developer has hot takes about code style. With Nim, no matter what hot takes you have about code style, I can import your module without you forcing your style into my code.

I assume you're talking about the partial case insensitivity of Nim? I find that a bit weird tbh but I can see the appeal, you no longer would have things like a dependency doing weird things like all caps function names and forcing you to have that in your code.

To some extent, Nim has a valid excuse because Whitespace is syntax. Forcing spaces should (ostensibly) make parsing more reliable, and users are less likely to have weird problems caused by their editors. Regardless, it's a negligible problem in my eyes.

> I assume you're talking about the partial case insensitivity of Nim?

That's one part of it, yes. I include UFCS in that as well.

Case-insensitivity isn't as weird once you start using the language. Everything in the stdlib (and every Nimble package I've used) is in camelCase anyway, so I often forget that feature even exists. If anything, I'm grateful the language punishes people for trying to name one function `doHttpThing' and another function `doHTTPThing'.

I'll admit that it's much easier in languages that are not whitespace sensitive but imho it's not an unsolvable problem for the ones that are and software from 50 years ago not handling things correctly is not an argument if you ask me. I know it bothers me more than it probably should and likely more than the average person but at the end of the day such things have an influence on people. Everyone has some things they just prefer and imo forcing everyone to use one specific style is always a worse solution than using tools like code formatters and letting people have their way.

> That's one part of it, yes. I include UFCS in that as well.

Oh yeah, UFCS is pretty nice. Though I don't like the extra step Nim takes of dropping parentheses for 0 argument functions, I'd very much like to see if I'm calling a function or not. Sure for things like the size of an array it doesn't really matter but the call could be expensive. This is mostly for reading code though, obviously in my own code I'd never drop them.

> Case-insensitivity isn't as weird once you start using the language. Everything in the stdlib (and every Nimble package I've used) is in camelCase anyway, so I often forget that feature even exists. If anything, I'm grateful the language punishes people for trying to name one function `doHttpThing' and another function `doHTTPThing'.

I'd argue that it's still weird because the first letter is still case sensitive and I"m coming from a case sensitive language but being able to use foreign code in your own style is very appealing. I can't think of any good reason why anyone would deliberately have two different functions with the same name in different cases anyway, the closest example I can think of would be T and t for temperature and time but you can also just type those words out, more expressive anyway.

> Though I don't like the extra step Nim takes of dropping parentheses for 0 argument functions

That isn't true, you need parentheses for procs with no arguments, else it's interpreted as a proc pointer. Try `echo` vs `echo()`.

> I can't think of any good reason why anyone would deliberately have two different functions with the same name in different cases anyway

It's used for declaring variables of an object. `type Foo = object` and `var foo: Foo`.

I like Nim's rules for identifiers, it prevents you for mistakenly declaring two different variables/procedures with similar names.

Other examples where, how you would write something doesn't matter, is Windows filesystems and gmail, reserving every possible derivative with a dot like your.name@gmail.com and y.ourname@gmail.com.

So it's not uncommon or weird. Just programming languages are overly restrictive.

> That isn't true, you need parentheses for procs with no arguments, else it's interpreted as a proc pointer. Try `echo` vs `echo()`.

My bad, I meant 0 argument member functions (or rather 0 additional argument functions, apart from the this pointer). So `foo: Foo` and then `i = foo.len()` directly shows me that I'm calling a function, `i = foo.len` looks like I just access a member variable. len is obviously not the best example but I still don't think hiding the fact that a function is being called is a good idea at all.

> It's used for declaring variables of an object. `type Foo = object` and `var foo: Foo`.

Good point, I didn't consider that. So far never came up in my code but I can see that it sometimes can be useful to do something like that.

> So it's not uncommon or weird. Just programming languages are overly restrictive.

Natural languages have been case sensitive long before programming languages existed. Again, I'm not saying it's a bad feature - on the contrary, definitely appealing in some case - it's just not what we're used to and hence a bit "weird".

From Araq on Nim Forums:

> I decided to make Nim "space only" after having read an interview with Guido van Rossum who said that it is what he would do for Python if he were to decide it again. Also, and more importantly, back then I had never seen "tabs for indentation, spaces for alignment" applied correctly once. In fact, "compress 8 spaces into a tab" was quite common. (This is not the same as "tabs for indentation"!)

In other words, it seems Nim simply attempts to learn from Python's mistakes (even PEP8 discourages the use of tabs for new projects), mixing tabs with spaces brings several potential technical issues for a whitespace-sensitive language, so why not just sidestep that problem entirely?

Guido van Rossum also seems to prefer tabs but was held back by the community [1]. But even if he changed his mind since then those reasons are incredibly weak if you ask me, they basically boil down to "some software 50 years ago was pure shit" and that's not a valid reason. Tabs are simply the superior/right tool for indentation. You might want to see a tab 8 chars wide and I want 4 but we both see the loop body indented one level. With spaces enforced one of us would not see the code as they'd like it to be and while that might seem like pure preference on surface the simple truth is that preferences matter (see the recent thread about the ideal font for reading speed - it's different for everyone) and tabs provide the freedom while spaces don't. And in some cases they matter even more, for example I've worked with someone who was visually impaired and pretty much needed a tab size of 0 to be able to work, he wouldn't be able to do that with spaces.

The only argument that ever comes from spaces people is "alignment" and while I personally think that most if not all of the things people try to align don't get any benefit from that alignment whatsoever nobody advoces for tabs only, you can still always use spaces if you really really really need to align something. Some people messing that up is really not a reason to straightup ban the superior way, you could just train those people better. If you want to align stuff, go ahead. The thing that matters is that if I don't care about your alignment while you do and we both have different tab width preferences then with tabs we both get what we want while with spaces only you would.

Also while this was mostly about tabs vs spaces, those are not the only aspects of a style and the main point was that an enforced style will always upset some people. People simply work differently and intentionally preventing them from using whatever they like is pretty close to malicious if you ask me and in the best case still disrespectful. Some people work better in light mode, some prefer a complete rainbow IDE, some prefer to use raw vi, etc. Or for a better analogy, maybe someone codes much better while listening to loud death metal or whatever. Obviously in an office that would annoy other people but the solution is not to ban developers from listening to music completely but rather to make them use headphones in such cases so that other people are not affected. For code style I might work better with a certain one and you with a different style and that's okay, we just come up with some compromise style that the code is converted to before committing. Languages that enforce a certain style simply don't have this freedom.

Now in whitespace sensitive languages like Python if (and only if) you want to align stuff then yeah things can go boom. But it also is not an unsolvable problem, if the code is all spaces then it just works, if the code is all tabs with no "alignment" then it just works, and if you use tabs with some spaces because you want aligned stuff that can pretty much be checked by the parser. Error if a space is found before a tab, error if a line has more than one tab more than the previous one, etc. I don't think sidestepping this problem is worth alienating people.

[1] https://legacy.python.org/search/hypermail/python-1994q2/020...

Likely the Python-like/Whitespace-sensitive language issue is vastly more polarizing than people suspect. Possibly, if a person was introduced to a certain style with their first language (maybe second), it can become a preference. Then add to that, the compiler is enforcing a certain style, and perhaps a lot of people are being turned off/away.

I can't even count the number times, over the years, I've had the silliest discussions with people about Pascal syntax. Untold numbers of people used to C-like syntax could not get over begin...end, instead of {...}. Then, even in Pascal circles (and other languages), there was the space versus tab or K&R versus Allman style preferences.

It might seem trivial on the surface, but don't think it is. If a person has spent years working with things a certain way, that's what they are comfortable with. Depending on the person, it can take a lot or there has to be very strong incentives to pull them out of their comfort zone. People might look at Nim (or other languages), on just that aspect alone, and not be interested.

It takes time for the ecosystem to emerge... and I think Nim's future is very bright.
c_asEAnd_UnDeRscoReI_nSen_Sitivity doesn't get many fans
Yeah... using absurd names like that is on the developer, not the programming language. You can do useless stuff like that in any language.
One possible solution in Rust could be:

    enum Value {
        Fizz,
        Buzz,
        FizzBuzz,
        Number(usize),
    }

    impl std::fmt::Display for Value {
        fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
            match self {
                Value::Fizz => write!(f, "Fizz"),
                Value::Buzz => write!(f, "Buzz"),
                Value::FizzBuzz => write!(f, "FizzBuzz"),
                Value::Number(num) => write!(f, "{}", num),
            }
        }
    }

    const fn get_fizzbuzz_equivalent(number: usize) -> Value {
        if number % 15 == 0 {
            Value::FizzBuzz
        } else if number % 3 == 0 {
            Value::Fizz
        } else if number % 5 == 0 {
            Value::Buzz
        } else {
            Value::Number(number)
        }
    }

    fn main() {
        (1..100).for_each(|num| println!("{}", get_fizzbuzz_equivalent(num)));
    }
While this implements FizzBuzz it does not actually end up doing the work at compile time.

You annotate get_fizzbuzz_equivalent() with const, so Rust would evaluate that on constant inputs at compile time, but that's not very interesting since it's basically a switch.

The use of const here does not oblige Rust to somehow figure out everywhere you can use this function and do the work at compile time since the inputs might be (and are here) variables. Sure enough if you try in Godbolt you will see that eliding const makes no real difference.

Rust's const today is far less powerful than something like C++ constexpr, I suspect that you can't really do what Nim did in a reasonable way with Rust. You could I'm sure get there with build.rs and/or proc macros, but that's not really in the spirit of this exercise.

To elaborate, the parent doesn't call get_fizzbuzz_equivalent in a "const context", which would require it to be evaluated at compile time. So it's called at runtime like it didn't have `const`.

You can do something like the nim without build.rs or proc macros:

    #[derive(Debug, Copy, Clone)]
    enum Value {
        Fizz,
        Buzz,
        FizzBuzz,
        Number(usize),
    }
    
    const fn get_fizzbuzz_equivalent<const N: usize>() -> [Value; N] {
        let mut result = [Value::FizzBuzz; N];
    
        let mut i: usize = 0;
        while i < N {
            let n = i + 1;
            if n % 15 == 0 {
                result[i] = Value::FizzBuzz;
            } else if n % 3 == 0 {
                result[i] = Value::Fizz;
            } else if n % 5 == 0 {
                result[i] = Value::Buzz;
            } else {
                result[i] = Value::Number(n);
            };
    
            i += 1;
        }
    
        result
    }
    
    fn main() {
        const FIZZBUZZ: [Value; 31] = get_fizzbuzz_equivalent();
    
        println!("{:?}", FIZZBUZZ);
    }
    
There are certainly some ergonomic issues here; having to use while because for isn't usable in const contexts yet, which is annoying. But this does compute the whole array at compile time.

(Shout-out to https://stackoverflow.com/questions/67538438/const-array-fro... which I basically smashed together with OP's code to produce the above example.)

> having to use while because for isn't usable in const contexts yet

For people wondering why, both as Rust outsiders or Rust beginners:

Rust in some sense really only has one loop, just named "loop", which is an infinite loop like while(true) { } in various other languages. Other looping is just syntactic sugar for "loop" specifying some way to escape the loop. Rust's for loop is sugar for a "loop" that uses the IntoIterator trait to get an Iterator and then call next() on it each time the loop repeats, until it breaks out of the loop when next() returns None.

Unfortunately, Iterator::next() isn't a const function. Your actual implementation of next() for trivial data structures probably is constant, but it wants specifically Iterator::next() and that can't be labelled constant today because it's just an implementation of a trait and some other implementations are presumably not constant.

As a result, even though all Rust's loops are just "loop" and "loop" is allowed in a constant function, you can't use the for loop because Iterator::next() is never constant so now your function isn't either.

A proper fix for this would be pretty cool, but is not easy to do.

Yep, you're perfectly right of course. My "solution" was mainly to address the problem "how can I return a String from a const function"... the answer is that you don't have to :-)
You can do a lot with Nim at compile time, check out my talk on Nim Metaprogramming not just for FizzBuzz, but real world applications: https://fosdem.org/2022/schedule/event/nim_metaprogramming/

I am working an a macro to compile Nim code into GLSL. So not only can you write Nim to C or Nim to JS, it can also (in limited way) do Nim to GLSL GPU Shaders. See here: https://github.com/treeform/shady

I am also working on a macro system similar to SWIG, where using a some macros one can write a Nim library and generate wrappers for your NIM library for many languages like C, Python, JS, Ruby. See here: https://github.com/treeform/genny

Great, now do it with C-macros! ;)
"jai" is pretty cool in this regard, you can just huck whatever you want into a #run {} block and it'll get evaluated at compile time.
Has anyone in the general public used Jai for any project?
I have, nothing big and nothing finished—the beta now has 150 people in it.
(comment deleted)
Here is a complete example how to generate a compile time FizzBuzz array in C++. This includes converting the numbers to string. This example does through 50.

https://gcc.godbolt.org/z/1nTcdvWM5

        #include <limits>
        #include <array>
        #include <algorithm>
        #include <utility>
        #include <string_view>

        template<unsigned int Num>
        constexpr auto to_array(){
            constexpr auto digits = 
            std::numeric_limits<unsigned int>::digits10;
            std::array<char,digits> ar{};
            auto x = Num;
            int pos = 0;
            while(x > 0){
                ar[pos] = '0' + x % 10;
                x /= 10;
                ++pos;
            }
            std::reverse(ar.begin(),ar.begin() + pos);
            return std::make_pair(pos,ar);
        }

        template<unsigned int Num>
        struct to_string{
            constexpr static auto p = to_array<Num>();
            constexpr static std::string_view get(){
                return std::string_view(p.second.data(),p.first);
            }
        };

        template<unsigned int Num>
        constexpr std::string_view get_fizz_buzz(){
             if constexpr (Num % 15 == 0) {
                return "FizzBuzz";
            } else if constexpr (Num % 3 == 0) {
                return "Fizz";
            } else if constexpr (Num % 5 == 0) {
                return "Buzz";
            } else{
                return to_string<Num>::get(); 
            }
        }

        template<std::size_t... I>
        constexpr auto get_fizz_buzz_array_impl(std::index_sequence<I...>){
            return std::array{get_fizz_buzz<I+1>()...};
        }

        template<unsigned int Val>
        constexpr auto get_fizz_buzz_array(){
            return get_fizz_buzz_array_impl(std::make_index_sequence<Val>());
        }
        #include <iostream>
        int main(){
            static constexpr auto ar = get_fizz_buzz_array<50>();

            for(auto s:ar){
                std::cout << s << "\n";
            }


        }
Much better than my poor attempt, thanks for sharing.
Another language that supports (arbitrary) compile time code execution is Haskell through Template Haskell. Ex. using FizzBuzz:

  -- FizzBuzz.hs
  
  module FizzBuzz (fizzBuzz) where
  
  fizzBuzz :: Int -> String
  fizzBuzz n
    | n `mod` 15 == 0 = "FizzBuzz"
    | n `mod` 3 == 0 = "Fizz"
    | n `mod` 5 == 0 = "Buzz"
    | otherwise = show n
  
  -- Main.hs
  
  import FizzBuzz (fizzBuzz)
  import Language.Haskell.TH.Syntax (lift)
  
  main :: IO ()
  main = do
    print $(lift $ map fizzBuzz [1 .. 100])
As you can see, the fizzbuzz function does not need any special syntax or annotations. You can use any code at compile time. The only downside is that you need to separate the code into two modules.

Other solutions involve using the type system to do this kind of computation at compile time, but I think that TH is very powerful (maybe too powerful since you can run arbitrary IO at compile time).

To take on another language solution, here's an obvious one, albeit in a lisp (Clojure):

    (defn fizzbuzz-nth* [n]
      (cond
        (zero? (mod n 15)) "FizzBuzz"
        (zero? (mod n 3)) "Fizz"
        (zero? (mod n 5)) "Buzz"
        :else (str n)))
    
    (defn fizzbuzz* [n]
      (->> (range 1 (inc n))
           (map fizzbuzz-nth*)
           (interpose " ")
           (apply str)))
    
    (defmacro fizzbuzz [n]
      `(println ~(fizzbuzz* n)))
    
    (println "macroexpansion: ")
    (println (macroexpand '(fizzbuzz 100)))
    (println)
    (println "invoke the macro: ")
    (fizzbuzz 100)
Compile-time programming isn't any different than runtime programming in a lisp. This is nice because we can write the business parts as regular code, which is easy to try out in the repl or write tests for and then wrap it in a macro for compile-time evaluation if that's what the situation calls for.

For those not versed in lisp, `fizzbuzz-nth` and `fizzbuzz` are regular functions and `fizzbuzz` is a macro (aka a function annotated to be executed at compile time). The `fizzbuzz` macro makes use of Clojure's `syntax-quote` (the backtick) to signify templating is going on and `unquote` (the tilde) to escape the templating and execute some code at macro expansion time.

It's a real shame most languages don't have these metaprogramming facilities as judicious use makes weird, hard, and untestable stuff into common, easy, and another boring part of the test suite.

I'm really pulling for C++ to eventually get here with templates, particularly now that vectors can be constexpr and the result of templates. Maybe c++26 will finally give us reasonable macro facilities in a mainstream language.

Crystal's macro system is definitely worth comparing here, considering it has taken a very interesting / powerful approach to macros
Curly braces FTW. I like Python, but I recognise that the use of whitespace is problematical, especially when it comes to refactoring code. I have a shortcut in vim that nicely reformats my C code to consistent indentation.

begin...end, while no disaster, tends to hide the structure of the code visually.

So, curly braces it is, then.