Preaching to the choir here, but feel free to send your disbeliever programmer friends ;)
I've read the article yesterday. Great read, thanks!
Hopefully more people see it.
Great article, thanks for sharing!
For those wondering how could @moigagoo have read it already... it was feature on HN: https://news.ycombinator.com/item?id=34761735 ;)
I believe this article might convince many procedural persons, but I see no functional programming languages in there.
Have you tried to improve your Rust "Yahtzee bot" with the knowledge gained with the last versions (C, Nim) of your bot ?
Like Python, indentation matters, so I can't indent a bunch of code under a comment heading then collapse that in my editor.
Why not use block?
block would introduce a new scope, whereas my use for this would be more organizational. I find myself making a heading for collections of related functions like
# UTILS
# ------
func codeAndStuff() =
...
VSCode offers a ▸ toggle for each indentation level, and it's nice to hide away sections of code I'm not working on. I'd take a hack if there was one.
Templates don't introduce a new scope by default. (but they're hygienic, so any variables introduced inside the body of the template itself won't conflict with ones outside, but that doesn't apply to a block that you passed to the template).
So you could do something like this if you wanted:
template section(name, code: untyped) =
code
section foo:
proc sayHello*() =
echo "hello!"
section bar:
proc greet*() =
sayHello()
this is a little known feature, but the vscode plugin actually supports this via folding comments (which original came from Visual Studio with C++? Atleast I've encountered them there the first time):
#region
let iCanBeFoldedIn = 0
#endregion
@dlesnoff
I believe this article might convince many procedural persons, but I see no functional programming languages in there.
Well, the Nim version could be turned into something that looks more functional if more variables were defined to be immutable with let's instead of var's and more work were done to avoid mutable global state, which then could be fairly easily translated into a functional language.
The question would be which functional language to use:
@dlesnoff, @mode80:
Have you tried to improve your Rust "Yahtzee bot" with the knowledge gained with the last versions (C, Nim) of your bot ?
From the repos for the other languages which are adjacent to the Nim YahtzeeBot one, no, they haven't been upgraded from the lessons learned from the C/Nim versions or for code optimizations that might be specific to a given language given more knowledge of that language.
Specific improvements might be as follows:
That said, even the C/Nim execution times can likely be improved by using a thread pool; the Nim version shows signs that the threadpool package from the standard library was tried and likely found to be too unstable for use. An alternative would be to write some simple thread pool routines oneself, which would almost time per thread invocation by up to a half for a significant speed-up.
However, the above doesn't really affect the significance of the article which shows that Nim is generally easier to learn and apply effectively than the other languages with quite concise code forms, while offering the same (or faster) speed as C, although the article kind of glosses over that languages like Rust likely are "safer" (at least currently until Nim concepts get more stable/non-experimental) although the Nim version is written more "imperatively" than it needs to be and is perhaps less safe than it could be.
Wow, I'm glad other people found my exercise instructive .
I learned a lot as I went, and I didn't (always) go back and update previous attempts. A surprising "meta-lesson" was -- If I re-write something 7 different times, I still find opportunities to improve it on the 7th attempt!
Rust was my 2nd attempt, and I'm pretty sure I could get its speed in line with Julia's performance. But that came from the lessons learned using the excellent Julia profiler after I'd already "wrung out" Rust to the best of my natural ability. So it feels fair to credit Julia with that improvement.
The original Python version was very different and more functional .. When asked for the correct gameplay, it would start at the current gamestate and recursively backfill the calculations for all the dependent states with some memoization and no stateful "buffers". It was beautiful, but very slow vis a vis the "bottom up" nested loops I replaced it with.
Perhaps more functional languages would fare better, but my general experience was that the more imperative I made it, the faster it got. Also, less elegant. For example, it helped to turn split my collection of structs into separate arrays of primitives so that no cacheline space on the processor would be wasted. My guess is that sort of thing would be unnatural in e.g. Scala?
One of the surest ways to improve performance in each language was to dismantle safety when possible.
For a real project, I'd aim for more balance between comfort, safety, and speed. I just became curious about what was possible at the bleeding edge.
As I recall C# didn't get much love on the final tuning or even multithreading as it was pretty far behind, and I was eager to try Swift next. The whole exercise wasn't really a "fair fight" outside the goal fitting my personal preferences.
Nim surprised me at being 1) the fastest, 2) the most versatile, and 3) the prettiest. (except for Python which was also the slowest and least versatile.) Also the most fun! That matters too.
@mode80:
Perhaps more functional languages would fare better, but my general experience was that the more imperative I made it, the faster it got. Also, less elegant.
That is sometimes true up to a point, but in a mature functional language such as Haskell there are often ways to get both. In GHC Haskell we can choose to use "unsafe" functions that (for instance) don't do bounds checking and (unsafely) convert an array between a non-nomadic persistent form and a monadic mutable without copying as well as drop to using low level primitives which can even include Address/pointer operations, while still following the form of functional programming.
For example, it helped to split my collection of structs into separate arrays of primitives so that no cacheline space on the processor would be wasted. My guess is that sort of thing would be unnatural in e.g. Scala?
I can't speak to Scala as it has been a few years since I dabbled in it (now preferring F# of the JIT-based functional languages, if I must), but in Haskell the way to get speed in array processing is to use arrays of "unlifted/unboxed" primitives and thus splitting the structures/records into arrays of primitives just as you ended up doing in Nim. So at least in Haskell, this is the natural way to obtain speed.
Nim surprised me at being 1) the fastest, 2) the most versatile, and 3) among the prettiest. Also, the most fun! That matters too.
Of course Nim should be fast and the minor difference of a few seconds can happen just in that the Nim compiler might be slightly re-ordering the output C code.
I'll agree with you that Nim is both versatile and fairly pretty, and can be fun as long as you don't run into issues with unstable features. I see that you avoided the deficiencies in multi-threading by using the very lowest threading primitives that basically just call pthreads, and that, unfortunately, is what one often needs to currently do for multi-threading (as per your experiment with the threadpool package).
Although Nim does offer a surprising number of features that are implemented as functional forms of code, it can never be a fully functional language as long as the whole language philosophy embraces using mutation as in var parameters and return values, etc. as a natural progression from Algol. It seems to me that there is room for a fully functional language that isn't as bulky and hard-to-learn as Haskell but is just as purely functional while generating C/C++ code as it's back-end using Nim-like Arc-type smart reference counting instead of garbage-collection memory management and without over-emphasizing list and lazy list processing, but we don't have one yet. The Elm language has the simple functional syntax and purity, but is restricted to generating JavaScript and has other severe restrictions such as only supporting persistent data types, etc. Someday we might have an easy-to-learn Elm-like pure functional language that has the power of Haskell and the speed of Nim due to emitting C/C++ as a back end...
Regarding the slow speed of Python: If you use pypy you can get a speedup of about 6x or more (which is still quite a bit slower than nim). Using the classical fibonacci benchmark (no optimizations) the results for fib(45):
python 3.9.16: 170 seconds pypy7.3.9 : 10.7 seconds nim 1.6.10 : 1.6 seconds (used d:release)
In Anaconda you can have a python activation area and a separate pypy activation area. You switch areas depending on what you want. pypy has most of the Python packages, but occasionally you will have to do more work (consulting the web) to get a pypy package to install. Anaconda does a pretty good job of keeping all the version requirements straight.