Models of Programming
lambdaland.org
lambdaland.org
You may not learn "programming" this way, but all of these are going to be useful skills when you work as a programmer.
> Since getting the syntax and the ceremony right is so much of C++, it turns into a guess-and-check game, where the student keeps tweaking things until it works. This is not the way. You don’t learn anything about why things are the way they are. This is similar to one argument I’ve heard about how Java suffers from a similar problem. We shouldn’t be teaching students how to solve programs in a given language. Rather, we should be giving them tools to think about the problems they face and how to solve them.
And here's the part where you learn to deal with office politics.
I guess my point is that I'm a bit skeptical about "good learning techniques" when there isn't any data to back it up and just the reasoning of the author. All of this "feels" true to me, in a way that "let the students figure out C++" doesn't, but I don't know if this theory is true.
It seems my life as a programmer is fighting with tools, fixing bugs in legacy code in a myriad of languages (including C++), reviewing code, resolving merge conflicts, configuring CI/CD, writing tests, benchmarking, monitoring... And it's not like we can choose the programming languages we want anyway.
Learning different paradigms and languages is all good, but it's really a tiny part of our job.
I am not a functional programmer. I'm also not an imperative programmer. I'm more of a "craftsman," and I use a lot of tools (including the dreaded OO) to write shipping software that ends up in the ungentle hands of The Great Unwashed. I've been doing that my entire adult life, and I'm 59, so that's been a minute. I've outlasted most of the tools and fads that have come and gone, during my time, and I've learned not to let the tools dictate the craft.
It's a bit discouraging to see how some people look down on folks like me, but I really like getting my hands dirty, and making stuff that people use.
It’s basic human nature. We always reinforce that the way we do things (in this case, FP), is The One, True Way, and all who do otherwise are to be pitied/scorned.
I’m a real slob. I mix my peanut butter and chocolate all the time. I may use 35-year-old techniques, and mix them with just-published-brand-spanking-new stuff.
I get stuff done. It may be a sausage-and-laws thing, but the end results seem to make people fairly happy.
A great deal of the time, I’m constrained by the context of my work. For example, I write software for Apple devices, and am still one of the poor, unenlightened that writes using UIKit. I haven’t written my first shipping product with SwiftUI.
If we write in UIKit, we can use different patterns, but UIKit was designed to be implemented, using classic MVC, and lots of OO. Attempting to circumvent these design considerations is a “kludge,” at best. If we want to write good, performant, maintainable software for Apple devices, we need to hold our noses, and use the tool the way it was designed.
Apple is refining SwiftUI, which I’m really looking forward to learning and shipping, but I am not there, yet, personally.
I assume that SwiftUI will also end up using a pattern that will become “obsolete,” even while SwiftUI is still the dominant development tool, much as is the case with UIKit.
Also, SV (or it could just be this community, specifically) seems to be focused on “going big.” I think there was a post, a day or two ago, entitled something like “I don't know how to count that low,” or whatnot.
The work I do is “small.” It may actually be fairly intense, “under the hood,” but I write stuff for small demographics, and spend a great deal of time, “polishing the fenders.” This is doing things like ensuring a smooth and intuitive UI workflow, presenting data in as simple and attractive a manner as possible, affording localization and accessibility, etc.
These are all about the individual end-user. They are not “big picture” things, and I have limited tools and venues at my disposal. I need to be practical, and make compromises and concessions to achieve my goals.
I have nothing but awe for some of the technology and technologists out there. It’s a great time to be alive. Cool stuff is happening.
It’s just that a lot of it doesn’t really reach me, down in the trenches, and it’s discouraging to see the mindset that what folks like me do, “doesn’t matter.”
Just curious... have you played with functional languages before? It may feel like snobbery to you, but once you start thinking functionaly, it really does feel like you're painting a house with rollers and a spray gun rather than a wet noodle.
Seriously. We all have our specialties. It is, indeed, "snobbery," when we rubbish the experience and expertise of folks that choose different paths. I won't rubbish some of the truly marvelous stuff I see, coming from people that think what I do is "quaint." It would be nice if the favor of respect were returned.
Delivering ship software is quite valuable; especially at the Quality and Usability bar that I set for myself. I suspect that you may not be aware of the Discipline, creativity, patience, and just plain old-fashioned Hard Work that goes into moving a piece of software out of the "toy" arena, and into the "ship" arena. The difference in project lifecycle times is quite sobering.
I can write a fairly full-featured app, in a couple of days, but it takes months to make one that I consider "ship-worthy," and I'm really at the top of my game, in my sandbox.
I have been delivering ship software for my entire adult life. I know that it isn't world-changing, prize-winning, theoretical marvels, but it has had direct and meaningful impact on thousands of lives.
I really appreciate it when the work of theorists makes it to my level. The Swift language is an object example. I really like it, and I know that one of the big reasons is because they cribbed from the FP workbook.
But what I'm trying to say is that this isn't about quantity shipped or how many users you have made happy - what I'm trying to convey is a measure of programming artistry and helping you see and maybe appreciate it.
Maybe a better analogy - I understand you like Rock music and that it's the only thing you play in your car, you grew up with it and it's all your friends ever talk about... but maybe if you looked at Jazz, you may be able to enjoy it as well.
... sorry if it came out as condescending, that wasn't my intention.
I also have problems with “purism,” when it comes to tools and techniques. Purism, by definition, eschews orthogonal viewpoints, and I feel that’s quite destructive. I also feel that it is anti-intellectual. One thing I’ve learned, over the years, is that I was definitely right, yesterday, but I’m definitely wrong, today. Happens regularly, and that can get tiresome (but it’s unavoidable -part of the price I pay, for a career in tech).
I really like the ideas behind FP. It’s just that it is not a practical tool, for me to use in anything close to pure form, in the drive towards realizing my goals. I do not have the luxury of purism. I’ve always had to play in someone else’s sandbox, and by their rules. Believe it or not, that can be quite exhilarating.
A house framer uses drills, saws, and nailguns. They can’t make these tools, but they use them, to do what the people that designed those tools, cannot do.
I do agree that C++ is not a good language for beginners. I think a language similar to Python but with static type checking will be a good one.
You say "clean all those dishes", not "let x=0, then while x<N, clean the x-th dish from the top, then let x = x+1"
Ask someone without a scientific background to solve (a * b) + (c * d) with a (physical, non-software) calculator. The "correct" answer is to do something like: a * b, M+, c * d +, MR. What they will do is: solve a+b, write down the result, then do the rest. They don't think of a calculator as something stateful.
But if we are using calculators, then depending on the make and model, you might do a b * c d * +, Which achieves the goal your second paragraph is aiming for: getting rid of (or at least abstracting away) any explicit use of incidental or intermediate state (which takes the form of memory operations in conventional calculators.)
RPN calculators (and programming languages), however, never became popular, and I strongly suspect that is because their expressions are often objectively harder for humans to comprehend, no matter how elegant RPN seems.
I agree on RPN, that was the point I was trying to make. RPN is more powerful, but is stateful and uses a rather complicated and unintuitive form of state (a stack). In my experience it's not true that people find it more intuitive to work with arbitrary mutable state of the kind imperative programming languages traditionally have.
Also, machines deal with data - which is encoded information about the world.
So when we say that a thing 'changes' in the world, what we're actually saying is that at time T, something happens to the world and from then on, the data we read back is different.
'Something happened' is new information which is added on top of the state of the world at time T-1.
Eg. the user e-mail does not 'change' to be come X, but rather, it used to be Y, then the user changed it to X on 1st of May.
So we can generalize that the world is a function `f` of some state combined with extra data of what happened at time T, giving us the state at T.
Apparently it is not, because otherwise it wouldn't have taken so long for the concept to spread, and folks like the romans would have used 0 from the beginning.
But just because something is not natural does not mean it is not more productive than the alternative solutions, given that one spends the time to learn it.
So, maybe mutating state is natural - but I would object the claim that this means it is more productive.
Monad is like highly effective and complicate patch to handle thing who couldn't be pure function. i don't like Haskell because of that is not natural force that much the langues for me(opinion), but is like try to tech Rust as first langues you don't have the grasp in the limitations to understand why ownership matters, and why all the langues need to be kind a weird, is still weird but you understand the advantage and grasp at least the syntax and basic patters make it more pleasure to learn it.
Haskell is hard for programmers, because it forces them to truly think. Haskell grows on you when you get to know how it evaluates and forces you to separate concerns structurally. For that reason, it's said to be a language that teaches you how to program better.
C was already taken out of curriculum (students were expected to learn it from the C++ learnings), and we made use of our own collection classes with bounds checking.
The secret was teaching the language on its own, and not as C with extras.
> "My hope is that programming courses in higher-education settings (and high school settings!) will move away from imperative and even object-oriented programming towards a more functional approach."
It reminds me of past debates over how to teach mathematics - maybe start with set logic and group theory instead of addition, subtraction, multiplication, division? Clearly set logic and group theory underlies those subjects so the students should start there...
It's a bit over the top. Should welders have to be applied physicists? Some knowledge of arc plasma might be helpful, but at the introductory level, certainly not. If you want to devise a new method for welding titanium or something, well maybe you need that level of knowledge.
I also tend to turn off when people talk about how much they love or hate this or that language. Languages are just tools, and selecting the right tool for the job is what experience teaches you - although a new tool might be better for a certain task, so experience alone is not enough. Also, I started programming learning C++, it worked out well enough, even if the first six months were a struggle. From there you can easily look at assembly or look at languages without direct memory access via pointers, and you've at least got a grasp of why memory management is important. I feel lucky for having started this way, vs. starting with a higher-level abstraction language like Python, JS, etc. I can't even imagine starting with a pure functional language like Haskell, that would have been pretty hopeless for a beginner without any experience with recursion.
As far as object-oriented vs. functional, hybrid approaches seem useful, for example classes with methods that avoid side effects, etc.
I personally tend to err on the right tool for the job, or just avoiding programming when not strictly necessary.
>One way of thinking about programming is that you are ordering a computer to do your bidding: you, the programmer, sit at the helm of your CPU, afloat on a sea of data, and you have various levers and knobs that you can pull and twist to make the CPU get from point A to point B: load this value into memory slot i. Now add five to it. Now print that back out. Etc. This is called imperative programming, because you tell the computer every step it should take.
I can tell the computer complex, high-level things too, not just this pseudo assembly.
You use wire instead of radio wave to design your world.
Sometimes it is much simpler to solve a problem by passing by reference (a var parameter in Pascal) than using the default of passing by value. Making the choice explicit helps those reading the code see this.
As for "pure" languages? Yikes, no language is perfect nor superior to all others, not even Rust.
When they say "pure", they mean functional (no side effects, no mutations etc). They aren't meaning "pure" as in some kind of judgement...
i.e. a "pure function" is a function that always returns the same output for the same input and has no side effects (it's essentially just a map from input->output).
https://en.wikipedia.org/wiki/List_of_programming_languages_...
Sometimes, but very rarely. I got a long way in Swift before I needed to use inout parameters, or even realised the language had them at all!
The article is specifically talking about teaching beginners; call-by-reference is a risky technique of limited value, and therefore it’s best to delay introducing it. If you introduce it early, students will get into the habit of using it too often, mostly in the wrong places.
For something like Java or Python, though, it makes sense that the argument is implicitly a reference to the object it takes (at least as a default). 99% of the times you don't want to pass around stack-allocated stuff.
I wonder if we’d have fewer memory-related bugs now if the industry had standardised on Pascal strings (length prefix) rather than C strings (null-terminated)?
And from that pov there indeed are "perfect" languages as much as they can be "superior" to all others. Lambda calculus, for example, is perfect in the sense that it is the smallest, yet Turing-complete, pure language. You might of course start to argue about the evaluation strategy, but that's quickly done because there's only so much you can change ;).
But that's the academic point of view, of course. In that view, every language needs to be defined completely on a few sheets of paper. If you can't do that, or have to use handwaving, you don't have a language, you have a piece of software that approximates a language.
The right way to specify parameters appeared in the DoD IRONMAN specifications, which were first implemented by the Ada language.
There are 3 kinds of parameters, in, out and in-out, and this is what the programmer must specify.
Any of them may be passed by either value or reference, this is transparent for the programmer. E.g. when an in-out parameter is passed by value, it is copied to the parameter location (which might be a CPU register or registers, or a stack location) before calling the function and then it is copied back after return. For "in" by value there is only 1 copying, before function call, and for "out" by value there is only 1 copying, after function return. (In many cases the copyings can in fact be no-ops, e.g. for register variables.)
If implemented correctly, there is no observable difference depending on how the arguments are passed, except in timing.
The decision to pass by value or by reference must be taken by the compiler, because it depends strongly on the target CPU, i.e. on the relative costs of copying and indirect addressing. Even for 2 Intel/AMD CPUs from different generations the optimal decision can be different (i.e. the length threshold for the size of a structure/class to be passed by value or by reference can be different).
For external functions, how to pass the parameters should be determined by the ABI for that processor family, also taking into account the typical relative costs of copying and indirect addressing.
There might be some very seldom cases when the programmer could increase the speed by choosing how to pass the parameters instead of allowing the compiler or ABI to decide, but if indeed such a decision could have a measurable effect on performance, it is likely that inlining the function or rewriting that function in assembly language would have an even larger effect on performance.
Even in C/C++, the apparent control of the programmer on how the parameters are passed is just an illusion, because the performance is mainly determined by the ABI, which decides, depending on parameter size, whether the parameters specified as passed by value shall be passed on the stack or in registers and whether the return value shall be passed by value or by reference.
Therefore if a parameter is considered by the compiler as too large, specifying that it should be passed by value will have no effect on performance, because it will still be passed as a stack location and addressed indirectly.
The lack of 3-way parameter specification has caused a lot of unnecessary complications in C++, like the distinction between constructors and any other functions (which is needed because in C/C++ output parameters are assumed to be in-out, they cannot be specified as out) and the need for both move & copy constructors/assignments and also the many other tricks that are frequently needed to achieve move semantics.
Also pass-by-reference vs in-out has very visible semantic differences on the face of aliasing, exceptions and abnormal termination (when referring to shared memory).
Whether a parameter is out or in-out is very important for types with destructors.
Before calling a function, an "out" parameter is raw memory, on which a destructor should never be called. After the function returns, the "out" parameter contains a new object, possibly with allocated resources.
Before calling a function, an "in-out" parameter contains an existing object. If inside the function a new object is assigned to the parameter, the destructor must be invoked first, usually implicitly, inside an "operator=".
Before C++ 2011, most C++ compilers treated the return value as an in-out parameter, like every other non-const parameter, so they allocated a temporary and constructed a default object in the temporary, before calling the function.
One of the major changes in C++ 2011 was a collection of extra syntax features and library tricks to achieve the so-called move semantics, i.e. to avoid unnecessary constructions and destructions of temporaries, all of which had their origin in the lack of distinction between out and in-out parameters.
Aliasing of "out" or "in-out" function parameters is something that does not have defined behavior in any programming language.
When 2 aliased in-out parameters are passed by reference, the value after the function returns is unpredictable, because it depends on the order of interleaving of the changes to the 2 parameters inside the function.
When 2 aliased in-out parameters are passed by value, the value after the function returns is unpredictable, because the function receives 2 copies, whose values are correct immediately before return, but the final value seen after return depends on the order in which the 2 values are copied over the original aliased location, which is undefined.
Similar effects exist for exceptions and abnormal termination, the details may differ between pass-by-value and pass-by-reference, but the result is always undefined, so it does not matter.
While aliasing of "out" and "in-out" parameters cannot have defined behavior in any language, regardless of the method used for parameter passing, aliasing of "in" parameters is always OK, and here again it does not matter whether they are passed by value (when multiple copies are passed) or by reference (when multiple references to a single immutable object are passed), the behavior is indistinguishable in this case.
So you are saying that assignment through out parameters will act as placement new, by constructing a new object? That's interesting, although it would be a big change from the existing language semantics (assignment is very different from initialization at the moment) so it would be very hard to retrofit.
Does that means one wouldn't be able to pass an existing object as an out parameter and would need to pass raw memory? Or the caller is responsible of destroying the object first? That has implications for exception safety.
If objects with constructors and destructors had been introduced in a language with distinct "in", "out" and "in-out" parameters, everything could have done in a much simpler way than in C++ and without needing to introduce a lot of weird syntax and non-intuitive mechanisms.
In a language with distinct "out" parameters, yes the compiler ensures that the "out" parameters are raw memory, i.e. registers or free stack locations, or if the space previously occupied by temporaries is reused, the temporaries are destroyed before invoking the function.
Inside the function, the compiler ensures that any "out" parameters must be initialized somehow, e.g. by a "new", before any kind of access.
In a language with "out" parameters, assignment would be defined as having an "out" left parameter, so there would exist no copy constructor or move constructor and "operator =" would be what in C++ is named now as copy constructor.
The compiler would take care to destroy any left-hand operands of assignment before the assignment, when necessary.
The same for out parameters, if they have been declared but not initialized, they are raw memory so they are passed as they are. If they have already been initialized, the compiler ensures that they are implicitly destroyed.
All these rules are obvious enough, and they are much simpler than the very complex rules that you must take into account to understand what a C++ compiler does with temporaries and what happens differently when you add either "&" or "&&" to the type, and so on.
Some people believe that having to write 4 member functions, copy constructor, move constructor, copy assignment and move assignment, as it is mandatory in C++, is an opportunity to sometimes make a performance optimization, but I do not agree.
I believe that the case when a worthwhile optimization can be made in a copy assignment function (e.g. by reusing some allocated memory, which slightly bypasses the same reusing that would have been done in the memory allocator otherwise) are far to rare to justify the waste of time with extra function definitions for every class.
Initially C++ did not have a copy constructor, but only an "operator=", which mistakenly assumed a pre-existing object as its left-hand side.
Initializations by copy were done by the default constructor followed by "operator=".
This was inefficient, so the copy constructor was added, but the right solution would have been to redefine the semantics of "operator=", to match that what is now called copy constructor.
Then initializations would have been done with "operator=" and for assignments the compiler would have invoked implicitly the destructor. The programmer would have always written only 1 "operator=" function (and a simpler function, corresponding to the current copy constructor) and there never would have been any need for the other 3 functions.
This would have almost always resulted in the same performance as now, but with a far less burden on the programmer.
We had so nice alternatives like Eiffel, Oberon descendents and Modula-2 descendents with some form of GC, value types, and reference parameters, and thanks to it, we had a tour of 20 years, where the .NET languages were the managed exception to that.
Finally this low level control is becoming mainstream, while Java tries to fix those design decisions.
Better later than never, I guess.
I think direction is orthogonal to this.
... but i would like to be explicit in telling a function to not copy 2Gb of data on each call when a reference will do.
.. or, in C#, (no specifier), "out", and "ref". However there's still a difference between plain types (copied) and objects (mutable).