As much as I hate some aspects of C, I also love it. I love it because the committees are so slow and conservative, I'm sure I can write in C89 now, I can write in C89 in ten years' time, and I don't need to care about new features someone might add or not.
You can call it a moot point since the amount of what you can do in pure C today is very limited - you have to use at least a few libraries to do anything remotely useful even on very light systems, but in today's fast developing tech world it's one of these things that really stand out.
For what it's worth, I find it remarkable what you actually can do with C with just a few libraries. For instance it's entirely possible to write a simple video game in pure C with a few well-chosen libraries for things like audio and asset loading.
You either stay unpopular enough that no-one demands that you compromise your vision, or you become popular enough that there is constant pressure to add features that are important to some part of your community.
I am mostly a fan of Rust, but it's made some missteps in its evolution imo.
For instance sync rust is mostly nice to work with, but async is another story entirely. I have the impression that it was somewhat rushed into the language due to community demand, but it's not really a fully solved problem.
My understanding is this is a FUD meme that has been passed around. Async isn't finished yet, but was debated for years and considered with incredible care and input from the community.
I don't know if that's FUD or not, but personally async turns rust in a language within a language: core rust is interoperable with everything via C FFI, you can build on top of existing stuff. But if you let async rust lure you with its (well deserved) appeal, you end up in a parallel universe, where everything is pure rust.
Surely you want to use the pure rust async aware postgres client and the pure rust grpc implementation, but with async that's not a free choice.
You now have to deal with the fact that these (actually often quite well written) reimplementations often lag quite a bit in functionality.
For example you think you can just use some boring technology like a RDBMS but then you discover that pgbouncer doesn't really work well with sqlx. Similar stories for gRPC and other stuff.
Don't get me wrong, I'm not saying that's because async implementation is bad or it hasn't been well thought out. Other languages make other tradeoffs (e.g. Go detects blocking syscalls and increases the size of the thread pool, which makes it a bit easier to accomodate blocking FFI with the core async model, but that comes with a price).
What I'm saying is that in practice async rust feels like another language, a language built on top of rust, that looks like rust, that can call rust, but effectively creates its own ecosystem. The problem is compounded by the fact that it's still called rust, and often you can write libraries that can do a bit of async or not based on optional features.
It's probably all unavoidable and for the better, but it does add a source of fatigue that must be acknowledged and not dismissed as FUD.
I think async creates a mini-language within _any_ language, i.e. the whole what-color-is-your-function problem. It's turtles all the way down or nothing.
In return, of course, you get a style of concurrency that tends to be much easier to reason about and much less prone to subtle bugs than traditional preemptive multitasking.
Whether that tradeoff is worth it is obviously very dependent on the particular situation.
It doesn't. You can call non-async Rust and C from async Rust just fine. You just have to be beware of calling blocking things if you're not in a context where blocking is ok (i.e. in a CPU work pool thread).
I'm not sure what the GP meant, to be honest. Async "colored" code does have an tendency to "infect" more and more of your codebase, but you can still write and integrate big chunks of non-async code (e.g. a parser or a network protocol) if you're mindful about your design.
Sure, you can technically call other rust code from async rust and thus you can also technically call C code via FFI, but if that code blocks you have a problem. There are ways around it but they are hard to use and create the pressure towards just rewriting the whole thing in rust.
I could be wrong though. The reason I like to discuss these things here is the opportunity to be proven wrong by someone who knows more and offers a counter-example
Yes you can. And this will create threads on demand as needed.
But you need to know when you must call this and when it's ok to not call it.
The type system won't help you with that. But if you forget to call it you can cause starvation and if you call it too often you may create too many threads.
What I see in the ecosystem is that such tricks are perceived as hacks and that it would be just better if one could just write a pure rust reimpl.
Surely there are enough reasons that drive people to reimplements stuff in rust. I think this aspect of async nudges people even further into that though.
I think you're right that async Rust isn't quite done yet (improvements on this front look to be a major focus for 2022). But I think the fact they were able to get an MVP released that so far looks like it can be built upon without too much technical debt is positive rather than a negative. Rust would be far less useful without the async feature.
async is a good example! I agree it is fine for the time, but of less quality than the rest of the language.
Still, the fact that Rust has MIR I think very much limits the damage premature async can do. If we get something better, it should be quite possible to extra "Rust - today's async" to recreate it.
Conversely, I wouldn't be surprised if many of the misfeatures in Switch are in impossible to extricate.
I neither expect Rust to go off the rails, but the Core developer team drama lately and Mozilla stepping away makes it that I will neither be surprised in the even that it does.
The Mozilla/Facebook partnership makes me glad they have distanced themselves from the Rust. Obviously not a lot of good judgement happening at Mozilla.
I still love Objective-C. And to me—I'm so amazed that Apple added "Property Syntax" and "Properties". It's pretty easy to do much of this with Macros (which could have been baked into the system) and the language stays very simple.
You would have 2 lines of code per property but one fewer concept—and I think that is a much more important factor. To me—this was not the right way to add sugar.
As someone who started learning Objective-C only after Swift become firmly established, I have to say I strangely like the language. I picked it up fast and have had no issues with adding features to a legacy codebase.
When I started, I thought it would be some complicated beast, but no - its a reasonably simple and elegant C superset with the only disadvantage of some extra verbosity. (but Apple API's suffer from verbosity as a principle)
I have to wonder: why did Apple create Swift ? Objective-C is quite nice, especially for C/C++ programmers. With Objective-C++, interop with C++ libraries is also terrific. Swift offers nothing like this yet.
I thought it should be obvious. Apple was competing with Google/Android for mobile developers. For Android, you programmed in Java, which everyone learned in school. For iOS, this funny language with smalltalk syntax was a barrier to entry.
So Apple needed a nice language with C-like syntax to woo developers.
I know Chris lather's intention. And I have read and listen to all the interview he did. I think the question should be, Why was it necessary for Apple to bet on Swift. Something I dont think Bertrand Serlet or Avie Tevanian would have done.
They wanted a safe, performant language that interoperates with Objective-C. Safety gets them fewer vulnerabilities, interoperability means they can gradually decrease (Objective-)C usage.
Also:
- opinions on the nicety of Objective-C differ (but the only arguments I’ve heard why it would be bad more or less are “I don’t like the syntax” and “it’s verbose”, both of which, IMO, are weak. Both, IMO, are acquired tastes. I don’t think anybody is born preferring terse K&R C, for example.
ARC has been there for almost a decade now. Also if you wish to do heavy string concatenation, please use a NSMutableString and you can simply do:
[string1 appendString:string2];
It is amazing that people forget to simply use the right tool for the right job and blame the PL instead.
Chris Lattner has mentioned that in a couple of interviews, there is a limit how much they could improve Objective-C towards being a safe systems programming language, exactly due to its C heritage.
Yeah, no macros in Swift is a bit of an odd choice. It means that every would-be macro has to go through the language evolution process and be blessed by the Swift team. You can't release macro-like functionality, such as Rust's serde, without support from Apple.
As I see it, when we will have built languages that didn’t de-evolve into a bazaar of features, those will be dependently typed languages.
Because a dependent type is a type that represents a computation. A word that symbolises a computation, a word which can be manipulated by hand and assured by machine – and vice versa.
Software is automation. Types are signifiers of meaning. Dependent types are the automation of the automation.
Using a dependently-typed language feels like handling a machine which is the result of having closed a fundamental loop of expression vs. meaning. And that is what it is.
It’s very philosophical-sounding! It has its roots in philosophy – in intuitionism, where math dug down and met philosophers who had dug down from the other side. Kind of.
Practically, the upshot is that things actually become simpler. Kind of because you can grip the tool everywhere. Un-dependently typed languages feel kind of like they’re part tool, part void. You usually can’t express things relating the context you’re in, or looking at.
(As I see it – at least these days – dependent types just are. It’s very, very nice to just have higher-order unification and the things that sort of fall naturally out of having it.)
They are a false idol in that, when people learn about them they seem like they’re the answer to all problems, and they’re not. The fact that types are equivalent to propositions doesn’t mean that making all of the propositions about your code at the type level is a good idea. For me, it’s completely unnatural, clunky, and more verbose than just writing propositions as logical statements.
I found this talk from Xavier Leroy a while back too: http://www.cs.ox.ac.uk/ralf.hinze/WG2.8/26/slides/xavier.pdf. He is the main person behind CompCert, the formally verified C compiler. They do that verification in Coq, so I was expecting him to be a believer of dependent types. But he had this to say:
“ Dependent types work great to automatically propagate invariants
- Attached to data structures (standard);
- In conjunction with monads (new!).
In most other cases, plain functions + separate theorems about them are generally more convenient.”
For that reason I prefer Isabelle/HOL as a theorem prover. The core logic is simpler, but you can express whatever you want as a theorem, without worrying about phrasing it within the type system. It feels a lot more natural.
That’s not without downside either of course. Proofs in Isabelle notoriously must match the structure of the code being verified, so changes to the code require proof changes. Liam O’Connor wrote an example of where they he feels dependent types are better here: http://liamoc.net/posts/2015-08-23-verified-compiler/index.h....
Even with that, I’d rather have simple code plus simple propositions with complexity at the proof level. This will likely be another eternal holy war though.
Thank you so much for taling the time to write this thoughtful and valuable reply!
It helps me see what I think I’m seeing, or rather to define it. It also has helped me to perspectives I hadn’t seen.
I’m coming to dependent types from here: “simple, clear code good yes program program argh I can’t express a very distinct thought without escaping to another language layer or generating code using string concatenation”, and from here: “my data structure is simple and clear but argh it needs a handwritten parser and serializer and it needs to be maintained and argh why am I writing a parser AND a serializer it should be just one bidirectional definition? and argh why do I need to write it for each output/input format?”, and from here: “my code is simple and clean and my variables are well named and my tests are well defined and my documentation is well written but argh why can’t I just fold the mechanics that the tests define into the code? as proofs? (and… argh? why can’t my variables and documentation and method names be checked against the code?)”.
It’s metaprogramming that I’m thinking about. And most programming ought to be programming. But we definitely need metaprogramming, and it needs to be understandable and composable and simple and clear. And I don’t know if dependent types are a complete solution to that, but I do think that they are necessary for it.
That-which-is dependent types, which by definition is a computation of what it is and is a proof of what it is. Those philosophical terms finally become practically grounded and practical help in a lot of the work I find myself doing.
I’ll certainly look for the false idol too! My sincere thanks.
I sort of agree with this sentiment but I must say that the implementation of said features has been pretty sane so far, especially if you compare with C++.
Is Java and C++ not the same? I think so. No hate for all three: I use all of them, and there are pluses and minuses about all of them!
Wider question: How can a language and its core library stand still? To me, standing still is death for any computer programming language ecosystem. Many languages are just getting started on the idea of "green threads" and "colours" (sync vs async). Some of this can be done purely with a core library using existing language features, but some evolutions are better done with language features.
> How can a language and its core library stand still
I guess by having a more abstract foundation and relying less on adding hacks like colored function? Haskell seems to be like that.
Another thing is language extensions that you have to turn on to use. This makes deprecating stuff easier in the future, so languages can trim itself instead of keep growing.
Java looks more leaner, except when really wants to master it, also needs to understand JVM APIs for low level coding (invokedynamic and friends), annotation processors, JVM agents, just like C and C++ how the many existing implementations behave, bytecode rewriting libraries,....
Sure, but day-to-day Java doesn’t usually require that any more than day-to-day development in Objective-C doesn’t require learning the details of the runtime.
Rust dropped the GC before reaching 1.0, it was the best decision back then as the memory management evolved. But right now as it guarantees backward compatibility, only new features are allowed to be added (although a lot of new features are coming without new extra syntax).
YMMV, but to me, C is a great example of a language that absolutely could stand for a few more features.
I would almost never write C++ (in the context of low level and performance-relevant code; I write a lot of things for a lot of stuff) if the type system was a touch more rigorous (why can't I specify, and have the compiler yell at me if I don't properly handle, "this pointer may never be null"? TypeScript can do this kind of type narrowing in its sleep!) and if error handling/lifecycle cleanup wasn't hazardous (I'm not even saying exceptions and try-catch, just make it easier to guarantee that a cleanup clause gets called when leaving scope, like Ruby's def-ensure-end).
As it is, whenever I finally hit the breaking point with C++ (which I write mostly from inertia because I know it pretty well) it's probably Rust for me.
That's exactly why Zig is such an interesting language to me. The premise of trying to write a modern C replacement - essentially what C would be if it were designed with the learnings of the past 40 years in mind - is super exciting.
I am a fan of Rust, but I am not 100% sold on it. The safety features and ADT's are nice, but I find it quite clunky in practice, and it is just such a huge language full of so many features. It almost feels more like a test language to try the concept of static memory management than a properly designed language in some ways.
I feel it's missing the quality from C/C++ that they are a very thin abstraction over assembly.
I think this statement implies that it’s clear and obvious what assembly will be generated from a snippet of C/C++ code. But few people can understand everything that optimising compilers and modern processors do to normal looking code. An example of this lack of knowledge is seeing people disagree on if a snippet contains UB or not.
If C is so simple, why do people struggle to write it?
> But few people can understand everything that optimising compilers and modern processors do to normal looking code.
Well when I'm talking about an abstraction over assembly I'm not talking about optimizing compilers. That's another topic entirely. What I mean is, if you look at a block of C code, it's very easy to understand what the machine is doing.
> If C is so simple, why do people struggle to write it?
Do they? I think people struggle to write correct, bug-free code in C, but that's because it doesn't save you from yourself, and it's happy to let you do whatever the hardware will do. That's a much larger programmable space than the set of all safe correct programs.
But I don't think people in general have difficulty looking at a snippet of C code and understanding it.
> What I mean is, if you look at a block of C code, it's very easy to understand what the machine is doing.
So they simultaneously understand what the machine is doing but don’t know what assembly will be generated? Both can’t be true.
If everyone understood what the machine was doing, everyone would be able to look at a snippet and agree - “that’s UB, let’s not do that”. But they can’t agree. Because few people understand what the compiler and the processor will do.
The “thin layer of assembly” was true for the first generation of C compilers. But it hasn’t been true for a long time. It’s a complete black box now. Anyone who thinks that it’s straightforward isn’t being upfront with themselves.
> So they simultaneously understand what the machine is doing but don’t know what assembly will be generated? Both can’t be true.
I mean I can understand a reasonable mapping to what the un-optimized assembly would be. Compiler optimization is very complex, and is going to obscure the results in every language.
> If everyone understood what the machine was doing, everyone would be able to look at a snippet and agree - “that’s UB, let’s not do that”. But they can’t agree. Because few people understand what the compiler and the processor will do.
What? I think everyone can agree that UB is much harder to detect in assembly than in higher level languages than assembly, and assembly gives the most clear view of what the machine is doing. It's a very complex topic to create a system which detects and disallows UB automatically in the compiler - this requires a lot more complexity than a simple mapping of high level instructions to machine instructions.
> The “thin layer of assembly” was true for the first generation of C compilers. But it hasn’t been true for a long time. It’s a complete black box now. Anyone who thinks that it’s straightforward isn’t being upfront with themselves.
Let me rephrase. You say C is simple. I say Rust (for example) is simple. I can look at a Rust code base and say confidently - this code base has no UB in it, it has no memory safety issues in it.
Can you look at a non trivial C code base and make such an assertion? You can’t. Even simple looking C code could be translated into problematic assembly because such a transformation is technically valid. And it’s beyond the ability of anyone but an expert to guard against that.
C is a very useful language. Very important. Very fast. A great tool in the right hands. The world wouldn’t run without it. And it will remain useful and important and fast for decades to come, certainly. But it’s not simple and hasn’t been for a long time. Let’s acknowledge that.
I'm sorry I don't understand your argument at all. What does lack of UB and memory safety have to do with simplicity? Those are very complex features of Rust which require a very complex compiler in order to achieve. Also Rust is notorious for having a steep learning curve, and it takes time for even very experienced programmers to become accustomed to it.
1. The compiler isn’t doing anything unusual or unexpected. It applies only basic, easily understandable transformations from C to assembly
2. An intermediate C programmer would be able to guess correctly most of the time what the generated assembly would look like. And thanks to this, such a programmer would be able to avoid most footguns.
But the compiler does unusual/unexpected things, and it’s hard to guess what assembly will be generated or what that assembly does, it’s not a “thin abstraction”. Would you agree?
I don't think this is the best way to understand this. C has been around for 50 years at this point, and there has been an enormous amount of investment and advancement in the realm of C compilers in that time, which has naturally resulted in complexity and esotericism in terms of how actual mainstream C compilers work. But that's not a metric of language complexity, it's an artifact of a half century of work on the topic.
I think a better metric is: an average CS grad with a little bit of background in compilers and assembly could reasonably be expected to be able to write a naive C compiler which covers say 80% of the footprint of the core language on their own in a matter of weeks.
What do you think is the size of the cohort of people who could write a naive Rust compiler, with borrow checking, ADT's, traits and non-lexical lifetimes? Even without some of the fancy bits like async you're already talking about grad level CS topics at the very least.
C and C++ are not a very thin abstraction over assembly language. The C virtual machine is very far from the actual hardware. And Rust is the same distance from the hardware as C and C++ are. Rust's core operations are, by and large, the same simple mapping to LLVM instructions.
I don't find the idea of a new non-memory-safe C replacement in 2022 very exciting. We should be moving away as an industry from non-memory-safe languages, for the obvious security and productivity reasons.
> C and C++ are not a very thin abstraction over assembly language
We can argue the thinkness (or the thinness) of the abstraction until cows come home, but
while (*dst++ = *src++) ;
is a direct abstraction over
L1:
movb @(r0)+, @(r1)+
tstb (r0)
bne L1
(assuming «src» and «dst» are both «char *» and addresses are loaded into «r1» and «r0» registers, consequently) in the PDP-11 architecture that C was designed on and for. The instruction sequence is exactly 4x 16 bit words long. As well as
*ptr &= 1;
becoming
and #1, (r0)
and being 2x 16 bit word instruction (assuming «ptr» is an «int *» and is loaded into «r0»). Pointer arithmetic and array design in C as we know them today was highly influenced by the addressing modes existing in the PDP-11 ISA, with many C abstractions having to a direct correspondence to specific sequences of PDP-11 instructions.
C++ is much less of a hardware abstraction, specifically when it comes to the higher level language features.
6502 and Z80 had not existed yet when C was begot for the PDP-11 architecture.
Yes – historically – C has never been a perfect fit for 8-bit architectures as it had been conceived for a 16-bit architecture. So what? There are C compilers for 68HC08 68HC11 MCU's as well.
Yet, C has outlived «many languages offering the same "low level" capabilities of C».
I don't see how you could argue that C is comparable in terms of level of abstraction to Rust and C++. For instance with Rust memory management is totally abstracted away from you as the programmer, which is a core part of what a computer is doing.
And if you index into an array in C, that's basically like saying `root memory location + stride * index`. In Rust it's calling a trait function which could be doing arbitrary work.
Rust and C++ are on similar levels of abstraction, but C is much, much simpler.
How is Rust memory management totally abstracted away from you? You have to opt in to every heap allocation, just as you have to call malloc() in C.
And it's certainly true that Rust has overloading and C doesn't, but that wasn't what I was getting at. The point is that C is defined in terms of the C virtual machine, not the underlying hardware. The C virtual machine is quite far from the actual CPU instructions.
In Rust you opt into every heap allocation with an abstraction like Rc, Arc, Box etc. With C you would have to implement each of those behaviors with primitives, because C is at a lower level of abstraction than Rust.
I never said C is without abstraction, only that it is a relatively thin abstraction. Are you seriously arguing that Rust is not at a higher level of abstraction than C?
I haven't looked at Zig much because I mostly noodle with lower-level languages with an eye towards game prototypes and the like, so I don't know much about it. Have anything you'd recommend reading about the language?
I like Zig because it feels like a refinement of C with all the knowledge of the years since C was created. Some of my favorite features are no global allocator, well-defined pointers (with optionals required for nullability), generics and partial evaluation at compile-time, and builtin features/functions for all sorts of operations that are only extensions in C/C++ like SIMD and packed structs.
Read [0] for a comparison among Zig, D, Rust, and C++, and read [1] for a deeper look at the language's goals. I personally really love Zig's type system [2], with a system for generics that is very simple and consistent.
I've always understood that C was designed to fit a hardware budget.
We knew about many of these features then, when C was created - but it wasn't until the 2000's really where computing horsepower caught up to be able to use them in a universal way.
Hmm I've always thought of C in terms of "portable assembly". Not so much that features were omitted because they weren't performant enough, but because they strayed too far away from machine-primitive operations.
void blah(int &x) { x++; }
int main() {
int *x = NULL;
blah(*x);
}
You can definitely write a smart pointer that more or less provides some kind of guarantee about this (with a combo of runtime checks and typefoo) but references only provide a guarantee that they are not statically initializable to null, which is very different.
Compilers are free to assume x is non-null at the time it’s dereferenced, so isn’t it true by definition? Do you have an example that doesn’t rely on undefined behavior?
"Compilers are free to assume" is a fine thing for like, a loop, but the compiler isn't free to assume you didn't mean to dereference the reference in the code snippet I posted, it's just free to not check before it does.
So it will crash, whether that's undefined behaviour or not. The thing at issue here is that other languages have reference types that are more strictly statically guaranteed to be non-null. My point is that references are not a substitute for those, because holding them wrong is not only easy, it's extremely likely to happen in code of any reasonable complixity that mixes pointers and references (ie. almost all production C++ code).
The over adherence to bloat in the name of backwards compatibility holds the language back - these are incredibly conservative changes that reflect practices which have already been used for decades!