Criticizing Hare language approach for generic data structures
ayende.com
ayende.com
But that's not "modern" applications, which often deal with a ton of data structures. C applications that do so usually have a few "template headers" lying around (indeed, certain operating systems ship with them), though one of the most frequently used data structures in C is of course the intrusive linked list. Which is not a good data structure to use in most cases. Why is it used so much in C? Because it is easiest to implement. Why does that matter? Because C makes implementing generic data structures tedious. Is it a good idea to replicate that model?
qed.
Hare clearly isn't like that: it's _actually_ aimed at (FOSS) C programmers, almost stubbornly so, and isn't going to appeal to many others. But for those people I can see C-with-tagged-unions being an improvement, and there's value in finding the minimal diff that makes a language better.
[1] Remember when Go called itself a "systems programming language"? Neither community seems to use this phrase any more, though.
What I like about go is that I can go full unsafe.Pointer if i want to, and do whatever I want.
The other thing I really like, is that they did such a good job discouraging it that i frequently hear people complain about go having pointers but not even letting you have fun with them.
The problem isn't that go doesn't give you enough control, the problem is that it's garbage collected. And you'd need to work around the GC. It's doable and people have done it though. Good idea? Maybe not.
Still waiting for someone to make "go but with ownership and borrow checker"
I'm no a systems programmer unless you count hobby microcontroller projects, but my understanding is that, when a systems programmer is talking about control, they tend to be talking first and foremost about keeping tight limits on memory usage. Pointers and suchlike just happen to be key tools that help you do that.
https://www.withsecure.com/en/solutions/innovative-security-...
That would have interested me more a few years ago, but I'm deeply invested in Rust these days.
Is that all you'd change about Go? One of the reasons I moved was the error handling and lack of generics. Go has fixed the latter, but I can't stand not having Result<> and Option<> these days. The '?' is awesome too.
No, go has plenty of warts, like every language (ever formatted a date in go?). I just don't like kitchen sink languages.
Leaving aside the GC, until Go has a real FFI it's hard to to imagine it being a really great fit for systems programming when the systems are all not written in Go.
I’m not bashing Go though. I appreciate the focus on readability among other things. It’s a fine language for the use cases it was actually designed for, like pushing petabytes of ad data to serving clusters all around the world with acceptable latency and reliability.
I guess the Genera, Xerox PARC, ETHZ, Microsoft folks are borderline dishonest.
Writing compilers is not systems programming in the sense that it requires a systems programming language, no. One could easily write a C compiler in Ruby, but we don't consider Ruby to be a systems programming language.
Thus, obviously, Go, despite not being a systems programming language, could be used to write the compiler for a systems programming language. I guess that is what Tamago is? I'm not going to read through the source to find out and the web page you linked is boring marketing copy.
TamaGo is a bare metal runtime for unikernels, whose main use is a commercial product for secure USB keys, sold by F-Secure.
Back on my youth, writing compilers was considered systems programming, in parity with kernel drivers, how things change.
I guess writing userspace drivers in hybrid kernels is also not considered systems programming.
Possibly related, building against foreign objects and manually setting up the FFI call: https://words.filippo.io/rustgo/.
What was good for C while it was gaining adoption, surely is good enough for Go.
Wouldn’t this be just a lesser version of rust then? If someone makes a new language and wants it to catch on, it needs new ideas. Not just nice simple syntax mixed with something someone else is already doing.
That depends if you consider C++ an upgrade over C :)
Many flamewars have been had on this topic and i think it's just a matter of personal taste.
I'm firmly in the less is more camp.
OTOH, if the differences are minimal, then the benefits are likely to be correspondingly minimal. And either way, the compiler/interpreter for a relatively new language will by definition be much less battle-tested.
Practically speaking, I think if a new language is going to compete with C, it needs to offer more than a few incremental improvements.
A lot of so called "modern" languages make a choice for you. Here is a hash map, over there is a class, have a tuple, set, etc. And they all have a specific way to be used.
In implementing all that, many languages lose on flexibility while gaining general utility. Hare seems to go in the opposite direction, one that makes programmers think hard about the solutions to their problems.
And that is, IMHO, a good thing.
Maybe I would come up with something like this if I only ever worked on Linux software using C, but outside those niches these choices seem weird.
You can declare your language feature complete sooner
There's a reason we call it "dependency hell" and not "happy fun fussing with dependency version conflicts among projects that are completely unrelated except that you happen to be using the same computer to work on them time."
What I keep daydreaming of is a package manager that resolves and download packages, but doesn't automatically grab transitive dependencies. I don't even want it saying, "Hey, we need to grab these 15 other ones, too, is that OK?" I don't want to hide the pain of huge dependency graphs like that; I want to feel it acutely. Give me an error message saying, "Oops I couldn't add FancyPackage because it depends on X, Y and Z transitive dependencies," and send me on a fetch quest. And I want the whole community around the language I'm working in to feel it acutely like that. That way we're all in the same boat, and collectively incentivized not to create the problem in the first place.
Now, keeping your language design stable and backward-compatible is a no brainer. Given that, it's possible to handle "dependency hells" simply by building from source.
Hare is designed to be ultra conservative, so you don't have to worry about dependency hells. Features are in code, not builtin into the language.
It is a win-win situation.
If I'm writing for a proprietary OS, that's probably Windows or OS X. Both of which give me a more stable target to aim at when it comes to what dependencies I can expect.
If I'm writing for an open source OS, that's probably some flavor of Linux. But which flavor? And which package manager does it use? And do they support all the dependencies I need, in the versions I need? Without pulling non-standard package repositories into the mix?
When open source software has difficulty working on proprietary OSes, I think it's usually not because of the package manager. More often it's because of something like a glibc dependency. At which point the domain of support isn't really "open source OSes", it's usually something more like "glibc-based linux distributions."
Hare is a systems programming language. What kind of a systems programmer doesn't know how to build from source?
Hare does the smart thing - it decouples the library distribution model from the language.
After all, it's the programmers job to make sure that distribution and packaging of his/her library is as simple as possible.
I think that Hare's workflow and usecase encourages having very few or no dependencies.
That means, in most cases, you should just be able to download a project's source, build and use it right away.
Many Hare programs will not need to have dependencies at all. We encourage a more conservative approach to dependencies than is common in many modern languages such as Cargo, PyPI, npm, Go, etc. Even dependency-heavy Hare projects will not have hundreds of dependencies, but maybe dozens at most. Think more C, not Node.
I can definitely see the problems with the Node "explosion of modules" approach, but if we're looking at fixing issues with C then IMO "everyone implementing their own data structures and not always with the care and attention they deserve" is right up there with things like lack of proper arrays/strings and poor null handling.
It seems like it ought to be possible to find a middle ground where dependencies are easy to find, install, update and integrate with a standardised build system, but where there is a cultural norm of being conscious of dependency size and not using micro-dependencies
A really common optimization is to replace a linear scan on a list with a hash table. In most languages, that is a trivial step to do, which means that it gets done.
There isn't any overhead.
With hare, you just blew your complexity budget on this thing.
And for the record, Hare does have built-in growable slices, so linked lists are pretty rare in Hare. They exist in a few niche situations -- I only ever wrote a Hare program with linked lists once.
It seems you can grow them by appending, so they aren't like Rust's slice which is non-owning or a typical array which can't grow.
I suspect a Linked List is actually almost never the right shape for a data structure in Hare, because you should just use these vectors to do that.
The two actual rationales for Linked List in the 21st century (other than "I was writing C and I don't know what I'm doing") are: 1. Concurrency, Lock Free and sometimes even Wait Free algorithms are practical for Linked List and the other costs pale into comparison on highly contended data structures and 2. I'm doing some serious acrobatics with huge lists, I constantly perform split/ merge operations and so those being cheap is crucial. Hare doesn't have concurrency, and nobody should attempt such acrobatics in a language this dangerous.
Also, did you use a nullable pointers to represent the next node element in the linked list? Or a tagged union of ( ptr | void)? Or something else?
https://git.sr.ht/~yerinalexey/carrot/tree/master/item/conso...
Went with nullable pointer types, which is also a rarely used feature in Hare.
maps/sets are also fundamentally building blocks for a lot of algorthms and structures, not having them makes it WAY harder to implement them. actually potential impossible as performance can easily degrade to a point where it's unusable when you use O(n) (or worse) lookups instead of O(1) ones.
this is not a case of premature optimizations but of missing fundamental building blocks
and if you don't want genetics, ok, do it like go did initially make the map special language thingy.
He is making a language he likes and wants to use. There are probably some people who like it and find it useful. Good for them! A lot of us have dreams of building our own language. He is following his passions and people are just lining up to be Debbie Downers.
Now I almost feel like I need to learn more Hare to give the guy some encouragement and positive feedback on things done right. Be the glass half full guy.
Well I won’t. I think Zig will be the last C-like language I invest time in. It is so easy to get sucked into something new that will never go mainstream.
Because this language isn't made by a nobody. It's made by a popular person with a following, so the language _will_ get used by a non-insignificant number of people. And those people _will_ (re)implement all the things that you want a language to have (database connections, networking libraries, (de)serialization libraries, etc.). And these things, which can be performance/security-critical, will be impacted by the issues people are pointing out.
Once we reach this plausible scenario, it's likely that some software that you want to use on, say, your server, will depend on those libraries. And _then_, like it or not, _you_ will be impacted by the choices of the language and be subject to whichever bugs all those people made when reimplementing (in this case) hash tables.
We could assume that everyone writing libraries in this language will be an expert and not make mistakes, but that is just a non-starter. In a sufficiently-large ecosystem, the people writing code will come from all sorts of backgrounds and have all levels of expertise, so you _will_ get buggy code in important libraries, and those bugs are the stuff most CVEs are made of nowadays.
I get with what you are saying that people are free to develop a language as they choose, and I agree with you there---I'm building my own and it's _not_ a modern language---but there can be consequences in the larger industry, and that's the "scary" part.
This language is only available on platforms where you are free to choose whatever software you want. If you think a Hare program is insecure, don't use it.
Let's say it's 2030, and I want to host my own matrix server. I would first compile a list of all matrix server implementations, and filter out those with confirmed bugs, and only then choose one (at random or some other criterion). The ones with bugs introduced by some library written in Hare/C/Rust/C++/X language will never be popular enough to even end up on the starting list cause people don't want to use buggy software (at least if there are options and in sufficiently-large ecosystem, there are options). And you might say that those bugs will go unnoticed - yes, but no more unnoticed than any other bug introduced by faulty logic but written in, say, Rust.
Also:
> so the language _will_ get used by a non-insignificant number of people
Hare is fairly opinionated (e.g. it mainly targets FOSS operating systems) so saying that it _will_ be used _just_ as a result of being related to ddevault (a person with a following), is a _wild_ guess. It does not matter how big of a following the author has, if the language itself is bad, it will not get used, or at least, it will get forgotten soon enough.
>In a sufficiently-large ecosystem...
In a sufficiently-large ecosystem, performance/security-critical software will be created using a language that is used not because of the author's following… Again: if the language itself is bad, it will not get used, or at least, it will get forgotten soon enough.
You can write buggy code in _any_ language.
If the industry makes mistakes, that's not news.
If Drew and Co want to make Hare the way it is, awesome! If you don’t want to use share and prefer something else, great!
I understand some people will not want or value any given piece of software or infrastructure, but the blanket pessimism is not proportional to the alleged detriments proposed by Hare’s mere existence.
Maybe I am just being overly sensitive to what I see as over reaction to someone’s programming language preference, but this is the reason I have never, and probably will never, release the language I made and use day to day to the public.
Hare's problems are much deeper. They stem directly from Drew's fundamental misunderstanding of the nature of software development. Drew is correct that managing complexity is central to the problem of software development. He is 100% wrong on how a programming language can contribute to solving that central problem.
There has been a great deal of progress, in the past five decades since C burst on the scene, on managing complexity in programming tasks, with programming language design taking a big bite. Hare adopts exactly none of that success, setting users straight back to the 1970s again. That might have been fine if demands on programmers were no greater than in the 1970s (although the bugginess of software coded then argues otherwise), but anyway we do not live in that world anymore. The programs we want to run don't fit in 64K of RAM and run single-threaded with no network connections, anymore. Where a 1970s language is used for programs today, we all suffer from the failings the language almost unavoidably invites into them.
The fundamental insight that Drew has wholly missed is that essential complexity demands more attention to get right than is typically available for one use case. We have learned that pulling complexity into a library component, or (better) a Standard Library component, or failing that a programming language feature, enables attention to be paid to it amortized across all the programs and libraries that depend on it. All the meaningful progress in programming languages since been in finding ways to bring more of what must be expressed into one place so it may be more widely usable, and able to command correspondingly more attention to its performance and correctness. Standard library components in our modern languages successfully remove their complexity from what programmers must manage.
The ways we have discovered to help with this process include powerful type systems that have been put to work doing heavy lifting far beyond mere "type checking". We have generics to work with types the way types work with values. Certain languages provide extra tools such as destructors/Drop traits that enable automating resource management. Many languages assert direct responsibility for pieces of that task, imposing borrow checking or GC. There is much left to do.
Hare provides exactly none of the tools that have been discovered to encapsulate complexity into carefully correct and generally useful components, or to enable using such a component in all the different places and ways it would be useful. This failing is painfully evident in the library components it does attempt.
In the 1970s, Hare might have competed with C and Pascal to express the then newly valued "structured programming". Today it is a toy. Evaluated as a toy, there is nothing wrong with it. But it is not presented as a toy. As a tool for programming in the modern world, it is sorely lacking everywhere that C is sorely lacking, that make C directly responsible for the massive suffering documented in the litany of CVEs, botnets, database exposures, and ransom events.
I would be very cautious when calling anything that encapsulates complexity "carefully correct" or "generally useful". Hiding such complexity behind language features is how we ended up with new generations of programmers who have no idea how to code anything themselves.
If generations and generations of stack overflow copy-pasters are the future of programming, then I don't want to be a part of it.
"In the 1970s, Hare might have competed with C and Pascal to express the then newly valued "structured programming". Today it is a toy."
Excuse me? The most useful, the most endured, the most sane and the most understood coding paradigm is a toy to you? You know, some of us really like to think of our programs as many procedures being called one after the other.
That's the most logical way to think about them, because it's the exact way the CPU sees them.
CPUs do not, in fact, see procedures. They execute machine instructions. Your "procedures", exactly, 'encapsulate complexity behind a language feature'. A rudimentary feature, true, but one identically the same as is found in modern languages, among their more powerful features.
I am, indeed, always cautious about calling things "carefully correct and generally useful". Still, I am aware of many language features and standard library components in modern languages that fully satisfy those criteria.
Who pastes code copied from stack overflow? I assume that was a ham-handed attempt at an insult. You are welcome to continue fooling with toy languages, old and new. Pretending they are adequate tools for serious work says more about you than about them.
Can you explain what you mean here? In the context of structured programming.
"CPUs do not, in fact, see procedures. They execute machine instructions. Your "procedures", exactly, 'encapsulate complexity behind a language feature'."
That's right, CPUs execute instructions. Those instructions are executed one after another, with jumps when a certain operation should be repeated or skipped. This is the definition of structured programming. Structured programming is a simple abstraction over how CPUs execute machine instructions.
I don't see any objects with methods there. Nor do I understand how would a CPU understand a lambda function or care if something is immutable or not.
"I assume that was a ham-handed attempt at an insult."
No insult there. It is just a sad reality how many developers rely on Stack overflow instead of using their own head a little.
Structured programming was defined by Edsger Dijkstra, and consisted originally of a model of programming with restrictions on where branches may go. Later programming language constructs if/then/else and while implemented this model. It does not address subroutines.
I describe C and similarly primitive languages as toys because they do not provide the expressive power needed to enable the levels of productivity and reliability demanded of serious work.
It's a very young language. One might consider that early mistakes can be addressed, but only if somebody speaks out about them.
I question framing this question as "shooting it down." The project will clearly survive this criticism. The article is by somebody who took the time to learn the language, and will probably continue to invest their time into the language. Criticism of this kind is a gift.
You might think "nobody will use the language", but if nobody pushes back and says why people shouldn't use the language, perhaps people will start to. It's really important that people do write feedback pieces like this, because it helps other people be informed about their choices.
And I will assume that your next comment will be about how the two don't compare. And I am in full agreement with that, except for the fact that trivializing abuse is another element that people use in this type of debate, and I believe that abuse is abuse. It might not have the same emotional impact on the victim, but the abuser psychology is similar in all cases, and the slippery slope of losing sensitivity to it begins with accepting discussion board bullying as normal.
PS. However this discussion is at least an order of magnitude removed from TFA, which is not abusive, merely presumptuous and premature.
I've really been enjoying Hare. As a primary Go/Rust developer its syntax and limitations feel like what I've been looking for and it's youth predicates plenty of opportunity to work on new project. When I was working on a ANSI Colors lib [0] last night I spent a lot of time in the community IRC. Your post is basically what everyone's feeling. This is a language designed for people who like it's style. It doesn't need to have this or that feature. And if the community changes their mind, what better candidate than an open source tight-knit young language?
Live and let live, don't tell others what they need.
> but for code that’s supposed to be used in production it’s useless
This isn't about doing what you love - it's about engineering a product, a product that needs to have certain safety, security, and stability guarantees. That's the context of this conversation, and for that purpose, Hare isn't suitable.
It has nothing to do with anything you do. It has everything to do with the impact it has on everyone but yourself.
Hare's value proposition is simplicity- which allows you to better reason about the software you write, for one thing. We are each entitled to our own opinion about how valuable that is.
Hare's "value proposition" of simplicity is, simply, a shuck. Any software effort has essential complexity that your language may help you manage, or chuck you in the deep water to sink or swim. Hare does the latter, and is even self-righteous about it.
Your opinion of Hare's view of simplicity is your opinion. Any software effort has some amount of essential complexity, in my experience many languages and tools quite a bit of incidental complexity that could be avoided.
Avoiding unnecessary complexity is everybody's responsibility. Dumping every last bit of unavoidable complexity onto the programmer where it has been long demonstrated that tooling can take care of much of it is simply irresponsible, and inexcusable.
I do not excuse it.
It's not really relevant.
Besides, none of those have to do with software engineers. Compliance doesn't imply any individual responsibility.
> I've worked in defense and there are stringent security requirements for applications and servers.
Like? FEDRAMP? The vast majority of compliance is about threat modeling and access controls.
No, STIGs.
> The vast majority of compliance is about threat modeling and access controls.
If compliance doesn't address security, that sounds like a bigger issue than a new hobby language existing.
And we're telling them they shouldn't.
That's pretty rich
I'm not saying that memory unsafety isn't a problem, but if you really want to make software better, you should address the biggest problems first.
Then ... don't use it. Others can use it and see what they learn from it, or even whether they find it productive to build stuff with. Even Brainfuck and Malbolge have devoted users who enjoy the challenges they pose, so I can promise you there's room enough in the world for this language. Let's just have some camomile tea and calm down a little.
Furthermore, it could be argued that our time could be spent more productively than on a language like Hare.
As for whether our lives could be used better, well, let's leave that to each individual. I heartily encourage you to live your life as I do, worrying about what I myself am spending time on (currently: this inane conversation), rather than whether other people might be spending their own time in some supposedly-slightly-suboptimal manner.
It is literally everywhere. Your kernel, your networking stack, your interpreters, your office suite.
Almost everything you use has some roots in C, a memory unsafe language that "messed up" and still became the most widely used language ever.
Software will always mess up, regardless of language it's written in.
Do I really have to guess the order that my procedures are called? FP zealots say that I do.
What makes generics and functional programming style mandatory for any given language?
These things just add complexity and philosophy to a field that values pragmatic thinking.
Functional programming is not pragmatic. And neither are thusand ways to handle generics.
It doesn't have in-built immutable data structures, most basic data structures and types are mutable and have mutate in place methods as their primary way of interaction. Struct fields being private by default is a dead giveaway (unnecessary in an FP language). Closures and functions are not the same thing and you cannot do much with them except calling them and passing them around.
I would rather describe it as an imperative language with lightweight OO (via traits) and some FP sprinkled on top. Note I'm not saying you cannot write FP code in it or that Rust doesn't have great unique benefits.
Immutable data structures by default would involve avoidable overhead, so that's why they aren't a built in. There are well-known crates that do provide persistent data structures in Rust.
> Struct fields being private by default is a dead giveaway (unnecessary in an FP language).
Data hiding is just as useful in functional programming; in particular, it enables you to expose an interface to data that stays valid under any isomorphism, while preserving desired invariants. Structs with generally accessible and updatable fields are a special case.
Certainly.
Since the announce of Hare I've seen __too__ much criticism IMHO.
I think the authors have stated clearly the intentions and the features of the language: They just want a slightly better C. Of course you may not like it, but no need to be that harsh...
If Hare is intended to be a deliberately Spartan C replacement, so be it; but pretending that writing a hash table is easy looks like the immature and arrogant opinion of someone completely unaware of his ignorance.
I agree, that implementation is definitely flawed. Probably the author do not consider that important a proper hashtable.
> looks like the immature and arrogant opinion of someone completely unaware of his ignorance.
This is what I'm referring to when I say "harsh" criticism. Maybe he is wrong, but I think there are better ways of saying it: "Hey, maybe a proper hashtable is not that easy.. have you consider [insert here improvement proposal]?"
Wrong questions (focusing on the narrow case of unique, overspecialized application-specific data structures), not only forgivable wrong answers (that hashtable from the Hare compiler, after all, could be close to good enough for its specific usage).
Personally, I think abstract data types based on some kind of macros or templates would be a valuable feature for Hare, even if their use in the standard library is limited, because someone will want to write reusable libraries.
Even within the confines of an application, there is a practical need to deal with different uses of a data structure in the same way and make changes in one place (e.g. "the generic hashtable in our ORM" ) rather than in possibly numerous quasi-duplicates (e.g. 250 different struct to struct hashtables representing database query results in a large business application).
C++ templates, a good example of what could be realistically offered by Hare, allow to upgrade a definition from "do X with one type" to "do X with any suitable type", which is a useful abstraction even without reuse (what's actually expected or not of the type parameters becomes explicit), a technique to write general libraries "for free" (e.g. you can put whatever you want in our hashtable), a technique to define general language foundations (e.g. you can have smart pointers to anything).
So far, not many languages measured up to C. Hare just might, with it's right amount of simplicity and C's footguns gone.
So it's small surprise to see The Rust's Evangelist Strike Force in action on these threads about Hare.
A huge potion of the criticism at this point towards Hare has been summed into three points: no ‘generics’, no memory safety, and small stdlib combined with no package management.
But, I can imagine that if Hare had generics using template metaprogramming then people would complain about how language design has shown that templates are not good enough, so it needs to be not just a way to have compile time type specialization of data structure, but enable ad hoc polymorphism as well and of course that needs traits or typeclasses.
Further, if Hare used testing and fuzzing and modeling, then claimed to be within the bounds of some error percentage ‘safe’ from leaks, use-after and double free, out of memory conditions; the counter claim would be that you can not be safe, for ANY usage of the word unless you have ownership and borrow checking.
And finally, without a centralized, language committee backed and certified web based way to store and access libraries Hare will never be able to make usable and fit for purpose software, so a package manager is a must have to be worth even considering.
All that said, I don’t think it is exclusively users/promoters of Rust that are complaining/disparaging Hare’s very existence. But it certainly seems like many, many people seem to be convinced that their use case/product goals/preferences are the only considerations for any individual of group making engineering decisions.
And those are the very same reasons Hare should remain a simple language.
You can never please the crowd, but you can please yourself and a couple of your friends.
In the example, maybe the static size of the table is fine, and maybe not being able to remove items from the table is fine, but unless maybe it's some sort of cache, it's almost never ok to assume that there will never be any hash collisions.
I mean, if you don't want to include hash tables in your language or library, fine, but using a bad example to attempt to justify it after the fact is.. not good.
Anyone can write a hash map, in the same way that anyone can write an HTTP server over a weekend. Doesn't mean it will be production quality.
Glibc's obstack doesn't count, it's non-standard and the only type it supports is (of course) void*, which is a bad interface.
One of the easiest things to do is write home-grown hashmaps that perform better than std::unordered_map. I suppose you have to worry more about technical debt / bugs, but yes home-grown hashmaps are feasible and I don't think std::unorderd_map is a "vast improvement".
Do I need to manage memory? Does it manage memory? How do I iterate? How do I add a new key space without thrashing the original implementation and making it twice as complicated?
That's the biggest thing a standard interface gives you. Even if it's less performant and not well-designed, I'll take it.
The C++ containers are actually well-designed and performant so it's a no-brainer if you get to choose.
Almost forgot the type checking you get with C++. That's also a major factor.
The STL doesn't allow the containers to be "well-designed and performant" other than std::vector which is trivial. People do what they can, but there just isn't much to work with because random coincidences about the data structures that were in mind at the time are enshrined in the C++ standard as API features.
std::unordered_map is obliged to be a bucketed map. This is a poor choice, but it's not optional because it's enshrined in the API even though you probably don't (and shouldn't) actually rely on this. Lots of the good options are drop-in replacements for std::unordered_map... unless you really depend on it being a bucketed map.
The other important thing to know if you've been relying on the standard library is that the powerful need for backward compatibility means it's not going to give you a proper hash, which is important for a proper hash map. std::unordered_map doesn't care that the standard "hash" provided isn't, but if you use a hash map written by people who explicitly told you to use an actual hash it's going to exhibit jaw-droppingly bad performance until you do so.
std::unordered_map isn't worse than a reasonably competent person's "my first hash table" but it's disappointing how little better it is than that after decades.
As with all performance, if you aren't measuring then changes are just wanking, but if you are measuring you will almost certainly find that swapping out std::unordered_map is worth doing.
- those hash maps are optimized for large sets
- they work around several edge cases
- they implement way more features than an in-house implementation
- they need to support many types of hardware
A decent engineer can beat the std collections in a day on performance. But to do that in production over a decade with shifting requirements is a different ball game.
But I do believe in edge cases, I'm sure there are some pathological edge cases (that don't usually matter, but deserve consideration).
I'm interested to know what 'way more features' are for hash maps/sets, because AFAIK C++ just supports the basic features you'd expect of the abstract data structure of a "map" with the algorithmic guarantees you'd expect from a hashmap.
And nothing I did was aimed at 'specific hardware' other than it vaguely benefitted from memory locality / avoiding thrashing the cache.
The STL map allows you to:
- specify the key/value types
- specify the equality predicate
- specify which hash function to use
- iterate over values
along with a host of other things that most users don't initially need but they might need in the future.
You could implement something that has feature parity in C++ or ideally find a battle-tested library that does.
Using C, however, you'd have to sacrifice type safety and possibly readability. Yes it can be done, and yes it can be done correctly. But it's tricky and difficult to ramp up on for the new developer. This is the one that you should be careful about adopting.
My philosophy is to use the STL containers because they're familiar and they're more than good enough for 99% of the use cases out there. Developer time has a premium, and this saves developer time.
If you need performance because you're constrained by CPU cycles or if you're running at scale (Google for example), by all means reach for something better.
C is a terrible language for building things in the modern world. Not including the progress over the last 4 decades in your new language is a mistake.
No allocation = no leaks, no use after free, no dangling pointers.
No multi core = no atomics or concurrency to worry about.
Ring buffers solve 99% of my interrupt data sharing = no need to worry about synchronization.
Bounds/safety checks? Need to run and test release builds most of the time because of space/memory constraints.
Often you don't allocate because it's not a necessity and a PITA due to C, but with rust I see no point in not using a tiny allocator if one has a few 100 KiB of RAM - totally depends on the project and target platform.
> No multi core = no atomics or concurrency to worry about.
There are interrupts though and those just need the same mechanisms than multicore and can be a PITA to manage in C. Besides that, there are quite a few cheap multicore embedded CPUs available now.
You can encode safety checks like pull-down/up clashes and the like nicely via the rust type system, that alone makes it a major benefit over C.
For example, from the rust embedded book:
One can also statically check that operations, like setting a pin low,
can only be performed on correctly configured peripherals.
For example, trying to change the output state of a pin configured in
floating input mode would raise a compile error.
-- https://docs.rust-embedded.org/book/static-guarantees/index....- The package management is a nice. Install a generic ring buffer or a board support package for the hardware you are using.
- Async-await can be really nice for dealing with hardware peripherals.
- Don't interrupts constitute concurrency?
- Reduced UB and footguns compared to C
For example, your i2c controller only supports two specific pins and they have to be in a specific mode? That can be a compile error if you make a mistake.
But C has no problems abstracting away peripherals, just hide the register bits in a private TU and expose i2c_init(), i2c_write(), etc.
That's not a zero cost abstraction, unless these "functions" are actually provided in an include file. Embedded programming is often performance sensitive, so this can matter.
And this is where Hare is meant to be used, is it not?
I believe the name comes from a tradition of putting such code in a directory named vendor/name-of-library or vendor/name-of-vendor/name-of-library which allowed for distinguishing between the src directory which was first-party code and the vendor directory which contained code form 3rd party vendors (often such code was proprietary and had to be bought/licensed).
Nowadays the term is used to differentiate between a manual approach and the use of a package manager.
Based on my long builds times and erratic network behavior in CI I'm beginning to think every system should just vendor.
Builtin hash maps are convenient for "dictionary languages" like Python or Javascript, but beyond that they are highly specialized and tailored to specific data sets and hardware properties.
This is why cryptographically secure short-output hash functions like SipHash were developed.
Note that this is only a problem when an attacker can influence your hash table usage, and can be mitigated with hashing functions like siphash. This is not necessary for the majority of hash tables in the wild.
I agree a hashmap is not simple and Hare's post is a bit naive, but I don't get what's his problem with the language not providing a default implementation. It's part of the language's design. It's targeting people that most likely won't need a hash table. It's not aiming to be a high-level batteries-included language like Java.
I'm constantly going back to Java to try things out, just because of the absence of convenient data structures. It's a pain to keep recreating them unless it's necessary.
I guess Im not the target audience for the language, but the lack of basic data structures like this adds a lot of friction for me.
https://lists.sr.ht/~sircmpwn/hare-dev/%3C20220503143516.19e...
I was sort of expecting Drew to... fix it? I mean, what you'd hope for is an updated blog post with a mea culpa in it, but I'm not setting my expectations so high. Fixing the bug seems like a pretty basic place to start though.
[1] https://harelang.org/blog/2021-03-26-high-level-data-structu...
Lots of C++ compilers shipped STL implementations with atrocious performance (and outright bugs) for like a decade. It's not like putting something in the standard library guarantees correctness or performance.
The entire issue is that the language does not allow for that: like Go (pre 1.18) it does not have userland generics. And unlike Go it doesn't even have a builtin hashmap, only arrays and slices.
Edit: plus, realistically now, the article's criticism revolves around shortcomings of a particular implementation of a hash table. If Hare had generics, and a hash map implementation in the standard library, and that implementation used the same hashing algorithm and made the same assumptions (e.g. no key collisions), all that criticism would still hold. You can find poor implementations of any data structure in any language.
So if I need a hash table to map inodes to strings and another to map strings to IP addresses, I'll need to create that anew each time.
That means either writing a lot of code twice, or relying on manual code generation (which sucks for many reasons).
> Recall that Hare is designed to be similar to C in terms of scope and goals. C also provides no general-purpose hash map, and little by way of other data structures (though some attempts exist, none of them good).
So you're stuck using a crappy general-purpose hash map that you have to convince your linux distribution package managers to maintain.
We have discussed adding first-class maps to the language many times. We recognize the value in this feature and have tried to come up with a good way of doing it that fits within the design constraints of the language, but it has several design issues. There are three key problems: finding a good way of hashing arbitrary data structures (or arbitrarily limiting the kinds of keys the map can store), finding a good way of determining equality of arbitrary data structures, and dealing with memory allocation semantics. Hare does not have generics, and the alternative is a hands-free approach to hashing and equality, which has some troubling limitations, some of which are very unintuitive and non-obvious (which conflicts with our values for explicitness). The allocation problem is also troublesome: Hare uses manual memory management, and any hash map solution which involves magic behind-the-scenes allocations has serious design conflicts with Hare's principles.
This article criticizes a sample of a hash map implemented for the build driver. This hash map is used to store a mapping of module details keyed on the module's namespace. It is true that it is a fixed size map, and that collisions can easily be found for the fnv32 hash. These constraints limit the ability for this hash map design to generalize. However, this is not supposed to generalize. It's designed to be a special-purpose hash map for this specific use-case, and takes the simplest approach which is sufficient to this specific problem. As the author notes, there are many approaches to hash maps and there is no one-size-fits-all solution. So far as hash collisions are concerned, these are very unlikely in this use-case. This is not a hash map where hash flooding is a concern, and accidental collisions are so unlikely as to be a negligible risk. If this were not the case, it is easily fixed by just comparing each bucket entry by the namespace rather than by the hash. For use-cases where these things do matter, I would be interested in seeing something like siphash end up in the Hare standard library.
Recall that Hare is designed to be similar to C in terms of scope and goals. C also provides no general-purpose hash map, and little by way of other data structures (though some attempts exist, none of them good). Each of these approaches and concerns comes with different needs and trade-offs, and Hare places responsibility for evaluating these needs and trade-offs into the capable hands of the programmer. This is a reflection of Hare's values, which are distinct from the values of some other languages mentioned in the OP - Rust, Go, Zig, C++, etc.
Thanks for providing me the opportunity to clarify this here, though perhaps this merited a blog post rather than an overlong HN comment. I understand that this is a design decision which appears especially confusing from the outside, but know that it was made through careful deliberation and discussion.
Oh-- and a map-like thing for net::uri would be nice to have as a convenience function in the future. We need to implement the basic approach using the most fundamental design and then build the convenience on top of it, in order to accommodate all use-cases and leave the trade-offs for the user to make.
This sounds like the language inherits all the limitations of C and isn't able to provide sufficient expressivity to solve problems that you yourself consider valuable (which aren't that many considering the niche scope). People move away from C because of these limitations, how do you expect they would choose Hare as a replacement?
The current compiler will silently ignore colliding hashes and (I believe) result in a very confusing error. The probability of this happening is not very large, but still not small enough that someone will hit this error probably this year. At the very least you need to report hash collisions so that you can somehow rename your modules and so on.
It's not unheard of or unreasonable to have collections that have take an allocator as an argument. And a comparator and a hash function as arguments.
I think adding generic types and/or a notion of an interface might increase the complexity of the language, but it will also become far more expressive. And yes, these are tools that are sharp and require caution, especially when designing a standard library - you want to think hard about the constraints you'd impose on the consumers of your standard library when picking which interfaces require what methods etc, but I still think that there's not much to gain from a language without these features when compared to C.
struct cmp
{
hash: *fn(_:*void) u64,
eq : *fn(_:*void, _:*void) bool,
free: *fn(_:*void) void
}
With maybe some default options should be enough for at least the basics.
Note that the indirection overhead would be significant, but without generics or something similar, you don't really have a choice.In the same sense that there's room for languages without, say, for-loops.
You are conflating the _implementation_ of a hash table with the _existence_ of a hash table.
You list a few problems:
Computing the hash for arbitrary structure & check equality of the hash is something that you can do by adding a func in the creation of the map.
That will likely add overhead (since you can't inline it, and most of them are trivial), but at least you'll have something.
I'm not sure how memory allocations for a hash table is any different than the usual. Memory ownership semantics are likely relevant, so you'll likely need to add a way provide a free() routine here, as well.
A common example may be a `map<string, file>` - where you need to deallocate the string and `close()` the file.
This gets interesting when you do a set over an already existing value, by the way. And certainly a challenge you'll need to deal with.
My issue isn't so much with the complexity of the solution. Design choices such as not having generics will have impact on performance. But having _a_ solution is still a baseline requirement in my eyes. The original post said that you can just roll your own, except that you really can't. Certainly not over & over again.
With regards to the hash table I criticized, that is exactly the point. You were able to "get away" with implementing a pretty bare bone thing, but you called it out as something that should be viable in general.
Hashtables are in _common_ use. They aren't something that you'll use once in a blue moon. Go's decision to provide dedicated syntax for map wasn't an accident.
And for certain scenarios, you may need to use a different implementation to get the best results. But for the vast majority of scenarios, you just want _something_. And whatever you have in the box should suffice.
That is because a generic data structure has a lot of work done on it to optimize it for a wide range of scenarios. Conversely, a one off implementation is likely to be far worse.
There are also security considerations to take into account. You are not likely to properly implement things like protections against has poisonings attacks, etc.
And "easy to fix" is sure, but you have to remember to _do_ that, each and every time you write this code. Because your approach is to give that ownership to the user.
And your language design means that you _cannot_ actually solve that properly. That is not a good place to be.
C was designed in the 60s, and it is very much a legacy of that timeframe. Today, there is literally no system beyond hello world you can build that won't require hash tables galore.
The web is full of those (response and request headers, query string parameters), data formats (JSON is basically a hashtable plus some fancy pieces) , caches (that are just hash tables), etc.
I can see how you had this impression from the language of the blog post, but what I meant to express is that the approach generalizes, even if this specific code does not.
>The web is full of those (response and request headers, query string parameters), data formats (JSON is basically a hashtable plus some fancy pieces) , caches (that are just hash tables), etc.
The web generally falls well outside of the target use-cases for Hare.
This:
foo = { "apples" : 100, "bananas" : 20 }
bar = [ 100, 200, "oops" ] // error
Instead of this: foo = new hashmap<string,int>
foo.put( "apples", 100 )
foo.put( "bananas", 20 )
bar = new array<int>
bar.add( 100 )
bar.add( 200 )
bar.add( "oops" ) // error
I believe, but cannot yet prove, this would eliminate 98% of the (popular) pressure (desire for concision) for adding generics.What does throw me off about some language projects is the rejection of "complexity" either in a compiler implementation or language feature because it is "complex" - despite decades of research and experience in other languages that some of those features are actually super useful (and also ways to get them wrong or right!). I'm not sure if this is where Hare has landed on generic programming, but it's an ethos I see in a lot of "C but not C" languages that I don't think is a sound approach to language design.