HN user

arc619

170 karma
Posts0
Comments52
View on HN
No posts found.

Nim too, as it can use Zig as a compiler.

There's also https://github.com/treeform/shady to compile Nim to GLSL.

Also, more generally, there's an LLVM-IR->SPIR-V compiler that you can use for any language that has an LLVM back end (Nim has nlvm, for example): https://github.com/KhronosGroup/SPIRV-LLVM-Translator

That's not to say this project isn't cool, though. As usual with Rust projects, it's a bit breathy with hype (eg "sophisticated conditional compilation patterns" for cfg(feature)), but it seems well developed, focused, and most importantly, well documented.

It also shows some positive signs of being dog-fooded, and the author(s) clearly intend to use it.

Unifying GPU back ends is a noble goal, and I wish the author(s) luck.

Behaviours don't have to be decoupled from the data they operate on. If I write a procedure that takes a particular data type as a parameter, it's a form of coupling.

However, there's no need to fuse data and code together as a single "unit" conceptually as OOP does, where you must have particular data structures to use particular behaviours.

For example, let's say I have a "movement" process that adds a velocity type to a position type. This process is one line of code. I can also use the same position type independently for, say, UI.

To do this in an OOP style, you end up with an "Entity" superclass that subclasses to "Positional" with X and Y, and another subclass for "Moves" with velocity data. These data types are now strongly coupled and everything that uses them must know about this hierarchy.

UI in this case would likely have a "UIElement" superclass and different subclass structures with different couplings. Now UI needs a separate type to represent the same position data. If you want a UI element to track your entity, you'd need adapter code to "convert" the position data to the right container to be used for UI. More code, more complexity, less code sharing.

Alternatively, maybe I could add position data to "Entity" and base UI from the "Positional" type.

Now throw in a "Render" class. Does that have its own position data? Does it inherit from "Entity", or "Positional"? So how do we share the code for rendering a graphic with "Entity" and "UIElement"?

Thus begins the inevitable march to God objects. You want a banana, you get a gorilla holding a banana and the entire jungle.

Meanwhile, I could have just written a render procedure that takes a position type and graphic type, used it in both scenarios, and moved on.

What do I gain by doing this? I've increased the complexity and made everything worse. Are you thinking about better hierarchies that could solve this particular issue? How can you future proof this for unexpected changes? This thinking process becomes a huge burden to make brittle code.

you risk a procedural programming style lacking the benefits of encapsulation. This can increase the risk of data corruption and reduce data integrity...

You can use data encapsulation fine without taking on the mantle of OOP. I'm not sure why you think this would introduce data corruption/affect integrity.

There's plenty of compositional and/or functional patterns beyond OOP to use beyond procedural programming, but I'd hardly consider using procedural programming a "risk". Badly written code is bad regardless of the pattern you use.

That's not to say procedural programming is all you need, but at the end of the day, the computer only sees procedural code. Wrapping things in objects doesn't make the code better, just more baroque.

Unfortunately, while OOP promises code reuse, it usually makes it worse by introducing boundaries as static architecture.

OOP's core tenet of "speciating" processing via inheritance in the hope of sharing subprocesses does precisely the opposite; defining "is-a" relationships, by definition, excludes sharing similar processing in a different context, and subclassing only makes it worse by further increasing specialisation. So we have adapters, factories, dependency injection, and so on to cope with the coupling of data and code. A big enough OOP system inevitably converges towards "God objects" where all potential states are superimposed.

On top of this, OOP requires you to carefully consider ontological categories to group your processing in the guise of "organising" your solution. Sometimes this is harder than actually solving the problem, as this static architecture has to somehow be both flexible yet predict potential future requirements without being overengineered. That's necessary because the cost to change OOP architectures is proportional to the amount of it you have.

Of course, these days most people say not to use deep inheritance stacks. So, what is OOP left with? Organising code in classes? Sounds good in theory, but again this is another artificial constraint that bakes present and future assumptions into the code. A simple parsing rule like UFCS does the job better IMHO without imposing structural assumptions.

Data wants to be pure, and code should be able to act on this free-form data independently, not architecturally chained to it.

Separating code and data lets you take advantage of compositional patterns much more easily, whilst also reducing structural coupling and thus allowing design flexibility going forward.

That's not to say we should throw out typing - quite the opposite, typing is important for data integrity. You can have strong typing without coupled relationships.

Personally, I think that grouping code and data types together as a "thing" is the issue.

It will not emit warnings saying it did that.

You're right. I was sure I read that it would announce when it does a copy over a sink but now I look for it I can't find it!

The static analysis is not very transparent.

There is '--expandArc' which shows the compile time transformations performed but that's a bit more in depth.

Nim is stack allocated unless you specifically mark a type as a reference, and "does not use classical GC algorithms anymore but is based on destructors and move semantics": https://nim-lang.org/docs/destructors.html

Where Rust won't compile when a lifetime can't be determined, IIRC Nim's static analysis will make a copy (and tell you), so it's more as a performance optimisation than for correctness.

Regardless of the details and extent of the borrow checking, however, it shows that it's possible in principle to infer lifetimes without explicit annotation. So, perhaps C++ could support it.

As you say, it's the semantics of the syntax that matter. I'm not familiar with C++'s compiler internals though so it could be impractical.

To be fair, you've posted a toy example. Real games are often chains of dependent systems, and as complexity increases, clean threading opportunities decrease.

So, while yes it's nice in theory, in practice it often doesn't add as much performance as you'd expect.

Native Nim libs are definitely nicer, but being able to output C/C++/JS/LLVM-IR with nice FFI means you can access those ecosystems natively too. It's one reason the language has been so great for me, as I can write shared Nim code that uses both C and JS libs (even Node) in the same project.

Personally, I think Python's success is down to the productivity of its peudocode-like syntax letting you hack prototypes out fast and easy. In turn, that makes building libraries more attractive, and these things build on each other. FORTRAN is very fast but it's a less forgiving syntax, especially coming from Python.

In that regard, I'm surprised Nim hasn't taken off for scientific computing. It has a similar syntax to Python with good Python iterop (eg Nimpy), but is competitive with FORTRAN in both performance and bit twiddling. I would have thought it'd be an easier move to Nim than to FORTRAN (or Rust/C/C++). Does anyone working in SciComp have any input on this - is it just a lack of exposure/PR, or something else?

Interestingly, Delphi Pascal has a single pass compiler with generics, though I'm not sure about type inference.

I was under the impression Go originally avoided generics more for a perceived abstract complexity for developers, the idea being they are hard to understand for new recruits.

Nim 2.0 3 years ago

Shoot me an email at arctsint@proton.me Cheers!

Nim 2.0 3 years ago

Types are stack allocated by default.

"var data: MyObject" is on the stack. "var arr: array[1000, MyObject]" is allocated on the stack sequentially.

Only dynamic seq or ref types use the heap by default.

Nim 2.0 3 years ago

Reference semantics are part of the type.

So "var i: int" is value, "var i: ref int" is a heap allocated reference that's deterministically managed like a borrow checked smart pointer, eliding reference counting if possible.

You can turn off GC or use a different GC, but some of the stdlib uses them, so you'd need to avoid those or write/use alternatives.

Let me say though, the GC is realtime capable and not stop the world. It's not like Java, it's not far off Rust without the hassle.

Nim 2.0 3 years ago

Too much string copying iirc. It was written a while ago.

Hopefully it'll get updated/replaced some time, but there's plenty of faster 3rd party ones already.

Nim 2.0 3 years ago

Of all the recent changes, default values is my favorite. Aside from generally useful and further reducing the need for initialisation boilerplate, I lets us guarantee valid state at compile time for things like enums - and, I assume, object variants?

Nim 2.0 3 years ago

Looking forward to trying out this release!

After programming professionally for 25 years, IMO Nim really is the best of all worlds.

Easy to write like Python, strongly typed but with great inference, and defaults that make it fast and safe. Great for everything from embedded to HPC.

The language has an amazing way of making code simpler. Eg UFCS, generics, and concepts give the best of OOP without endless scaffolding to tie you up in brittle data relationships just to organise things. Unlike Python, though, ambiguity is a compile time error.

I find the same programs are much smaller and easier to read and understand than most other languages, yet there's not much behind the scenes magic to learn because the defaults just make sense.

Then the compile time metaprogramming is just on another level. It's straightforward to use, and a core part of the language's design, without resorting to separate dialects or substitution games. Eg, generating bespoke parsing code from files is easy - removing the toil and copypasta of boilerplate. At the same time, it compiles fast.

IMHO it's easier to write well than Python thanks to an excellent type system, but matches C/C++ for performance, and the output is trivial to distribute with small, self contained executables.

It's got native ABI to C, C++, ObjC, and JS, a fantasic FFI, and great Python interop to boot. That means you can use established ecosystems directly, without needing to rewrite them.

Imagine writing Python style pseudoocode for ESP32 and it being super efficient without trying, and with bare metal control when you want. Then writing a web app with backend and frontend in the same efficient language. Then writing a fast paced bullet hell and not even worrying about GC because everything's stack allocated unless you say otherwise. That's been my Nim experience. Easy, productive, efficient, with high control.

For business, there's a huge amount of value in hacking up a prototype like you might in Python, and it's already fast and lean enough for production. It could be a company's secret weapon.

So, ahem. If anyone wants to hire a very experienced Nim dev, hit me up!

I should add that Nim still still has a 'separate language' for types, so doesn't quite fit OP's bill. Nevertheless, it's quite easy to build type constructs using statically resolvable expressions.

I'd love to know if anyone could reproduce the N-queens example in Nim: https://www.richard-towers.com/2023/03/11/typescripting-the-...

I believe it is possible, but don't have the time to try it out.

The Nim compiler includes a simple linear equation solver, allowing it to infer static params in some situations where integer arithmetic is involved.

From: https://nim-lang.org/docs/manual_experimental.html#concepts-...

That was fascinating, thanks. Please do consider posting this here as its own article, it would be interesting to read others comments.

My reading of this - and please correct me if I'm wrong, I'm still learning - is that you're extracting hyper-parameter planes from the data flow in the model's embedding space?

Its really exciting to think of the hidden knowledge and relationships we could extract from our own linguistic interactions.

Nim 2.0.0 RC2 3 years ago

Your argument applies to style sensitivity as well to be fair - do you search for 'MyTestFunction' or 'my_TestFunction'... or was it 'My_TestFunction', maybe 'm_Y_tEsT_fUnCtION'?

Style insensitivity lets you automate your local code (and/or through CI) to consistency for easy searching, even when external libraries YOLO their own style.

It's incompatible with a lot of search-engines

Google and search engines are case insensitive, and mostly ignore underscores too. Let's be honest, how often would you just search for 'testfunction libraryname' on the web and get the signature you need?

My experience from using the language in production for years is that case insensitive searches will find what I want in Nim code specifically because no one can use case/underscores to make things make unique. Instead, the language uses overloading through the type system to distinguish things. This is a far more succinct, safe, and expressive way of writing code from my perspective.

It's worth noting that the first letter of an identifier is case sensitive, with the convention for types to start with a capital letter. So, you can write 'type Test = object' and 'proc test(t: Test)', then use them with 'var test: Test; test(test)' without any ambiguity, and with code completion/jump to source in VSCode (and others).

If you then add 'proc test()' with no arguments, it's still unambiguous because the functions have different signatures, so you can then write 'test()' and your second function is called.

If something is ambiguous, it's a compile time error. If you want, you can prefix modules like Python, but there's never any need. If you have two libraries with the API, same types, and function signatures (such as using two async libraries in the same program), you can just slap a generic '[T]' so it's lazily instantiated at the call site and move on.

IMHO relying on casing/underscore to distinguish identifiers is objectively worse than using the type system to statically and unambiguously prove your intent, and normalising names removes a class of identifier confusion bugs to boot. Even style guides in case sensitive languages beg us not to use case and underscores to disambiguate symbols for the confusing code it creates, and to be honest, I've yet to see a compelling argument in favour of case sensitivity beyond simply being what people are used to.

Zig and Rust 3 years ago

That's a a compelling argument: GC/RC w/ stack allocation where possible.

Indeed, it's a great combination that means you're productive and performant without really trying most of the time. The type system is really good as well, and in general there's a strong focus on compile time over run time like Zig, but with better procedural macros than Rust.

To what extent is this possible? Does Nim have LTOs that rewrite memory handling across compilation units?

The GC is built on general move analysis with destructors you can hook for your own types. The nice thing is it's a deterministic, compile time expansion (you can view with '--expandArc:somefunction'), so it's useful for embedded or high performance stuff.

The intro to ARC is pretty good for an overview: https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...

Zig and Rust 3 years ago

Also, aside from the fact GC is optional in Nim, what are you thinking of that cant be done with a GC?

Zig and Rust 3 years ago

Nim uses stack allocated value types by default, and GC (or ptr) is an optional tag to the type definition.

GC types use borrow & move analysis like rust (not as good yet tho) so it can elide GC work when possible. GC is also not stop-the-world, deterministic (assuming no cycles), and configurable to soft realtime performance.

So you dont need to use the GC, but it will give you RAII if you want without needing to perform the dance of the borrow checker.

BTW Nim is also closing in on Rust's borrow checking semantics, too!

Agree with all of this. The Nim language might fit the bill of treating humans as first class citizens, and certainly fits better with my need for productivity and performance without over-specifying details.

The language focuses on readability first and borrow checking is an automatic compiler optimisation behind the scenes, with full type bound, constrained borrowing/move semantics being opt in as required.

There's loads of sensible ergonomics like this, for example large immutable parameters are automatically passed as pointers for you while the compiler enforces the immutability in the code, making things simple, efficient, and safe by default, with less cognative overhead or need for these kinds of micro-optimisations leaking into the architecture.

Saying that, Rust‘s multithreading story is better for now as it currently offers wider aliasing protection although arguably at the cost of developer friction.

TBF the reply in that link was written 4 years ago, before the language went 1.0

Nim lacks official support to multiple inheritance or interfaces

Multiple inheritance is a bit of a trainwreck IMO (see diamond problem).

The language isn't designed around OOP, and is instead procedural with metaprogramming for extension. You get more bang for your buck this way, but for people with their head in inheritance it’s probably a shock.

UBI is added on top of work so work will always mean you're better off.

Would you give up your job just to spend all day doing nothing, or would you end up doing something productive?

If you had infinite money, would you do nothing?

What do uber rich people do all day? Why do they work?

With noting that the GC in Nim (not "stop the world" BTW) is attached to the type. Unless you declare a type as `ref`, you're using stack allocation.

Even if you do use the GC, the compiler elides GC work if the type doesn't escape the scope.

They're probably laughing because a) you're suggesting manually doing the work static typing does in a dynamic language because its untenable not to for large projects, and b) you can't easily add type hints to other people's libraries.