Lisp, for example, is very opinionated when it comes to things like side-effects. Go is opinionated when it comes to how memory should be managed. Rust is opinionated about when data should be readable and writable, and passed between threads.
C doesn't impose any of these restrictions, largely by virtue of being low-level, and moreover, it should be possible to implement whatever data transforms are implemented in any of those other languages in C as well. The inverse is not the case, so I would argue that C is more permissive than any of those languages.
* In the absence of language extensions, you don't have a pointer type that can be incremented all over the place and dereferenced. You don't just mutate memory willy nilly.
* In relation to the above point, (in the absence of a language extension like "locatives") there is no address-of operator; we can't pass the address of a variable somewhere to have it changed. This can be simulated with a lexical closure, whereby we provide the function to do perform the change, which is called from somewhere. "Use lexical closures as thunks if you want var parameters" is quite opinionated.
* All mutation takes place through data-structure-specific functions (like rplaca for mutating the car of a cons cell), or dedicated special forms (like setq for mutating a variable). The use of generalized places like (incf (car x)) compiles into type-safe function calls. On one particular Lisp, CLISP, that looks like:
1> (macroexpand '(incf (car x)))
(LET* ((#:TEMP-3341 X) (#:NEW-3340 (+ (CAR #:TEMP-3341) 1)))
(SYSTEM::%RPLACA #:TEMP-3341 #:NEW-3340))
there is no idea there of calculating some effective address and just clobbering bits through that pointer. The original value is accessed using car, and the mutation happens opaquely inside the SYSTEM::%RPLACA function. That could be inlined by the compiler, but the model is basically that mutation of objects is done via an API.* The argument evaluation order of standard functions is well-defined, so side effects occurring in argument forms are well sequenced:
(let ((x 0))
(list (incf x) (incf x) (incf x)))
--> (1 2 3) ;; required result in ANSI Lisp
(Scheme bungles this by having less of an opinion; it has C-like unspecified argument eval order.)Ermm...no, it's not. Functional programming languages do tend to do that, but most lisps aren't functional--certainly, it's not a defining characteristic of the language family.
> C doesn't impose any of these restrictions, largely by virtue of being low-level, and moreover, it should be possible to implement whatever data transforms are implemented in any of those other languages in C as well. The inverse is not the case, so I would argue that C is more permissive than any of those languages.
1. Turing equivalence.
2. Because it's so close to the hardware, c inherits the opinions of all the major ISA designers.
Fair enough. I haven't worked with Lisp since university days, but I think the point stands with any strict functional language.
> 1. Turing equivalence.
Ok technically it's possible to write programs which perform the same computation any two turing-complete languages, but what I mean is that, for example, it's easier to emulate how an arbitrary Go program performs that computation from C than in the other direction.
edit:
> 2. Because it's so close to the hardware, c inherits the opinions of all the major ISA designers.
That's one way to look at it. Another way is that C just gives you an interface to the hardware as it is, and doesn't make many judgements about which abstractions should be built on top of it by default.
You're conflating lack of guardrails & helpers with "low-level". C is not lower level than Rust or C++, not at all. It's just more dangerous, and that danger brings no "low-level-ness" advantages.
> it should be possible to implement whatever data transforms are implemented in any of those other languages in C as well.
Define what you consider "possible" here. There's a bunch of things I'd consider "not possible" in C that you can do trivially in C++/Rust, such as the codegen templates & coroutines perform, for example. If you throw enough macro pre-processor hacks at it to the point it doesn't reassemble C in the slightest sure you can sort of pull it off. But is that sort of thing considered "possible in C"? Or have you just made a second language in front of C that happens to compile to C via the hello world of compilers?
Even with macros I'm not entirely sure you can actually do something like static_assert in C since you can't put an #error in a macro substitution. Or more practically defining the backing type of an enum
- If you're a variadic function, you're forbidden to know directly how many arguments you have been given and what their types are"
- If you're any function, you may know who called you, without resorting to platform-specific extensions that come with various limitations.
- You may not know which areas of your activation chain are root pointers, for implementing accurate garbage collection.
- You may not dynamically construct a function call with arbitrary arguments and invoke it
There are some languages that can get smaller than C. Assembler, obviously, is basically the smallest. Rust does have a default runtime, but it's thin, and most of the rest of it can be shut off. Still, it's not a long list of languages that are less opinionated than C.
[1]: If you want to get really technical, this is "relative to the hardware it is running on". C gets a bit of a bonus here because the hardware has over the decades actively targeted C. Particularly visible in the processor's support for calling conventions, and manipulating a "stack" so natively. (While a stack is really, really convenient, it is not, strictly speaking, necessary for programs to be structured that way.) But for the sake of simplicity I'll ignore that for now.
For instance, Javascript has a massive runtime, but I'd say it's an extremely unopinionated language. If you want, you can implement a Javascript function which rewrites itself every time it's executed.
In my opinion Rust is an extremely opinionated language.
Languages have a lot of aspects.
I used this aspect because upthread we were discussing memory management and "portable assembly". In another context I'd happily use this concept instead.