1. 119
    I thought I was building a C replacement. I was wrong c3-lang.org
  1.  

    1. 40

      TLDR; people think that a "C-alternative" means "used for what C is used for today". But that's not what the C alternatives (except for Zig) try to achieve.

      1. 7

        Huh, yeah that's a good clarification because I definitely assumed it was for replacing C in microcontrollers and other resource constrained systems.

        1. 24

          (And this is also why Zig's decisions are so different from C3's: Zig's primary goal seems to be to replace C where it's used today.)

          Don't put words in my mouth.

          1. 32

            They weren't putting words in your mouth, they were saying what it seems to them is the primary goal of Zig. If you think this is incorrect I'm sure it would be interesting to hear your view.

            1. 13

              Andrew has long said "I built Zig to replace my own usage of C++", but people still go "Zig is a C replacement, not a C++ replacement." On some level, it's just really hard to control some memes once they get out there.

              I find that anyone talking about an "x replacement" is almost always saying what they mean, not what others mean. That is, there's basically no universal meaning to "x replacement." Heck, back when people were saying "Go is a C replacement," I was always confused: It has GC! How can something with a GC be a replacement for something that doesn't have one! Turns out, for some people, "no gc" is not a defining feature of what C means to them.

              1. 32

                As an aside, the Zig story is a little more complex. I've quoted that statement of Andrew's as well, but if we look at something like the 0.1.1 release notes https://ziglang.org/download/0.1.1/release-notes.html it says:

                Zig is a new programming language that is designed to replace C.

                And https://ziglang.org/download/0.3.0/release-notes.html

                Zig is aggressively pursuing its goal of overthrowing C as the de facto language for system programming.

                Maybe AndrewK himself could set the record straight.

              2. 8

                Then try reading the TLDR right above us.

                I found the article helpful because the author's redefining C3 for himself, but its impact was weakened by redefining Zig, which is pretty weird.

                1. 24

                  I think the Zig comparison was one line in a decent length blog post. He mentioned it again in his post here, but I think it's very normal to talk about Zig, C3, Odin, and other burgeoning systems languages together. And I don't think its "putting words in people's mouths" to share thoughts about a language.

                2. 8

                  Zig sucks as a general purpose programming language

                  ✅ everyone is entitled to their opinion

                  Zig is not trying to be a general purpose programming language

                  ❌ objectively false, try reading the first 7 words on ziglang.org in big bold letters


                  It's a pattern of behavior from Christoffer Lerno (putting words in my mouth) which I'm starting to get annoyed at.

                  1. 11

                    Andrew, I am confused. When did I write “Zig is not trying to be a general purpose programming language”?

                    I believe I've consistently only said that I think one of Zig's priorities is to “replace C where it is used today”, rather than “replace C where it used to be viable”.

                    This was a fairly recent insight for me personally, and it helped explain why Zig, on the one hand, and Odin/C3, on the other, seem to prioritize things differently. Language design is about trade-offs, so prioritizing one thing necessarily means prioritizing other things less.

                    Is it somehow wrong to say that Zig is trying to be more explicit than C? Or that Odin/C3 offers more abstractions than Zig? I am not sure what you're reacting to.

                    1. 5

                      I agree with Andrew, that you have misunderstood or are mischaracterizing Zig.

                      In your post, you say that one would not use C today to write a video editor or a game, and that Zig isn't aimed at those kinds of programs either. The Zig community disagrees. Zigator is a Zig video editor, and Mach is a Zig game engine.

                      Zig is intended to be used for application programming, in cases where high performance is required. Video editors and games are certainly in this domain. As a replacement for C where it is used today, Zig is weaker. Lack of a stable ABI is a problem in that domain. So is the absence of language stability. Today, stability is a key part of C's value proposition.

                      You claim that Zig is not intended to compete with C++. But, the Zig build system has built in support for building hybrid programs written in a combination of C++ and Zig, and C ABI compatibility, making it easier to piecewise migrate C++ code to Zig. The Zig overview compares Zig to C, C++, Rust, Go and D. Zig provides a lot of modern features, which, like C++, make Zig more suitable for building library abstractions than C: generics using comptime, built in error handling, optional and sum types, async, and so on.

                      1. 4

                        ”You claim Zig isn’t intended to compete with C++” ”You say one would not use today to write video a video editor or game”

                        Where did I write this?

                        1. 2

                          This is what I believe the text says, but apologies if I misinterpreted your intent.

                          People write databases in C, not their next video editor or game. For that they use C++ or something like that.

                          I was thinking of C as a language for writing programs. General-purpose applications. The kinds of things people now reach for C++, Objective-C, Swift, Java or Kotlin to build. ... In the eyes of most programmers today, this is not what they associate with C.

                          Zig's primary goal seems to be to replace C where it's used today.

                          1. 2

                            As I was writing in the blog post, this was about my own observation of how people's perception of C has changed. For me, using C for a video editor or a game is perfectly reasonable. But today people often assume that you need C++, Swift, Objective-C or Kotlin for those kinds of applications. The perceived scope of C has narrowed.

                            What I was getting at is that Zig seems primarily aimed at replacing C where C is still used today. It adds some ergonomics, such as slices and tagged unions, but makes relatively few concessions to the higher-level abstractions that people moved to C++, Swift, etc. for. So there is a group of developers who feel they need those abstractions to build applications, and Zig isn't really targeting that group.

                            That's quite different from saying that Zig isn't a general-purpose language. I think C is perfectly viable for many of those applications too; the C3 compiler itself is written in C, and something like raylib demonstrates that C can work very well for game programming given a good procedural API.

                            1. 2

                              I wouldn't call Zig a competitor for Swift (since no GC), but I strongly disagree that it doesn't compete with C++, or that it is stuck at the level of C and that it makes "relatively few concessions to the higher-level abstractions that people moved to C++ for".

                              I moved from C to C++ because I found the lack of support for generic data structures in C to be brutal. Zig has generics, type introspection and type-level metaprogramming (C++ is just now catching up to Zig w.r.t. the last two capabilities). This makes a huge difference compared to C. Zig supports both compile-time and run-time type based dispatch, as do C++ and Rust, but in Zig, you use idioms built on top of Zig's superior metaprogramming facilities, rather than purpose-built language mechanisms such as C++ classes and Rust traits.

                              I agree with your statement that Zig is more explicit. When you do dynamic dispatch in Zig, you retain control over the memory layout of objects, which is not the case for C++ vtables or the Rust equivalent when traits are used.

                              1. 3

                                I didn't say it was "stuck at the level of C", or that it doesn't compete with C++.

                                C++ is used for everything from embedded to general application development. Zig covers much of the same range. But language capability is not the same as language positioning.

                                To illustrate, let's say we have a scale like this. It's not meant to be an exact taxonomy:

                                <------------------------------------------------------------------------->
                                embedded  kernel  system/dbs  dev apps  consumer apps  scripts/web frontend
                                

                                Any language distributes its design attention somewhere along this scale. This isn't about what the language can do - it's about what kinds of programming tasks we're primarily optimizing for. That affects the trade-offs we make.

                                C++ can be used for embedded as well as consumer applications. However, features such as smart pointers, RAII, and higher-level abstractions around resource management put some emphasis on the latter end of the spectrum. (Again, it's not a perfect classification, operator overloading for example helps C++ with nice abstractions for embedded too)

                                My interpretation is that Zig is positioned closer to C than C++ on this spectrum. My reasoning is that Zig deliberately retains more explicit control over things like allocation and object representation, and makes fewer concessions to higher-level abstractions. That doesn't make it less capable; it means that its trade-offs are different.

                                And that's what I mean when I see Odin and C3 as somewhat differently positioned from Zig.

                                Odin has a rich set of built-in types, math types, built-in maps and dynamic arrays, a context system, and other conveniences. Those are trade-offs: you give up some control over implementation details in exchange for having fewer details to manage yourself.

                                In C3's case, operator overloading, near C++ functionality in some areas (minus RAII and OO), a global temp allocator (although implemented in user-land), managed containers, etc. Again, these are trade-offs. C3 deliberately provides abstractions that I don't think fit as well with Zig's design priorities.

                    2. 7

                      I don't see Lernö saying "Zig is not trying to be a general purpose programming language" anywhere. All he said in the blog is that Zig's goal seems (not is) to be replacing C where C is currently good at.

                      Nobody said Zig isn't made to be a general purpose programming language.

                  2. 4

                    Would you characterize Zig as an attempt to make a pleasant general-purpose application language?

                3. 30

                  You're not alone - the Go developers conceived of themselves as building a "better C" too, which caused some confusion at first since the software they aimed to replace was mostly written in Java.

                  1. 19

                    one unexpected killer feature of go was to replace python and ruby sysadmin/infrastructure scripts, because you ended up with a single self contained executable you could cross compile and scp around

                    1. 12

                      I'm curious how common this actually is. Most people using Go are squarely in the "cloud native" landscape, and nobody SSHs into anything. It's all containers and cloud APIs.

                      1. 23

                        I've seen quite a bit of it. I had a conversation a few years ago with a Go developer where I was quite surprised at how much he liked the language given the lack of type-system support for avoiding concurrency bugs in a language designed around concurrency. It turned out he didn't use any of the concurrency at all, he used it as a faster Python that produced easy-to-distribute binaries. That completely changed my framing for how people looked at the language and I've had a lot of conversations with people since then who use Go in the same way.

                        1. 11

                          he used it as a faster Python that produced easy-to-distribute binaries

                          I used to be really turned off by Go because of its perceived shortcomings, but after my job required me to learn it and put in the time, I grew to appreciate it. It's a very dull language, with little place to be creative with, but as long as you work with a couple of linters, it's very fast to develop with and you can be reasonably confident in your code.

                          I do wish tools like staticcheck and errcheck weren't necessary though. For how much Go frames itself as an "unsurprising" language, it does sometimes behave very surprisingly, and it's not fun when some invariant you thought held turns out to not hold.

                          1. 20

                            but as long as you work with a couple of linters, it's very fast to develop with and you can be reasonably confident in your code.

                            I disagree on this if you use any of the concurrency features. Go makes data races UB (and will break memory safety on some of them) but provides nothing in the type system to determine whether you've aliased something that you've captured in a goroutines or sent over a channel. There are some linters that catch the obvious bugs, but the fact that objects in Go can be arranged in any arbitrary object graph means that you can't (in the 'not computable' sense) solve this in the general case with static analysis.

                            But Go as a mostly single-threaded thing, maybe with the occasional goroutine for some background processing in the limited situations where you can reason about ownership of everything the goroutine captures? It's fine. Much simpler than Python, runs faster, trivial to distribute.

                            1. 10

                              Yeah, alright, that's a far more precise assessment than mine, I agree with it.

                              To this day I don't get why we have WaitGroup in the standard library, but errgroup is relegated into /x/sync. It's a total footgun.

                          2. 5

                            I think this is a great lesson: it's not unusual for languages to find a niche which it was never planned to have. The most famous example is probably Pascal!

                          3. 4

                            it was definitely common in the early days of go. these days I guess the heavy use of containers makes a single statically linked executable less of an advantage for stuff running on a server, though it's still a big plus for end user apps.

                            edit: anecdotally, my friend who does devops is very much a go user

                            1. 3

                              Anecdotally, I thought a lot of Go's initial burst of users came from Python. I assumed it was folks excited about inexpensive but real multithreading which has remained an issue with Python for a long time. The scripting thing mentioned above was a reason my group at NRAO specifically chose Go for certain projects. Containerization probably would have been the more correct move though (in our case).

                              1. 4

                                again anecdotally, but as a python and ruby user around the time go came out, shipping and deployment was a far larger issue than real multithreading. just look at the number of "package your code as an executable" projects out there, none of which were ever satisfactory enough to catch on widely.

                                1. 3

                                  I think it went well for go because it appealed to both Python and Java users by being a simple imperative language with good packaging and a much better concurrency story than either of those languages.

                            2. 3

                              I don't know if they wanted a "better C" or not initially. What has been documented and written is that they wanted something to replace the frustration of dealing with long C++ compile times. Go still to this day compiles very quickly but C still does as well.

                            3. 15

                              At that time, the most difficult thing was actually finding a compiler if you were young and had hardly any budget.

                              Not really the point of the article, but this really resonated. Many people today who use gcc or clang today may not realize that in the 80s getting an optimizing compiler was expensive, and a non-optimizing compiler produced very bad code compared to assembly. Turbo Pascal did well because hobbyists could afford it and it optimized.

                              It's actually noteworthy how much gcc had to do with making C truly mainstream, and that C's success was not guaranteed. Most OS or embedded code in the 80s was assembly, and applications could be written in Anything (tm.)

                              These days I'm buying those old compilers on eBay just to learn the "high end" tools that I didn't have access to at the time. And even with optimizers, you really need to, uhh, help them optimize.

                              1. 6

                                Yes and don't forget how the first MacOS was Pascal + asm!

                                1. 11

                                  The Windows (win16 and win32) APIs are all designed to be carefully language neutral. The low-level types are all defined explicitly in a way that can be mapped to any language that has pointers and fixed-width integers and both Pascal and C were first-class. The APIs also explicitly document their calling convention so that Pascal and C compilers (which generally made exactly opposite choices in calling-convention design) could both use them. Higher-level APIs were defined as COM interfaces, which had a language-agnostic IDL that could be compiled for arbitrary targets.

                                  1. 2

                                    Doesn't WinAPI use C-strings, though? Were Pascal compilers targeting Windows expected to provide conversion functions, in sort of a reverse classic Mac OS fashion?

                                    1. 4

                                      Modern (‘80s onward) Pascal, including Turbo Pascal supported null-terminated strings. The original Pascal strings were limited to 255 bytes, so you needed either a bigger length field (more overhead) or a null terminator (same space overhead, computing length becomes O(n) instead of O(1)) to express longer things. It was one of the things I found most confusing learning Pascal: so many string types! Oh, and there was a bug in the Delphi 3 runtime that accepted a length-at-the-front string in one function that was supposed to take a null-terminated string and crashed if you did this. I spent a long time debugging that one.

                                      The Win32 APIs specify the kind of string explicitly. These may be lowered to a type definition for some native type, or to some structured type, depending on the source language. Windows (and COM)’s BSTR type, for example, has a 4-byte length field and a NULL terminator, which doesn’t map directly to either C or Pascal’s default string representation but is easy to surface from both.

                                      1. 3

                                        Windows even has some APIs that use C strings (LPCSTR et al) and others that use Pascal strings (BSTR).

                                      2. 1

                                        Oh, that's an interesting tidbit too. I knew about MacOS having traces of Pascal conventions well into the MacOS era because of Carbon, but I never really took a deep look at the Windows APIs from that perspective. Thanks for sharing it.

                                        1. 1

                                          It runs even into .NET due to the influence of Anders Hejlsberg. I was susprised at the familiarity of the Pascal shipped by InnoSetup compared to the APIs in the .NET Framework when I ended up looking into some InnoSetup scripting at a previous job

                                        2. [Comment removed by author]

                                    2. 11

                                      One note about the "methods first" type of thinking that I feel is missed in this post is; one of the biggest benefits it's ever brought me is discoverability.

                                      If I have a game variable how can I find out what I can all do with it? We basically have two answers to this 1. Read the docs and/or source code. 2. Type game. and get a pop up of most things that you can do with that type. run() might be a bad example for this because it's something you probably already know you need to call. But like to_string()? If you don't read the docs/src practically your only option is to type to_string( and hope that your IDE is smart enough to bubble &game to the top of the list of overrides, which it probably isn't.

                                      This is more a tooling failure than a language level issue though.

                                      1. 16

                                        I had exactly this conversation with Simon PJ almost ten years ago. I have a thing, I type ., I get a list of things I can do with the thing. That's an incredibly useful tool for discoverability. The pipeline operator in modern functional languages was inspired by this observation (I wasn't the first to make it and the pipeline operator predated my making it). It lets you get the same kind of autocomplete as chains of methods in OO languages.

                                        1. 10

                                          This is more a tooling failure than a language level issue though.

                                          I will always say that the true secret power of the Haskell language is Hoogle. For those unfamiliar, it's a search engine that lets you search functions by type.

                                          How do I split a file path into its components?

                                          > FilePath -> [FilePath]
                                          SplitPath : Split a path by the directory separator.
                                          SplitDirectories: Just as splitPath, but don't add the trailing slashes to each element.
                                          

                                          How do I reconnect those components?

                                          > [FilePath] -> FilePath
                                          joinPath : Join path elements back together.
                                          

                                          How do I check for a file extension?

                                          > FilePath -> String -> Bool
                                          # A bunch of very generic functions
                                          > String -> FilePath -> Bool
                                          isExtensionOf : Does the given filename have the specified extension? 
                                          

                                          How do I list the files in a directory?

                                          > FilePath -> IO [FilePath]
                                          listDirectories : listDirectory dir returns a list of all entries in dir without the special entries (. and ..).
                                          getDirectoryContents : Similar to listDirectory, but always includes the special entries (. and ..).
                                          getDirectoryContentsRecursive : List all the files in a directory and all subdirectories.
                                          
                                          1. 4

                                            You don't need objects for that. In most functional languages, functions related to a particular type are all collected in an appropriately named module, e.g. List.reverse, and your IDE will helpfully list all the available functions if you just type List..

                                            1. 3

                                              I find Kotlin extension functions to be a really great middle ground. They're implemented as f(object) but allow you to write them as object.f(). So you get discoverability and readability, but also the freedom to define them anywhere in any scope.

                                              1. 3

                                                You are not wrong, but it comes at a cost. In a lot of ways this has been a learning experience for me too. But let me try to share what distinctions that seem to work from what I've discovered so far:

                                                Allocators all share an interface, which originally was the whole range of alloc/realloc/free. However, those methods are now simply called acquire, resize, release. This is deliberate.

                                                So to complement this we have the alloc module with things like alloc::new(allocator, Foo) ("new" variants will zero out memory) and alloc::alloc(allocator, Foo) (no zeroing). The entire module is just full of variants: alloc::clone, alloc::new_array etc.

                                                So this becomes essentially: you have an allocator, then you have a module which contains all the various things you can do with that allocator. They're all found through alloc::<autocomplete>.

                                                A similar thing happens to streams. A stream has .read and .read_byte, but then the io:: module has a wide range of reading functions that works on streams.

                                                And this works well. There's no need to attach all the things as methods to the InStream type, but at the same time there's a designated place for these helper functions.

                                                The downside is that unless the module names are fairly succinct, the name gets long. But it's very free, and you can place functions in io that might work on an InStream, but which at the same time might not feel natural as a method for it.

                                                I'd like to emphasize that I have only used these last few years and there might be drawbacks that I haven't noticed yet. But I would (a) like to recognize the problem (there is of course the autocomplete variants that support foo.x syntax to match on functions starting with x and taking the type of foo as the first argument) and (b) that some amount of such code completion can be gotten by using the module and then code completing on that module, if the modules aren't too wide.

                                              2. 10

                                                While I can't speak to C3 or how you've marketed it, really, because this is the first I've heard of it, your observation about replacing C really resonated with me.

                                                I had only kind of noticed the shift, and would've interpreted your marketing the way you intended it. C was the second or third language I learned, after BASIC, depending on whether you count LOGO as a language. And it really served an application language. I knew that was much less common now, obviously, but I hadn't really observed that as a real change in its role.

                                                1. 14

                                                  It's easy to get it wrong, because you're getting users for which C is already something attractive. They're not going to tell you the messaging is wrong, because they're the ones who got it.

                                                  I only realized this when I started to understand why Zig was different from Odin and C3: Zig was targeting what C is used for today, while Odin and C3 were aiming for general application programming (not to say Zig can't be used for it, but just like C of today - it's not the focus). From that perspective people saying "Zig is more C than C" suddenly made complete sense.

                                                  And this is what led me to the revelation (for me) that when you say "C alternative" that means something else to most of today's programmers.

                                                2. 10

                                                  The methods are fundamentally linked to this problem. As soon as we write:

                                                  foo.do_something(bar)

                                                  we have created a hierarchy where the class of foo is more fundamental than bar. […]

                                                  The problem with OOP — or, if I may spread the net wider, "methods first" — is that the up-front architecture and the use of methods inhibit these changes.

                                                  As an aside, while this is true with the usual single-dispatch approach, some languages support multiple dispatch. Common Lisp Object System is a classic example, but nowadays Julia might be the most popular take on it.

                                                  1. 2

                                                    Multiple dispatch is super cool. I haven't used it enough to say anything about how it affects the design and architecture, so I'll leave it at that :D

                                                    1. 5

                                                      It's super cool from a distance. In reality, from what I've been able to find in Julia at least, it mostly turns out to be a means of implementing binary operators. (And it does that reasonably well in Julia.)

                                                      1. 1

                                                        When I was dabbling (poorly) in language design decades before C3, I had multiple dispatch on my wishlist – but I never really tried it out. I remember reading all sorts of papers on it (since multiple dispatch is a bit tricky to make efficient) but my research never really led me to try it out in practice. I should try some Julia in practice some day – not just read about it.

                                                  2. 6

                                                    One of the sad evolutions of computing platforms is that there is no lingua Franca that other higher level languages can be built upon at the “platform level” anymore. In the era of mainframe and personal computers, C was the lingua Franca that bound it all together. While competent in C, I benefited from”higher level” languages which tended to be built on C and took advantage of a nearly ubiquitous foreign function availability. Win32 was C. X11 programming was C. MacOS was C. I think the computer industry, though at times frustratingly diverse during this period, benefited. With the advent of mobile computing and browser-as-an-everywhere-application shell, that died. Realistically, if you want to do things in iOS, you have to Swift. Maybe some Objective C will still work. On Android, Java/Kotlin. On the web, JavaScript. Each of these higher level “improvements” also comes at the cost of decreasing evolution. Various “transpile/adapt/emukate X to Y” things exist, they’ll demo well at inception, and 2 years later the maintenance cost is horrific. None of JS/Java/Swift is as good at hosting other language solutions as C was. To me, something was lost when the various user interaction platforms of computation became as specific as they have.

                                                    1. 9

                                                      Win32 was C

                                                      This is not true at all. Win32 is designed to be language agnostic. Look in windows.h and you'll see pages and pages of typedefs at the top. These are all mapping language-independent type names into C. Many of these have direct equivalents in standard C, but the platform APIs were defined in terms of the language-agnostic types, complete with explicit calling conventions. Pascal and other languages could use these as well with the same localisation.

                                                      The higher-level bits of the Windows environment all expose COM interfaces. These are defined in terms of MIDL interfaces. Various tools can take MIDL and generate the interfaces from other languages (C, C++, Rust, Visual Basic, .NET, and so on).

                                                      1. 1

                                                        I think you're pointing to the fact that they brought with them features that simply weren't possible to translate to other languages. It's nothing C3 can solve of course, but it tries to be a good citizen – all its added types can be represented in C, and by extension – in any other language with C FFI. Slices, optional returns, error handling, bitstructs, typedata for runtime introspection. All have very simple C-friendly representations.

                                                        Because to me, how can you be a C alternative if you don't present an ABI that can be an expanded lingua franca? I know other C alternatives don't agree on this, but to me this is a vital thing.

                                                        However, as you point out, we're kind of forced to deal with the state of things that already moved far from where we started. But at least I try to do my part.

                                                      2. 5

                                                        I think this might have been 1996 and the internet was all new.

                                                        In Germany, the internet was new even until 2013: https://knowyourmeme.com/memes/events/neuland

                                                        1. 2

                                                          Oooh. So that's where the meme is from!

                                                        2. 5

                                                          Another part of being a C replacement is the fact that after a defined "1.0" version it must be backwards compatible till the heat death of the universe.

                                                          Zig is written with this in mind, which is why it makes so many breaking changes: Neither language nor stdlib api can change after the language was released as a "1.0".

                                                          Languages like C persist because your code from the early 90ies is still usable and maintainable today. If you want to be a true alternative, this stability guarantee should be part of the plan

                                                          1. 11

                                                            That's not quite true for C though. C is slowly shedding some features, e.g digraphs and trigraphs. And interestingly Zig wasn't originally planned to stop at 1.0, but rather 2.0, 3.0 etc was planned (see early issues on Zig).

                                                            The only language which I've seen being explicitly dedicated to the goal of "1.0 and no breaking changes after that" is Hare, not Zig.

                                                            In any case, even though C3 1.0 is a stop point for the language with no further changes planned, I feel I must push back a bit on the idea that this is something that completely defines C.

                                                            It is a nice property of C, but as we see in plans for future C versions, there are pushes to more aggressively deprecate features and add things that don't match C (the early version of defer of example – not to be confused with the one which is getting added).

                                                            I personally reject the idea that you should constantly add features in the way that is currently in vogue. However, I am also somewhat careful of making categorical statements. What if, some years later, the language has a deep and fundamental flaw that couldn't be predicted? Should the language then never adapt to this?

                                                            1. 7

                                                              Yes, but C has a huge disadvantage:

                                                              it came from a time where the empirical data of good and bad language design ideas was still fairly low.

                                                              we know have much more knowledge. "everything nullable" is rolled back by (afaik) java and c#, c++ also tries to get away from all the weird pointer semantics by getting clearer pointers, c does so as well.

                                                              optionals, tagged unions, sum types, ... are all coming to many languages as they have provem themselves to be great.

                                                              C has gotten a defer inclusion as it has proven itself to be great.

                                                              C has the burden to come from a time of less knowledge and experience.

                                                              new languages habe the advantage of knowing which ideas where shit and which not.

                                                              and: there's nothing bad about releasing new versions, but if we'd stick to semver, some C standard releases are minor bumps (ipdate and you won't even notice) and major (youe code may break).

                                                              this will likely be true for C3 and Zig as well. the good thing is that we can use semver or similar for language versions as well, and thus can communicate potential breakage trivially to the user.

                                                              1. 7

                                                                Yes, but C has a huge disadvantage: it came from a time where the empirical data of good and bad language design ideas was still fairly low.

                                                                The C language was first implemented on a machine with 24 KB of RAM. It wasn't feasible to implement a popular mainstream language on this machine, because the machine code for the compiler, plus the data representing the program being compiled, plus the Unix kernel, would not all fit in 24 KB. BCPL was an inspiration for C. Today, BCPL might seem to be a minimalist language. But you couldn't fit a BCPL compiler into this much memory. So the original C was a brutally stripped down language.

                                                                optionals, tagged unions, sum types, ... are all coming to many languages as they have provem themselves to be great.

                                                                Tagged unions already existed in Algol 68, which was one of the inspirations for C. But there was not enough memory to implement a fancy type system. The only reason C had a type system at all was to distinguish between different word sizes on the PDP-11: 8 bit chars, 16 bit ints and 32 bit floats. Any type system machinery not directly serving this purpose was superfluous and could not be fit into the available memory.

                                                                Once the Unix team got machines with progressively more memory, C grew more features and the compiler got larger. But by that time, they were already maintaining a C code base, and fundamental design decisions were not changed.

                                                                1. 2

                                                                  A side note: it's surprisingly hard to fit a good (as in, fits the task) tagged union on top of C syntax and semantics.

                                                                  1. 1

                                                                    thanks for the history lesson!

                                                                    i'm not able to answer longer answers rn, but thanks for the historic background.

                                                                  2. 5

                                                                    I largely agree, but I think we should always be a bit careful about declaring "we have much more knowledge". We do know more, but fads are very much alive in language design today.

                                                                    For example, it's not uncommon that some feature is made popular by some language and then almost every language needs to add it in order to stay "modern" and "relevant" and to damn with whether it actually fits that other language.

                                                                    Language adoption is also not driven by "what's objectively the best language", but rather by other considerations, from "access to developers" to "what it hyped and makes investors interested"?

                                                                    Furthermore language design is more an art than engineering still. So it's not necessarily an exercise in logically picking the best features from what we know.

                                                                    1. 7

                                                                      Some of the things in newer languages exist specifically because we have a lot of experience with not doing them. Null is a trivial example. Having null means that every pointer type is actually T* | Null, but with a special case in the language that you can implicitly cast it to a T* and then get some kind of badness when you use it if it's actually Null.

                                                                      C++ demonstrated that coupling subtyping and implementation inheritance leads to fragility. StrongTalk showed that you can completely decouple them and good performance if (and, probably, only if) you have a JIT with type feedback. Java tried to separate types and implementation composition a bit and showed that inheritance is the wrong tool. And this leads us to compositional approaches to code reuse combined with algebraic and structural type systems, which is conveniently something that's possible to make run quickly and gives you a static type system that feels almost like Smalltalk's entirely dynamic system to program.

                                                                      1. 2

                                                                        It's interesting though, that removing null in a programming language is itself an experiment, with drawbacks that we then need to learn in order to draw conclusions how to do it the best way in the next programming language – until we settle on some proven things that seem to be more or less universally good.

                                                                        A small nitpick and correct me if I'm wrong, but I seem to recall Bracha explicitly writing that the type checking in StrongTalk didn't feed back into the optimizations, and that the improved speed came from other innovations. But maybe you meant the mechanism where JIT analysis would determine the frequently used type and optimize for it, rather than talking about the type coming from the type system?

                                                                        1. 9

                                                                          It's interesting though, that removing null in a programming language is itself an experiment

                                                                          It's been running long enough that we have some useful data. C# introduced nullable reference types (which, really, means it introduced non-nullable ones) in 2019. TypeScript 2.0, which introduced an option (strictNullChecks)to require null to be explicitly listed as a type in union types, is was released ten years ago. These give data points on both retrofitting the behaviour to an existing language (C#) and applying it in a new language (TypeScript). TypeScript showed clearly that, if you have algebraic types, null can be represented cleanly as just another type in a union type (null | T is a nullable T, where T may be a concrete type or some other type expression) and so you don't need special machinery for it if you built it in from the start. Pony (11 years old) and Rust (14 years old) have both done this from the start.

                                                                          If you want to go further back: The C# null-coalescing operator (21 years old) is similar to the Objective-C default behaviour. Objective-C showed that defining operations on null often led to simpler code than throwing an exception or crashing but it can hide errors. C# then showed that it's often nice to have this as an option so that you don't hide bugs in the same way Objective-C did.

                                                                          That's 1-2 decades of evidence for replacements for null that all show benefits and have been used in languages with hundreds of millions of lines of code in deployment.

                                                                          A small nitpick and correct me if I'm wrong, but I seem to recall Bracha explicitly writing that the type checking in StrongTalk didn't feed back into the optimizations

                                                                          Correct. This is why it needed a JIT to be fast. The StrongTalk type system used mixins for implementation composition and protocols for subtyping. The static types were provided as protocols (it's been ages since I looked, I vaguely remember that each class implicitly had a protocol of the same name that described its type, but I could be misremembering).

                                                                          The key observation that they made was that static types define a constraint on a set of types that may appear, but in a running program the set that actually appear is smaller. Type feedback records the concrete types that appear and allows the JIT to specialise for them.

                                                                          With structural and algebraic type systems, you get somewhat tighter constraints and the ability to do a lot of this analysis statically. More importantly, you get a set of operations defined at any point that mean selector colouring can give you vtable-like dispatch (for the cases where you can't reify to static dispatch or inline) without tying the dispatch mechanism to the implementation.

                                                                      2. 3

                                                                        For example, it's not uncommon that some feature is made popular by some language and then almost every language needs to add it in order to stay "modern" and "relevant" and to damn with whether it actually fits that other language.

                                                                        This is my biggest pet peeve. Incidentally, it's also why I like Lisp (Scheme), because the language is powerful enough to let the user add and experiment with such features. There's no need to wait for the language implementor/designer to hand it down to you. And that also means the feature isn't written in stone and there forever because of backwards compatibility.

                                                                        I think C is also a bit like that - a lot of features aren't needed in C, but that's more because it's so low-level, you can build many such features from the basics.

                                                                        1. 9

                                                                          The problem with this approach is the everyone does build their own implementation of these ideas and those then don't compose. I've seen C codebases that had to deal with over a dozen (incompatible) string abstractions, for example, because the core language doesn't give you a usable one and every library that wants one implements a subset of the features than a generic one would provide. I've also seen C codebases with multiple incompatible hash tables, where switching a piece of code between them was a major refactoring effort because there was no common interface and their semantics were subtly different.

                                                                          1. 2

                                                                            I agree this can be a problem, but it's also something the project team has to manage. Why on earth were those codebases allowed to grow so many different string abstractions?

                                                                            It helps if there are popular implementations, like Boost - that eventually make their way into a standard. For example, in Scheme we have the SRFIs for commonly implemented things. The biggest disadvantage of those is that they're somewhat "design by committee". In the best case, people explore first and then the good designs get taken up and turned into a standard. Then the bikeshedding happens on something that has been proven in practice, and any potential flaws get taken out.

                                                                            1. 7

                                                                              Why on earth were those codebases allowed to grow so many different string abstractions?

                                                                              My guess is a combination of the following:

                                                                              • There is no such thing as a one-size-fits all good data structure for strings.
                                                                              • Designing a good string interface that can efficiently support all data desired structures is hard.
                                                                              • C doesn't really have facilities for designing abstractions over data types.

                                                                              Each library implements a string data type that meets their needs and then a program ends up needing to work with half a dozen libraries that all brought their own string data types. And, because C doesn't have abstractions over types, there's no way of making this generic.

                                                                              C++ makes this a bit easier, but has the problem that they did standardise the world's worst string abstraction in 1998 so now replacing it with a good one is hard because everyone has implemented their own (last time I looked, WebKit had three entirely unrelated string abstractions, one was called WTFString, which seemed appropriate).

                                                                              Lisp has less of a problem here in that it can express abstractions cleanly, so you can usually paper over different implementations with mostly compatible semantics.

                                                                              1. 3

                                                                                And heck, even within the standardized abstractions of C++, you could improve efficiency in various ways. But they also de facto stabilized the ABI of all the standard library types in addition to the source-level abstractions, and so we're truly stuck with some very inefficient datastructures.

                                                                            2. 1

                                                                              Do they, though? I've never actually seen this issue of everyone building their own version of stuff manifesting in Common Lisp codebases. Despite so many open source CL codebases being single author projects and CL being famous for being one of those "make your own solution" langauges, there's a good number of canonical libraries everyone uses for solved problems.

                                                                              1. 3

                                                                                I suspect Lisp and Smalltalk are outliers here because they both:

                                                                                • Make it easy to build abstractions over types.
                                                                                • Make it easy to build adaptors that wrap one type and make it look like another.
                                                                                • Avoid performance cliffs when you do any of this across module boundaries.

                                                                                C fails on the first point, it doesn’t support building abstractions over types.

                                                                                C++ fails on the last one. It’s easy to make abstractions over types and to wrap other types but most of the tools for this require full visibility of the code. As soon as you get to a compilation-unit boundary, you lose a bunch of tools. And that’s why so many C++ libraries are ‘header only’.

                                                                                Swift does a better job at both (and is about the only language that starts with a well-designed string type. Objective-C did but then the requirements changed after they’d baked their interfaces) but reifies generics only within a shared library, they do type erasure at boundaries. That’s better than most alternatives that support ahead-of-time compilation with shared libraries.

                                                                      3. 6

                                                                        Right, C's level of backwards compatibility is very high but not infinite. It is shedding features decades after they fall out of use. To get to that same level, a new language has to become decades old first, for starters.

                                                                        And also yes, I agree that some things are too flawed even for C and need to be broken or removed. It's good that gets() is gone as of C11 for example.

                                                                        1. 4

                                                                          C is slowly shedding some features, e.g digraphs and trigraphs.

                                                                          To be clear, digraphs were introduced by C++98 and C99 to replace trigraphs (digraphs don’t have the obnoxious rewrite semantics of trigraphs).

                                                                          Another example is gets() which was dropped before trigraphs.

                                                                          1. 2

                                                                            I remember reading about Zig having some tools in the future to automatically port (or help porting) old Zig code to newer Zig versions. I think (though less sure about that) same thing is also being thought for Hare.

                                                                            1. 4

                                                                              zig fmt does that, but only for unambigious changes, and the results sometimes look rather crude.

                                                                              it also does not adjust the code to changes in the stdlib

                                                                            2. 2

                                                                              Clojure is dedicated to the backwards compatibility goal very firmly. Following in the footsteps of Java.

                                                                            3. 4

                                                                              More to the point: C is from an era when languages had actual written-down standards both implementers and developers could point to to say "This is how it works in this version of the language, defined by ANSI/ISO standard... " and if you wanted to diverge from that, like the GNU people do with GNU C, you mention it explicitly in documentation. Standards change slowly, but they do change, and old standards continue to be available.

                                                                              With modern languages, there's One Blessed Implementation and whatever that does is correct by definition. Only perl Can Parse Perl as the saying goes. Non-standardized languages can have versions and versioned compatibility but there's no external body maintaining an independent source of truth for the language, such that anyone could rock up and implement it with no reference to the source or behavior of another implementation.

                                                                              1. 16

                                                                                C is from an era when languages had actual written-down standards both implementers and developers could point to to say "This is how it works in this version of the language, defined by ANSI/ISO standard... "

                                                                                Remember C took off in the 1980s when the main reference (K&R) didn’t specify features (lvalue structs, enum, void) that were part of the language since 7th edition Unix or soon after.

                                                                                ISO-style standardization is a very mixed bag for programming languages. It is not helpful unless the language has multiple actively maintained implementations and they need a forum where they can coordinate changes to the language. Standardization doesn’t help multiple implementations to appear, and if the standards process isn’t working for the implementers then it’ll be ignored.

                                                                                Looking at this list of languages with ISO standards there are maybe only the 4 most popular (C, C++, JavaScript, SQL) where the process is effective. There are dusty decks that are fading away (COBOL, Fortran, PL/I) which might have had effective standards in the past. There were languages that were popular despite standards that were widely ignored (BASIC, Pascal). There are single-vendor languages that left their pointless standards behind (C#, Ruby). There are languages that weren’t popular enough to sustain the overhead of maintaining a standard (Modula-2, Eiffel).

                                                                                1. 7

                                                                                  JavaScript is a little bit different from the others in a couple of ways.

                                                                                  First, (at least, unlike C/C++, not sure about SQL), the ISO standard is just ISO ratifying an ECMA standard (I wasn't even aware it had an ISO standard), which means that it doesn't go through the same process as ISO standards. I'm not particularly familiar with the ECMA standards process, the ISO process is really designed around physical things where the cost of divergence from a standard is high because interoperability is vital, and companies that build or use widgets in category X have a strong incentive to pay people to attend their country's standards-body meetings on X and no one cares about the standard unless they are in a narrow field. This doesn't really apply to something like C++. The BSI C++ group is the largest BSI group and, I think, is more than twice the size of the second largest. The processes really weren't designed to work for this kind of community.

                                                                                  Second, and most importantly, JavaScript has an executable formal specification. I wish more languages would produce such a thing. It gives you a gold model where you can say 'my implementation does this, is it allowed by the standard?'. It's easier for JavaScript because there's intentionally less variation: the same C code is expected to work on dozens of architectures and operating systems, but the same JavaScript code is expected to work in the same way on all implementations (modulo performance and extension APIs that are available).

                                                                                  1. 9

                                                                                    Yeah.

                                                                                    There are a bunch of problems with the ISO process. ISO standards are ridiculously expensive, and from the typical programmer’s perspective, if documentation is too hard to obtain it might as well not exist. ISO requires its committees to work in secret and restrict participation to people with credentials from their national standards bodies. (Somehow the committees for C, C++, and some adjacent standards have managed to work somewhat in the open, which ISO is not happy about.) (And in another anti-ISO process hack for ISLISP, Kent Pitman wrote the spec “outside the committee” and “gave it to the committee unencumbered” so that he could publish it freely.)

                                                                                    Other standards bodies are more open than ISO to varying degrees. But to me it’s unclear if their added process and imprimatur is much improvement over a multi-vendor spec documentation project hosted on a suitable software forge.

                                                                                    1. 9

                                                                                      We're in the weird position with C++26 that some national bodies voted against the standard (I don't know if this has happened before), but if one national body publishes it then it becomes the de-facto standard and so it turns out there's basically no way for ISO to not publish the standard unless there's overwhelming consensus against it.

                                                                                      Typically the final draft is published, so it doesn't matter hugely that the 'official' standard is expensive, because everyone works to the version that was published as a final for-approval draft.

                                                                                      Other standards bodies are more open than ISO to varying degrees. But to me it’s unclear if their added process and imprimatur is much improvement over a multi-vendor spec documentation project hosted on a suitable software forge.

                                                                                      I've seen a bunch of failure modes in standards bodies. The two extremes are:

                                                                                      It's too hard for experts to get involved. You end up with only vested interests putting things in, but no overall design and no review from people who could avoid things that are known to be a bad idea by experts in the field. HSA was the poster child for this, which is why HSAIL ended up being my case study in how not to design a compiler IR when I was teaching the MPhil compilers course at Cambridge. There were lots of people who could have told them why their design was stupid, but none wanted to pay $20K to be individual experts.

                                                                                      It's too easy for people with no stake to participate. People who won't be the ones suffering from a bad standard have equal voice to the people who are going to be maintaining compatible parts of the ecosystem for future decades. You end up with exciting research ideas being pushed into the standard and then, once they're deployed, people discover why they were bad (the ones who knew and said at the time are outvoted).

                                                                                      ISO's process manages to collect some aspects of both failure modes. It's perceived as being hard to join (it's actually pretty easy, at least for the BSI delegation to WG21 - there's some bureaucracy but it doesn't take much effort, just a lot of waiting), so a lot of experts don't join. It's also easy for people to join if they're very enthusiastic, but very few people in the standards process are actually implementing C++ and so, for example, things keep being added to the standard library at a rate that outstrips implementers' abilities to keep up. When things are added, they constrain every future version of the standard, but the people who are inflicting that pain are not the ones who experience it.

                                                                                      1. 3

                                                                                        Really good insight into those processes, I wish I could highlight this comment because I think it's something that would be valuable for more people to know about.

                                                                                        I guess you could write a blog post, but then odds are people won't read it because standards work tend to turn most people off.

                                                                                2. 12

                                                                                  That's revisionist. C only got a spec after it got popular, and there was over a decade of implementations just copying each other. Languages having specs was never an era - some had them, some didn't.

                                                                                  The scope of the C standard is too small to even compile a "Hello World", so in practice we do have GNU-flavored implementations in eternal disagreement with One Blessed Implementation of MSVC.

                                                                                  1. 4

                                                                                    C only got a spec after it got popular

                                                                                    C had a spec since the early days, titled “C Reference Manual”, and published in the Unix documentation and K&R. It’s the direct precursor of the standard: the shape of the standard and much of its wording came from dmr’s spec. In the 7th edition era the spec was shaped by Steve Johnson’s pcc as well as Dennis Ritchie’s compiler.

                                                                                    The scope of the C standard is too small to even compile a "Hello World"

                                                                                    Well, it doesn’t specify the compiler command, but it does specify more than enough IO and character display semantics for Brian Kernighan’s first example program.

                                                                                    1. 9

                                                                                      Why are we counting C's reference manual all of a sudden? It's decidedly first-party, no "external body maintaining an independent source of truth" as described upthread.

                                                                                      If we're counting first-party documentation by the authors of the original/primary implementation, a whole lot of newer languages now have a spec.

                                                                                      1. 5

                                                                                        Why are we counting C's reference manual all of a sudden?

                                                                                        Because there’s a difference between a standard and spec.

                                                                                        1. 3

                                                                                          Oh you're quibbling about kornel using "spec" to mean "standard". Sure, whatever. Does it change anything about the point though? Did the era chbarts describes ever exists?

                                                                                          1. 6

                                                                                            Did the era chbarts describes ever exists?

                                                                                            I learned C in the early 1990s when ANSI C was new, using the 2nd edition of K&R with Norcroft C and GNU C. So my knowledge of C comes from an era described by chbarts, and it was the peak of comp.lang.c and comp.std.c on usenet, but that’s not what C “comes from”.

                                                                                            But at that time (perhaps moreso in the 1980s?) a lot of other languages aspired to having a Standard, it was seen as the path to being a Real Language. Even though that was a mistake, getting the arrow of causality wrong: standards are the consequence of success. Going for a standard too early is more likely to hinder success than cause it.

                                                                                            1. 2

                                                                                              I see what you're saying now, and I think I agree. Even though I wasn't around in the 90s, I have witnessed this "Real Languages need an ISO standard" even for newer languages and always found it weird. The idea that it's people confusing correlation for causation seems like a good explanation.

                                                                                  2. 5

                                                                                    Hare started with a spec first. C3 has a spec as well. The criteria for Odin 1.0 is going to be having a spec. So I don't think "possible to implement from a spec" is really forgotten. At least not when it comes to Hare/C3/Odin.

                                                                                3. 5

                                                                                  (And this is also why Zig's decisions are so different from C3's: Zig's primary goal seems to be to replace C where it's used today.)

                                                                                  Very cogent observation!

                                                                                  1. 5

                                                                                    It’s strange to me: what is it about OO that it motivates people to adopt such strong “No True Scotsman” views? I write Java[0] in my day job and a lot of my hobby projects, but I freely adopt more or less procedural or functional approaches depending on what I feel makes sense for the context (and my view about which contexts require which approaches still isn’t fully settled after over a decade).

                                                                                    [0] Inviting the inevitable “Java isn’t really OO” no true scotsman takes.

                                                                                    1. 5

                                                                                      It's a result of huge amounts of marketing from the 1990's and 2000's, which built off of both the fear of an oncoming software apocalypse due to the perceived failure of traditional/structured programming to keep pace with the demands of software, and the 1970's/1980's Unix counterculture which was full of "no true scotsman" vibes anyway. So you got billions of dollars of holy war stoked by different software vendors, none of which really had particularly revolutionary products in retrospect.

                                                                                      Or maybe not, I'm just an idiot with an opinion.

                                                                                      1. 5

                                                                                        I think you are doing like me. Don't get me wrong — I'm fine doing Java, it's easy. But what I realized is that I basically internalized the cost in upfront architectural design. The way we get better at OO is getting better at guessing the correct structure up front. But we still pay in many ways in extra code just for doing OO.

                                                                                        And the promise of OO was that it was going to be more extendable, more easy to refactor because we paid this cost. This, however, was largely wrong. By encapsulating state, the promise was that state was going to be manageable. What this failed to account for is that by dividing state and still having an object graph, we're not actually encapsulating state, we end up distributing it, which is making it worse.

                                                                                        Naturally it will differ by project and by author, but the observation is that the promise of OO is largely false.

                                                                                        This is not going into OOD, which is another thing and is just plain wrong (you can use OOP without doing full OOD, so that's an important distinction)

                                                                                        1. 5

                                                                                          I don't want to get into the no true Scotsman of saying Java isn't OO, but a single data point is not necessarily representative of the entire space. Not all OO languages couple implementation inheritance with subtyping in the way that Java does and that's where a lot of the rigidity comes from in Java (and C++ if you write OO-style C++). That is not intrinsic to object orientation, any more than having type classes is intrinsic to functional programming. Objecting to OO on those grounds is like saying 'I don't like monads, therefore I don't want a language with higher-order functions'.

                                                                                          1. 4

                                                                                            It's not really the inheritance/subtyping I'm pointing to, but rather the resulting object graph. This is especially problematic in Java and C++ where "manage state by turning it into fine grained objects" is THE way to do it.

                                                                                            I find Objective-C much better because it can be used in a more pragmatic way, where the OO can be used as high level glue between components, but internally you can use fat objects with a lot of code just written in C. It doesn't need to be OO all the way down.

                                                                                            If you look at Brad's original vision for Objective-C it was the "Software-IC", where you'd create reusable components, but the internals could be whatever you wanted. If you go with that idea then OO stays out of your way except where it's useful.

                                                                                            So a rough way to phrase it is "the less granular you make the objects, the less you need to think about architecture up front". And as I said in the blog post and here: I believe that architecture up front to be the real problem. And it's not limited to OO, it's about that "method-first" thinking.

                                                                                            1. 3

                                                                                              I think the method-first framing is interesting.

                                                                                              The big known limitation of OO is that it's hard to add a new verb to the system: verbs are associated with nouns and this means that you need to modify every noun that might be expected to work with that verb.

                                                                                              The big known limitation of FP is that it's hard to add a new noun to the system: verbs require overloads or pattern matching on their behaviour and so adding a new noun requires modifying each of the verbs.

                                                                                              There's a technical term for this in PL theory, but I'm not good at remembering PL terms.

                                                                                              Neither of these is intrinsically more fragile in principle, but one will typically be harder than the others for specific kinds of refactoring. I generally subscribe to the software-engineering philosophy that says 'get your data model right and the rest flows naturally' (I first heard this expressed explicitly by the authors of JMAP and it resonated with how I think about software design). This works well in an OO sense.

                                                                                              But, as always, no language really implements these extremes. Objective-C has categories, for example, which make it easy to extend existing objects (although the lack of namespacing makes this somewhat error prone). C# has extension members, which provide roughly the same thing but with some scoping, which makes them more composable.

                                                                                              But I don't really know what 'methods first' means, because OO design, to me, is units of encapsulation first. Defining the things whose implementation details don't need to leak into the rest of the code makes it easy to do top-down design and bottom-up implementation, which remains my preferred way of working. And the design never quite survives contact with the code, the boundaries are always in slightly wrong places (sometimes in completely wrong places), but that's fine. The ability to move them around a bit is a property more of the concrete set of abstractions that a language picks than about the underlying paradigm. Java, for example, really encourages you to use concrete types rather than interface types and has only nominal typing, which makes it very hard to make changes without their impacts spreading across the codebase but that's a property of choices that are not specific to OO.

                                                                                              1. 4

                                                                                                There's a technical term for this in PL theory, but I'm not good at remembering PL terms.

                                                                                                Phil Wadler called it the expression problem which tbh is not great terminology because it doesn’t try to describe the problem directly but instead refers to the usual example that illustrates the problem: your program works on an AST for expressions, and you consider how the program changes as you add node types (nouns) such as variables, constants, add, multiply; and operations on the AST (verbs) such as evaluate, print, compile.

                                                                                                I generally subscribe to the software-engineering philosophy that says 'get your data model right and the rest flows naturally'

                                                                                                Fred Brooks (in The Mythical Man-Month) phrased it as, “Show me your flowcharts and conceal your tables, and I shall continue to be mystified. Show me your tables, and I won't usually need your flowcharts; they'll be obvious.”

                                                                                                Linus Torvalds wrote on the git mailing list in 2006, “I will, in fact, claim that the difference between a bad programmer and a good one is whether he considers his code or his data structures more important. Bad programmers worry about the code. Good programmers worry about data structures and their relationships.”

                                                                                                Rob Pike’s fifth rule of programming (1989) is, “Data dominates. If you've chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.”

                                                                                                Those are the versions of this idea that I know of (at least, I remember the people but I have to look up their exact words). I’d love to collect more quotable quotes along the same lines.

                                                                                                1. 1

                                                                                                  My objection to “method-first” isn't really about methods versus data, or about inheritance. It's about the mental model that comes with it. When every piece of state is turned into an object with methods, you start modelling the program as a society of autonomous agents: give object A a command, A commands B, B commands C, and so on. The resulting delegation graph is the architecture, so you have to decide that architecture up front.

                                                                                                  That's what I mean by “method-first thinking”. Data stops being plain data that code operates on and becomes a collection of objects, each of which is expected to own behaviour and make decisions. Making the objects less granular breaks that chain. A large object can encapsulate a substantial amount of state and implementation without requiring you to decide all of those boundaries beforehand.

                                                                                          2. 3

                                                                                            It's really hard to see especially since the components of "OO" (encapsulation, runtime dispatch, subtyping) are wildly successful. People hating on OO for little reason than fashion is getting a bit old.

                                                                                            1. 3

                                                                                              I tried to explain it in the blog post, but it was very brief. The problem comes from the upfront architecture you need. As we get better, we tend to internalize this, but fundamentally for OO we immediately need to consider how objects relate to each other, which is accessed by what.

                                                                                              An easy sign of the object graph being sub-optimal is when you end up with things like parent.foo().bar().baz() – I think we all run into this time and time again.

                                                                                              In this situation the solution is usually to rethink what belongs to what and so on. In languages like C++ pre smartpointers, this was doubly difficult due to the de-facto ownership might end up being moved around (a very common source of C++ bugs early on).

                                                                                              If we don't think "method first", we don't run into this. The initial data can just be a flat god struct, and we can move data around freely, since it's not attached to behaviour. We can start grouping information as we learn more about the problem space, rather than having to take a stab in the dark and then try to refactor it right over the course of development.

                                                                                              This model also explains why there is "good" uses of OO and "bad". If we have a wholly self-contained object, which perfectly abstracts its internals, then such an object can be freely moved around in the object graph – it has no preferred place, and its internal state is completely encapsulated. Something which has zero coupling. This is good and useful.

                                                                                              The "bad" use is when we have an object and yet it's dependent on the state of its children and/or its parent. In other words it has coupling.

                                                                                              In OO we might prefer the former, but we need the later type to do any real work. If we can break out of the "object thinking" we can make this flat, but my experience shows that this almost never happens in practice. The habit is instead of just making an extremely tightly coupled architecture and then think it's somehow loosely coupled just because the explicit dependencies in the graph seem few, when in fact moving a class in the hierarchy requires rewriting all its methods and methods of its parents.

                                                                                              I'm being a bit abstract now but I'm sure that if you think about it you can see what I'm pointing to.

                                                                                              This is not the usual thing I hear about when people criticize OO, but to me it's the main problem.

                                                                                              And this also happens even without explicit OO and just "method first" style of code. It's not tied to OO itself.

                                                                                              1. 1

                                                                                                Good points, thanks. I realize I had misrepresent your point in my N~2 post, I was speaking about an ambient "throwing OO with the bathwater" style of thought, which is unrelated to your very valid points and blog post.

                                                                                                Something I sometimes regret about encapsulation is a bit the too easy to mix interface vs implementation (unlike in Pascal), the fact private methods can call public ones, or that oftentimes Java-like interface are underused in presence of subtyping. Maybe there is an orthodox OO decomposition to find.

                                                                                                imo this would be about improving modelling by creating fluency where it is needed (the decoupled interface) while allowing perhaps more sophisticated information hiding (different users of a class?)

                                                                                                1. 1

                                                                                                  I haven't really dived into the question that much. I'm adept at tailoring something like Java to my whims (unless people impose that I must use all those useless Java frameworks), and unless performance requires something different. Java would be my to-go-to for writing game servers. I like the ability to introspect during execution and the various levels of catching exceptions you can utilize. I just had a good time with it.

                                                                                                  But that said, doing something else where I don't need a VM or anything like that, I now very much prefer just procedural, because it reduces the architecture to nothing and tends to produce so much leaner and more easy-to-understand designs compared to the OO solution with the same amount of functionality.

                                                                                                  But I am pragmatic, I use the tool that I find most convenient for the task, not what some dogma tells me.

                                                                                          3. 9

                                                                                            Zig's primary goal seems to be to replace C where it's used today.

                                                                                            Zig's goal is the one stated on our official website:

                                                                                            Zig is a general-purpose programming language and toolchain for maintaining robust, optimal and reusable software.

                                                                                            None of what is written there corroborates the interpretation given in the blog post nor what has been argued in comments:

                                                                                            while Odin and C3 were aiming for general application programming (not to say Zig can't be used for it, but just like C of today - it's not the focus)

                                                                                            If replacing C "where it's used today" was the main goal, we would be recommending to rewrite everything in Zig instead of creating things like https://github.com/allyourcodebase.

                                                                                            The whole point of AYC is to leverage the existing C ecosystem to build new software that can depend on pre-existing C code in a robust manner (instead of jerry-rigging a build pipeline that adds more system dependencies, and that might or might not work on the platforms you know less about).

                                                                                            1. 18

                                                                                              Do you see the difference in Zig's message "general-purpose programming language" and the quote's "general application programming". The key is in "application". Why isn't C popular for that (consumer apps and games) today and does Zig address those problems?

                                                                                              I always thought Zig's decisions on explicitness an odd focus, but it is explained if you, as another Zig user said, see it as "a language more C than C". It's "we want more details than C", you get that in Zig:

                                                                                              More precision in casts, in pointer types, in memory management, in IO handling.

                                                                                              But getting good for general application programming is about being able to quickly address the main functionality of the app. That means: more abstractions, more ergonomics. Exactly what Zig is opposed to. Zig wants to be more explicit with fewer abstractions, and ergonomics is second to this explicitness as well.

                                                                                              The Zig bet is that by having more explicitness you will get something "optimal, robust and reusable".

                                                                                              But the general application often doesn't care about "optimal, robust and reusable" that much. It's about delivering the application in the hands of the user and allow them to achieve what they want.

                                                                                              Time isn't infinite, if the programmer spends a long time on the explicit code and this makes the code better, then that still means less time for things like features that the user also cares about.

                                                                                              It's a trade-off. C3 doesn't walk all the way to something like Python, but it still tries to offer same or more conveniences as C++ or Swift would. That's a trade-off, because going more to Python/Ruby would make it more high level but also less performant and less possible to do low level code.

                                                                                              If Zig wants to be nicer for applications then it needs more ergonomics and abstractions, but then it can't be the explicit, detailed low level thing it currently is.

                                                                                              Examples: Zig stdlib removes managed dynamic arrays – more detail yes, less ergonomic and less easy for general use. Zig doesn't have operator overloading – more explicit yes, but less ergonomic and easy to use. I could go on. It's just different trade-offs and you can't say Zig both is explicit and has comparable abstractions, because it doesn't.

                                                                                              1. 6

                                                                                                I think I read "replace C" differently than you. It's not that the Zig project wants all C code to be rewritten as Zig code. But it would prefer us to write Zig over C right? It even goes as far as to let you start adding Zig to your C codebase. My rephrasing of this might be "Zig competes with C in the contexts where you'd reach for C today".

                                                                                                This certainly rings true for me watching from the outside. I think of C as a language you use when you want very fine grained control over what your computer does. And I think of Zig as a language you'd reach for in that same circumstance. Andrew has even called Zig "a DSL for emitting machine code," which I think is cool!

                                                                                              2. 3

                                                                                                As I write my own C alternative, I'll keep this in mind. I think my own definition is closer to the authors, though I learned C when it was already relegated to niches. With the perspective from the post, I'd assume others would also see mine as more of a C++ replacement as well.

                                                                                                1. 2

                                                                                                  The C3 author always come across a nice and respectuous person, FWIW.

                                                                                                  1. 2

                                                                                                    Good article. I think I will use procedure style more then "methods first" when writing in C3. As everyone can make own @inline procedures calls in methods if they want to make API more OOP styled.

                                                                                                    I also started from BASIC and, also, then found Turbo Pascal, then C++, C# and finally found myself working 20 years as webdev who forget about his initial intentions...

                                                                                                    Now I am feeling good to have such tools as C3. Thanks!

                                                                                                    1. 1

                                                                                                      what does tscoding has to say about this?