C++ Has Become More Pythonic (2014)
preshing.com
preshing.com
Hmm? Go has almost none of these features.
I do wish it had support for slice unpacking like it does for return values. The assignment from returns or ranges is one of the things I really ejoy.
Edit: [1] I'll clarify that I define that as an immutable, indexable data structure that you can unpack in to multiple assignments.
Most languages with tuples have indexable tuples either via language syntax or library functions. (For example, OCaml practically does, with fst and snd.)
> It doesn't have "true tuples" but you can do multiple returns in a tuple-like way and there is syntax for unpacking it.
There's a large difference between having multiple return values and having first-class tuple values.
The parent also mentions: binary literals, raw string literals, uniform Initialization, standard algorithms like map, filter and any, and parameter packing. All of which Go doesn't have.
Plus, Go range loops are constrained to built-in types (slices, channels, etc.), unlike C++/Python where you can implement begin()/end() or the iterator protocol respectively and get it for any type.
So, from 10 things mentioned, whereas C++ and Python share ALL of them, Go only has 2 of them and sort of has one more (the range-for).
I'd stick with: "almost none of these features".
The definition is very simple - the inference engine must use nontrivial inference rules to reconstruct the types. The rule “given P then P” alone doesn't quite cut it, which means that ”given that 0 is an int, then something that's initialized to 0 is an int“ also doesn't quite cut it. Calling what Go, C# and C++11 have “type inference” is akin to calling Python a statically typed language because it has a trivial type system with exactly one static type.
> actually most languages before Rust and Swift that have local type inference (C#, D, C++11) are limited to type inference at the declaration/initialization point.
Then they just have unidirectional type propagation - which is perfectly fine, just not type inference.
Where exactly is this rule coming from ? What is the source of your definition ? And if it is the authoritative definition, why don't you go rewrite the wikipedia page, which is then wrong ? https://en.wikipedia.org/wiki/Type_inference
> The rule “given P then P” alone doesn't quite cut it
"Doesn't quite cut it" sounds like a very precise and scientific definition ! Also your categorization of C# is wrong, even by your own definition, because of function types inference and of subtyping, which makes the algorithm non trivial.
> Then they just have unidirectional type propagation - which is perfectly fine, just not type inference.
Again:
1. This is wrong, even by your own definition. You can have local type inference limited to the initialization point, and have non trivial resolution rules. See this paper by Benjamin Pierce for an example : http://www.cis.upenn.edu/~bcpierce/papers/lti-toplas.pdf
2. Where is this definition even coming from ? In my book, unidirectional type propagation is a form of type inference, and it quite logically follows: The type is inferred. The fact that you chose to draw a line, say, to flow sensitive inference (in the case of Rust and Swift) or to global unification style inference (ala ML) is a completely arbitrary definition, and one that I have to this day never encountered. Indeed, I can't find any online resource that agrees with you. Most language documentations, including C#, C++ and Go, call this type inference. Most researchers call any mechanism where a language infers the type, type inference, even the mechanism that allows you to call generic without specifying the type of the instantiation, as in this paper : https://www.researchgate.net/profile/Erik_Meijer/publication...
I have absolutely never encountered any definition of type inference which draws this line, and for good reasons, because it doesn't make any sense.
But I disagree with the rest of your post. From the point of view of type inference, what matters is the nature of the type constraints that the type checking algorithm generates:
(0) Traditional type checking: All type constraints are of the form “T1 = T2”, where both “T1” and “T2” are closed type expressions. There is nothing to infer.
(1) Type propagation: All type constraints are of the form “X = T”, where “X” is a type variable and “T” is a closed type expression. Again, there is nothing to infer, but we might need to propagate “T” between places. Say, from the RHS to the LHS of a variable initialization.
(2) Type inference: Type constraints are arbitrary type expressions, which must be solved using a unification algorithm.
As for why type propagation doesn't count as type inference: http://lambda-the-ultimate.org/node/4771#comment-75771
And it also doesn't have tuples: you can't store tuples in variables or larger data structures, pass them as function arguments, etc.
Go doesn't have local type inference
For loops on ranges are limited to Go built-in types, you can't create your own iterators.
So yes, almost none of these features are available in Go.
Come on, now. D is 15 years old, C# 16.
Python is more than 25 years old.
There's also nothing Pythonic about lambdas. Come on, lisp is ancient and it has lambdas.
This article essentially says C++ got stuff that has been around for decades, and Python has stuff that vaguely resembles them, so C++ is learning from Python.
No type inference: http://ideone.com/fmm92M, http://ideone.com/HzyM2E
Type inference: http://ideone.com/0Gv8fV, http://ideone.com/r9nHUF
There are certainly more or less advanced forms of it, but solving systems of type equations is not the definition of type inference.
But what you're claiming is the equivalent of having a “number inference” engine that can conclude that “x = 8” from “x = 4 * 2” - that's not “inference”, it's just evaluating a single expression. Actually, what Go has is even less than that, because Go's type checker doesn't need to reduce anything.
No mainstream (imperative) programming language has type inference in this sense (for good reasons). That's why the term is usually used with a different meaning when talking about mainstream programming languages.
Auto does not add anything new at all on top of the existing type checking: if for a specific LHS type a checker would normalise both left and right hand types and check for assignability, in case of auto it will assume LHS=RHS without checking anything.
Therefore auto is a subset of type propagation, not type inference.
auto y = f();
Where is the type of x specified explicitly in the local context?
What is that other than inferring the type of x from the type of 7?
Edit: You don't need to convince me that this is extremely primitive type inference. I'm not defending the quality of C++ or Go type inference at all.
Although I think this kind of twisting the common term definitions is totally pointless. "Type inference" got a very well defined meaning, which got nothing to do with any kind of a type propagation - the latter term also existing for a reason, to designate a certain sort of type systems, fundamentally different from the inference-based ones.
After digging a little deeper into the history of this terminology I have to concede that you and catnaroek are right. There was from the beginning in the 1950s a distinction that I didn't know about. So I was wrong.
Thanks to you both for enlightening me.
You said two things (A, B); I pointed out that they seem incompatible; you doubled down on one of them (A).
Did you intend to give up the other (B), or to refute the notion that they are incompatible?
For clarity, A is "what they are doing is not type inference" and B is "what they are doing is a special case of type inference". Either of these positions seems reasonable to me, but as I noted they seem to conflict.
#include<vector>
float make(float) { return 0; }
int make(std::vector<int>) { return 0; }
int main(int, char **) {
auto x = {1, 2, 3};
auto y = make(x);
}
You've also got return type deduction and overloading, template <typename T>
auto id(T val) { return val; }
int main(int, char **) {
auto x = 7;
auto y = id(x);
}
and even stupid template tricks template <typename T, typename U,
typename = std::enable_if_t<std::is_same<T, U>::value>>
auto add(T lhs, U rhs) { return lhs + rhs; }
template <typename T, typename U,
typename = std::enable_if_t<!std::is_same<T, U>::value>>
float add(T lhs, U rhs) { return lhs + rhs; }
int main(int, char **) {
auto x = 7;
auto y = add(x, x);
}
Whilst all the type inference is still one-directional and falls out template expansion, it's still legitimate inference.One tiny thing that would be cool to see built into C++ would be an equivalent to the Python range function. The boost version is nice for now, though.
Finally, I also see this as a nice compliment to Python and the power that it offers as a language.
I'm curious, what motivated your switch from embedded to web?
Language features are nice to have, but I never really missed any specific feature. Almost every sort of syntactic simplification I wanted to achieve, I could do so with a function, class or a template.
We can't say that (yet) about Swift and Rust. In 3-5 years I think this is going to be a very different conversation, though.
In my mind, C++'s flexibility is both its greatest benefit and its greatest danger. You can do almost anything, and there are so many ways to do it. I agree this is a "problem" that's likely not going to be fixed. You can ask your fellow developers to read Scott Meyer's Effective (Modern) C++, you can go to meetups, listen to the wisdom of the steering committee, etc. but at the end of the day it really boils down to the fact that your team needs to be committed to being resilient and responsible. That's true in any language, but much more so in C++.
Not sure about Swift, I haven't really been paying attention in that space.
Even then, in C++ you still need to expose a C interface, so it feels at least on even footing with Rust. But of course, I am biased...
Writing proper code in C++ requires, as you say, "your team needs to be committed to being resilient and responsible", which just doesn't happen on my little piece of the world.
So using it in personal projects, yeah. At work, not really.
These are symptoms of language issues.
https://blogs.msdn.microsoft.com/vcblog/2015/12/03/c-modules...
Regarding templates, the approach is similar to the deprecated export keyword, so partially compiled templates and inline functions are stored in the metadata database used by the compiler for modules.
Bjarne was of course aware of modules, being brought on Simula and other programmer friendly languages, but then he had to make C++ code fit into UNIX linkers that only knew about AT&T Assembly and C binary formats.
So no modules there, and along the years you got a culture of developers, specially those that only learn on the job, that never learned about modules and its history.
You see it happen also when cache friendly code and RAII get discussed.
Those things were also possible outside the C family of languages, before they won the market. So most millennials think that they are some special language capabilities only possible in C and C++.
To be honest, I can't talk intelligently about cache-friendliness at all. I know that there exist models of computation (say, for complexity analysis purposes) that explicitly take memory hierarchies into account, but I've never seen them actually used on anything but the simplest data structures and algorithms.
With respect to RAII, I think the main benefit isn't cache-friendliness, but rather deterministic destruction - you can't delay relinquishing a scarce resource that someone else might want to use.
> So most millennials think that they are some special language capabilities only possible in C and C++.
I'm a millennial, and have to admit with shame that I grew up thinking C++ is the best thing ever. But it's possible to recover from it. :-)
Take a look at a typical performance-oriented GPU coding, cache is the major analysis parameter there. Data structures are mostly 2D and 1D arrays, but they may be scrambled in a very complex way, and the algorithms are arbitrarily complex.
I never said it was related to it, I said it was the other feature many think it is C++ specific, or introduced via C++.
Ada also has RAII via controlled types and Object Pascal also had destructors when it was introduced.
Functional programming languages that allow for rich macros, support trailing lambdas or currying also allow for RAII like implementations using those features.
For example, with-open-file in Common Lisp.
C++ doesn't seem to have a guiding philosophy besides "be a Swiss army knife backwards compatible with C." That's OK but I would argue it prevents C++ from every being "Pythonic."
There is the one that appreciates C++'s abstraction capabilities with ability to go low level when needed. Cares about writing safe code and sees C copy-paste compatibility as a compromise required to get C++ adopted by the industry. Those developers usually appreciate languages like Ada, Modula-3 and Haskell, and would rather kill the pre-processor.
Then there is the culture that somehow had to move from a C compiler into a C++ one, constrain themselves to the C subset of C++ while ignoring the standard library because bloat and write exploit sensitive code as always did in C.
Many on the second culture tend to do micro-benchmark driven development. Each code line is questioned how much fast and memory it takes by gut feeling, without any regard from profiler tools or actual needs from the application users.
This got me thinking, what would one call an idiomatic C++ style (which I suppose is part of what the core guidelines are trying to push)? Cppthonic? Bjarnic? Python is 'Pythonic,' I usually hear idiomatic Ruby described as the 'Ruby Way.' Is idiomatic Rust 'Rustic'?
Weed pun intented :)
The (implied) corollary is that compile time cost, mental cost and social cost of language features do not matter.
As long as there is at least one person on the planet who really understands the ins and outs of a language feature, that is sufficient proof of the language being simple enough. There is no need for anyone to understand the entire language.
I don't know how much of that is due to C++'s historical baggage and how much is due to a greater concern for usability and elegance on the Rust side, with consequent discretion when adding new features. But I don't think it's entirely the former.
So you end up paying for things you didn't intend to use, but used anyways because of some small detail.
Sometimes I feel like the only way to write good C++ is to keep looking at the disassembler output...
C++ is my dayjob.
auto triple = std::make_tuple(5, 6, 7);
std::cout << std::get<0>(triple);
Ugh. And don't even get me started on "myList.push_back(5)" . "push" usually means "add to the front"; why not use "append" ?std::get<0>(triple)
It really doesn't look great. It looks like the template syntax using the <>. I'm sure I would get used to it if I were using it every day but from an aesthetic sense it doesn't win me over.
Here is me translating as literally as I could a bit of nontrivial code (which I originally learned from R's C code):
http://inversethought.com/hg/medcouple/file/default/jmedcoup...
http://inversethought.com/hg/medcouple/file/default/medcoupl...
I'm really happy how almost every Python statement has a nearly equivalent C++ statement. The only one I had trouble with was list comprehensions, but they can be very nearly translated with C++ stdlib algorithms and a few lambdas (kind of looks more like map and filter than a list comprehension, though).
The best part is that the C++ code runs as fast and sometimes faster than the original C code I grabbed this from! (The translation path was C -> Python -> C++.)
However, what idea has C++ not accepted? It seems for it to be a "new" language, it should at least say "no" to something.
auto x = new int[64];
harder to read than int* x = malloc(sizeof(int)*64);
?If so, I think it's just a matter of habituation and imprinting. Whatever you learned first is easier and everything else is hard.
((void(*)())exec)();
or template <typename T>
struct value_type {
typedef typename T::value_type type;
};
template <typename T>
struct value_type<T*> {
typedef T type;
};
Six of one, half a dozen of the other.Drop the semi-colons, convert "dict of int to string' to map<int, std:string> etc.
It would be ok if it only did a subset of C++ at first.
It also doesn't have Concepts yet, as they got pushed back again.
Built-in to the language would be nice, but C++ is always extremely spartan with its standard library functionality. Thus:
https://gitlab.com/higan/higan/blob/master/nall/range.hpp
37 lines (without the boilerplate header stuff) and you have:
for(int x : range(20)) ...; //iterate from 0 ... 19
for(int y : rrange(myvector)) ...; //iterate from myvector.size()-1 ... 0
Also supports xrange's offset/stride arguments (it's pretty much exactly Python's xrange object in C++.) And you can add support to do ranges over any class anywhere, eg range(myvector) will pull the length from myvector.size() for you with a simple overloaded function.Timing tests in extremely critical loops (like convolution kernels eating 100% CPU usage) shows no discernable performance impact at -O2 and above over the traditional C++-style for(int x = 0; x < size; x++) style.
Windows APIs have always supported using forward slashes, using "C:\\path\\to\\file" is just an ugly mess.
I find true type inference to be very helpful for readability. For example, you can write "let mut x = None;" where writing a type would be just noise.
While productivity can be more or less objectively measured in the long run, the effects of a language feature on productivity vary from one programmer to another. There's no universal basis for evaluating this.
> Maybe you have a different goal
My primary goal is correctness. I don't really believe in 80% solutions. Having an incorrect program is just as good as having no program.
> have calculated the expected value differently
Very differently. My calculation is based on the following objective, unquestionable, technical facts:
(0) Determining whether the internal structure of a program is consistent (in the formal logic sense) involves lots of long, tedious and mostly unenlightening calculations.
(1) Computers are very good at performing long calculations. On the other hand, humans suck at it.
(2) Type structure, at least in sensibly designed languages, is the most efficient way known so far to communicate the intended logical structure of a program to a computer.
With type inference:
(0) The programmer only has to provide just enough information for the type checker to reconstruct the logical structure of the program.
(1) If the pieces fit together, the type checker will tell you exactly how.
(2) If the pieces don't fit together, the type checker will tell you exactly why.
Yes, yes, the formal definition of type inference as supported by languages like Haskell is not fully supported by C++, or Go, or C#, etc. But that's not the definition of it that anyone else in the comments here seem to be using, so can we drop it?
You seem to be knowledgeable about type theory. Is there a formalized term for the subset of type inference that C++11 supports? If so, can you just assume that others are using that term? Not that the discussion isn't interesting, but you're coming off as needlessly contrarian.
"if (a)", "for (...)" -> "if a", "for ..."
"if ((a > b) && (c > d))" -> "if a > b and c > d" (yes, I know C++ supports "and" if you wish)
Or how about accepting a new-line as a ; at the end of a statement (ie: optional ; at line end)
I also love nested tuple unpacking: "for i, (key, value) in enumerate(dict.items())"
And have to use backslashes or something for multiline statements? Ugh.