Brian Lovin
/
Hacker News
Daily Digest email

Get the top HN stories in your inbox every day.

treeform

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

vegai_

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.

ryukoposting

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.

jdrek1

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.

MercurialBlue

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?

Tozen

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.

ryukoposting

> 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.

vaylian

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?

ryukoposting

> 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".}

verdagon

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

pjmlp

Because comparing grammars and semantics is meaningless without an ecosystem and killer use case to come along with the language.

skywal_l

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?

elcritch

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...

coliveira

ARC is not new, it was used by Apple in their languages for at least 10 years now.

MercurialBlue

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.

dom96

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
    )

throwaway234272

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...

MercurialBlue

>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).

poulpy123

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

undefined

[deleted]

indymike

It takes time for the ecosystem to emerge... and I think Nim's future is very bright.

polotics

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...

renox

Same with D..

lenkite

c_asEAnd_UnDeRscoReI_nSen_Sitivity doesn't get many fans

dinHeld

Yeah... using absurd names like that is on the developer, not the programming language. You can do useless stuff like that in any language.

jbandela1

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";
            }


        }

pjmlp

Much better than my poor attempt, thanks for sharing.

valcron1000

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).

emidln

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.

Cloudef

The compile time in zig and nim is breath of fresh air, while rust seems similar spaghetti mess as c++

lionkor

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?

mhh__

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.

pornel

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.

epage

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.

pjmlp

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.

anecd0te

> 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.

undefined

[deleted]

flukus

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.

elcritch

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...

olodus

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...?

flukus

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.

pjmlp

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
    }

andreidd

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.

pjmlp

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.

andreidd

Your example only works because the optimizer eliminates your call to std::to_string.

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

cornstalks

You’re gonna have a bad time if you return a string_view to a locally created string.

deschutes

Have to defer conversion to std string to runtime https://godbolt.org/z/rWKEr4P6h

nemothekid

>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.

jmyeet

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?

secondcoming

Like, no. Have you never programmed Python?

jmyeet

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?

stefano_c

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)));
    }

tialaramex

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.

steveklabnik

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.)

tialaramex

> 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.

stefano_c

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 :-)

winrid

Nim continues to impress.

undefined

[deleted]

adamrezich

"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.

rishav_sharan

Has anyone in the general public used Jai for any project?

adamrezich

I have, nothing big and nothing finished—the beta now has 150 people in it.

blippage

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.

pabs3

Are there any compilers that let you run arbitrary code (like running external processes) at compile time?

cb321

Nim has staticRead and staticExec to run external processes. [1]

[1] https://stackoverflow.com/questions/55891650/how-to-use-slur...

valcron1000

Haskell's GHC allows you to do anything at compile time. Literally anything.

Daily Digest email

Get the top HN stories in your inbox every day.