18 ms·
Compile time evaluation in Nim, Zig, Rust and C++
- winrid 5y agoNim continues to impress.
- pjmlp 5y agoWith 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 5y agoNo, 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 5y agoCompiler Explorer is your friend, https://godbolt.org/z/94hzK8svE 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 5y agoYour example only works because the optimizer eliminates your call to std::to_string. Call get_fizzbuzz(11) and you'll see the error.
- pjmlp 5y agoI stand corrected, however I have a couple of ideas to try later on.
- cornstalks 5y agoYou’re gonna have a bad time if you return a string_view to a locally created string.
- pjmlp 5y agoIt is compile time, so the expectation is that the compiler replaces it anyway, although you might be right.
- MaulingMonkey 5y ago`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>());
- pjmlp 5y agoThat 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.
- jmyeet 5y agoBased 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 5y agoLike, no. Have you never programmed Python?
- jmyeet 5y agoI'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?
- secondcoming 5y agoOh I see, I thought you were referring to language features rather than code correctness.
- deleted 5y ago[deleted]
- Cloudef 5y agoThe compile time in zig and nim is breath of fresh air, while rust seems similar spaghetti mess as c++
- lionkor 5y agoI 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__ 5y agoconstexpr 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.
- pjmlp 5y agoD 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.
- rat9988 5y agoSomehow 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.
- pjmlp 5y agoMostly because unless I am going crazy, the comment was different when I posted my remark.
- bachmeier 5y ago
- vegai_ 5y agoNim 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.
- skywal_l 5y agoQuickly 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 5y agoI 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 https://en.m.wikipedia.org/wiki/Uniform_Function_Call_Syntax Python comparison example: https://narimiran.github.io//2018/05/10/python-numpy-nim.html https://narimiran.github.io//2018/05/10/python-numpy-nim.htm...
- coliveira 5y agoARC is not new, it was used by Apple in their languages for at least 10 years now.
- stefano_c 5y agoOne 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 5y agoWhile 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 5y agoTo 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-from-range https://stackoverflow.com/questions/67538438/const-array-fro... which I basically smashed together with OP's code to produce the above example.)
- treeform 5y agoYou 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/ 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 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 https://github.com/treeform/genny
- tjoff 5y agoGreat, now do it with C-macros! ;)
- adamrezich 5y ago"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 5y agoHas anyone in the general public used Jai for any project?
- adamrezich 5y agoI have, nothing big and nothing finished—the beta now has 150 people in it.
- pabs3 5y agoAre there any compilers that let you run arbitrary code (like running external processes) at compile time?
- valcron1000 5y agoHaskell's GHC allows you to do anything at compile time. Literally anything.
- cb321 4y agoNim has staticRead and staticExec to run external processes. [1] [1] https://stackoverflow.com/questions/55891650/how-to-use-slurp-gorge-staticread-staticexec-in-the-directory-of-the-callsite https://stackoverflow.com/questions/55891650/how-to-use-slur...
- deleted 5y ago[deleted]
- jbandela1 5y agoHere 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 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 5y agoMuch better than my poor attempt, thanks for sharing.
- valcron1000 5y agoAnother 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 5y agoTo 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.
- sam0x17 5y agoCrystal's macro system is definitely worth comparing here, considering it has taken a very interesting / powerful approach to macros
- blippage 4y agoCurly 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.