Programming Paradigms for Dummies: What Every Programmer Should Know (2009) [pdf]
info.ucl.ac.be
info.ucl.ac.be
A model is a set of concepts. A concept is an orthogonal language feature, like closures, concurrency, explicit state (which he now calls named state), exceptions, etc.
His approach is not so much that you should select one language that supports a paradigm that seems the most suitable for a given project. Rather, what he advocates is that you should use a language that supports multiple cleanly separated concepts (close to what is called multiparadigm), and then you select the simplest set of concepts for each program component.
This is the principle of least expresiveness, you should choose the simplest model (simple meaning that it is easy to reason about and to get right) that keeps the code natural (meaning there is little code unrelated to the problem at hand, little plumbing).
Each model has an associated set of programming techniques, like accumulators for FP or transactions for stateful concurrency.
It is possible to mix and match components that are written in different models by using impedance matching, which consists of creating an abstraction in the more expressive model that wraps the other component. An example would be a serializer, which allows you to plug a non concurrent component in concurrent program.
Basically, you should use FP as much as you can, but you can't if your program needs, say, visible non-determinism, like a server in a client-server application. But then you can add just one concept, ports, to the declarative concurrent model, and you have a new model that allows a whole constellation of new programming techniques, the concurrent message passing model, Erlang-like. The non-determinism in this model is restricted to the ports, the only place where it's required, the rest of the program can still be functional.
Other situations where FP might get stretched are when modularity or performance are a priority.
This book helped me realize the whole FP vs OO debate is sterile, the ideal is to have a language that allows you to use FP in a natural way, and switch to, say, OO, in a natural way in some cases.
More info here: https://www.info.ucl.ac.be/~pvr/book.html
Seems like a very interesting read!
I have accused some academics of "promoting ideas that require more education" so as to line their wallet. It didn't go over well and I got counter-accused of "promoting mediocrity" so that "my type" don't have to learn. (I don't believe such bias is intentional, just human nature. We are all biased in ways we don't know just by the fact we only live one life.)
Rather than delve back into that bitter debate, I ask that people consider the economics of it: is it better on a macro-economic scale to spend extra education to master many paradigms/techniques, or to settle on a few to get through school/training faster? (There's always going to be niches that need specialized training/skills.)
The average programming career is relatively short-lived: you either have to move into management, analysis, project planning, etc. or be subject to agism. For good or bad, the industry doesn't "like" old programmers. RSI (wrist problems) is also common with seasoned programmers. Thus, I believe the shorter-education approach is the economically logical one. You are welcome to disagree.
Some orgs might be, but they are better off hiding their secret from competitors, and thus we are not likely to hear about it.
Warren Buffett often expresses shock that universities and investment organizations ignore his well-published techniques, following BS and fads instead. He and other "value investors" have big bucks lasting decades to prove it; they don't. They only have silver words and catchy-sounding theories. IT is the same, I'm afraid to say.
The GREAT LIE of IT is that there's not a lot of real science in "computer science", beyond machine efficiency. It's much easier to do science and math to test and measure machines than it is the human mind, but code is meant for the human mind as much as machines. Humans make, read, and modify software, not machines: computers are just dumb automatons (hopefully) following instructions verbatim.
So, is the increased expressiveness worth the decreased predictability? For one developer, it probably is worth it. For a large team distributed over space and time, it probably isn't, even if everyone is comfortable with each individual paradigm.
I won't dispute the problems in the industry, but I'm not sure you can really say the career is relatively short-lived. I've been at it for 20 years, and while there are far fewer of my age-peers than there are of younger coders, the number is also far from zero. The "requirement" that you move to a related field doesn't seem nearly as certain as when I was younger (I've avoided all efforts at moving away from coding itself with only minimal effort on my part), and just landed a new job pretty effortlessly.
In terms of the topic, I agree that newer coders should tackle a bunch of problems in a few different approaches rather than trying to master them all from the start (I certainly haven't, even as an old fogey), but in terms of troubles I've found lack of free time/energy to do so to be the bigger obstacle rather than no longer being a programmer. Then again, the problem I see most among younger coders is not trying to master everything and failing, it's treating everything as a nail for their single hammer.
My anecdotal experience is not data, but it is all I have right now: Do you have a stronger source of information that the "average programming career is relatively short-lived"? I've been at this for 20ish years and currently expect another 20 more, but if that's foolish I'd like to know for (reasonably) certain.
https://www.businessinsider.com/silicon-valley-age-programme...
The data has lots of issues, but I'm forced to conclude that....yeah, agism likely reduces the average length of careers in coding.
Working in mostly Java shops the last 10 years has shown me that most programmers only know how to add complexity, and not restrict themselves to any strict subset of features. Spring is a good example of this - statically defined, decoupled configuration has morphed into everything configured with the full expressiveness of the host language.
I feel like a curmudgeon when no one seems to understand my preference for well defined boundaries.
A 2004 textbook by the OP:
Concepts, Techniques, and Models of Computer Programming
https://www.amazon.com/Concepts-Techniques-Models-Computer-P...
And his 6 week edX course:
Paradigms of Computer Programming – Fundamentals
https://www.edx.org/course/paradigms-of-computer-programming...
Who did that, exactly?
Plus, the failure of Plan 9 was more an early 90s thing.
It's also extremely well-suited for very large teams and much harder to mess up projects in Ada than in many other languages.
Unless a survivor of the project can chime in, I'm going indulge in a little idle speculation and agree that defense sounds like a good candidate for losing a billion dollars. Maybe not DoD itself, but a big contractor?
"• Violating the substitution principle. A procedure that worked with objects of a class no longer worked with objects of a subclass. As a result, many almost-identical procedures needed to be written.
"• Using subclasses to mask bugs. Instead of correcting bugs, subclasses were created to mask bugs, i.e., to test for and handle those cases where the bugs occurred. As a result, the class hierarchy was very deep, complicated, slow, and filled with bugs."
That's a good question. I can think of several candidates, but none that match the specific problems.
Microsoft (Cairo) seems to fit the bill almost exactly, but it was only initiated in 1991.
There is a right time and place to use OO (and inheritance) and wrong places and times to use it, and it takes training AND experience to know the difference. Further, a lot of it depends on the language; some languages have poor OO models, forcing one to use lambda's etc. instead. In my opinion, a better OO language reduces the need to use lambdas. I know this is a controversial statement, but I stand by it.
The AXE-N venture was to be the most expensive industrial project in Sweden after Saab’s JAS fighter. One calculation estimates that it cost Ericsson SEK 10 billion. The project has often been described as a total failure.
https://www.ericsson.com/en/about-us/history/changing-the-wo...
Swedish wikipedia has more information: https://sv.wikipedia.org/wiki/AXE-N
Some people here might know Ellemtel from their C++ style guide, which was a byproduct of the AXE-N project. In Emacs "ellemtel" is one of the built in choices for CC Mode style.
Java, ok. But C++??? That language has everything and the kitchen sink, including pure functional programming (templates).
Secondly stuff like LINQ was already available in Smalltalk.
So all those map/filter/fold/.... constructs from lambda calculus, which Java now enjoys.
Then if we apply the modern concept of only Haskell is FP, then there are a couple of FP languages that won't meet the classification.
Ah, and Haskell does not require TCO on their language specification, so it isn't an FP language according to your arbitrary definition.
open Printf;;
printf "After all OCaml isn't a FP language\n";
for idx = 1 to 10 do
printf "%d\n" idx
done
Same goes to Common Lisp, F#, Scala, Clojure.Tail recursion of course is great to have, but you can certainly FP without it, even in a language that supports it. I mean, what if you don't put the recursive call in tail position in a function written in a language that supports tail recursion? It would still be FP.
Just create a static class without member fields where the class plays the role of a poor man's ML module, with all static functions only interacting with their parameters.
Then static import it into the client package.
How to do FP in Java is very well explained in the MIT OCW course 6.005 "Elements of Software Construction", 2008 [1], in particular in lectures 10, 11, 13, 14 & 15.
Remember that you can create closures with inner classes.
As for the fact that in Java you could have state inside, say, a Visitor, the program can still be FP, if you know what you are doing. The point is that when doing FP in a language like Java, you are adopting a definitional approach to FP, which is totally legit: any operation is FP if it is deterministic and has no visible side-effects (a corollary is that then it wouldn't have any observable state of its own, it would be reactive). This is sort of a "if it walks like a duck ..." approach to FP.
In fact, this is exactly what is going on when you are doing reactive programming in a non FP language like Javascript, where there are no restrictions to doing destructive assignment anywhere. This doesn't stop you from doing reactive programming, as long as you follow certain guidelines (because the language won't stop you from infringing them and having state).
How is this possible? The reason is that the OO computation model subsumes FP: anything you can express in an FP language, you can express in an OO language, and then some (although not as naturally, there is more plumbing in the way).
The reverse is not true, and this is a good thing, it is exactly what allows FP to have all those desirable properties.
[1] https://ocw.mit.edu/courses/electrical-engineering-and-compu...
Is time and memory usage a side effect btw? :)
There are some recursive structures that are just much more natural than their iterative counterparts. Parsing (as lysium suggests in the sibling post), for example.
But also things like graph and tree traversals, and many search algorithms related to those same structures. If you attempt a tree traversal iteratively, you have to maintain the return stack manually, rather than permitting the language to do it for you (assuming a full traversal and not a search, a search could be done iteratively without much trouble).
TCO isn't about recursion; it applies to any function call in tail position. Some toolchains only manage to avoid over-allocating stack frames for self-recursive tail calls, but that isn't full TCO, and it doesn't help for other interesting case like mutual recursion, state machines, or continuation-passing style. Compilers which compromise on full support for TCO emit programs with built-in memory leaks; they fail to free data which is no longer needed, and thus unnecessarily exhaust their stack allocation.
Programs which use built-in iterative constructs are still recursive; all that "recursion" means is that the control flow folds back on itself. A traditional "while" loop looks like: (1) stop if a condition is false; (2) otherwise do something; (3) do it again. The "it" in (3) is a recursive reference to the loop.
Now, unstructured explicit recursion is barely better than unstructured "goto", so I'm not advocating that everyone start using tail calls in place of loops. However, bare loops are in much the same position with respect to higher-order primitives such as non-strict folds—and those higher-order primitives are much easier to implement in environments which properly support TCO. Languages where TCO is not customary tend to suffer from a proliferation of built-in constructs—iteration, generators, list comprehensions, coroutines, and the like are all implemented as language features requiring custom code generation, where another language where TCO is customary might relegate such primitives to a library.
The "function" in "functional programming" refers to mathematical functions, which are fixed mappings from inputs to results with no side effects. If your "functions" can have side-effects then they're not functions, they're procedures. Programs composed of effectful procedures are imperative, not functional.
“Optimization” makes it sound as if you could turn it off or on for performance reasons. Instead, programs rely on tail call elimination being in place or otherwise they would not work.
Everything else are just variations on "What is FP" and sugar coating.
That's a criterion that's so broad as to be almost meaningless, though. The lambda calculus can be expressed in any Turing-complete language.
I wouldn't personally consider a language to support a paradigm unless programming in that paradigm feels natural in that language. Java supports using a few functional techniques. The experience of trying to write in a truly functional style, though, is painful.
FP Lisp/Scheme, FP ML, FP Haskell/Miranda, FP Idris, FP Scala, FP Kotlin, FP OCaml, FP ATS, FP .... ?
All of them express different views of what Functional Programming is supposed to be like.
Just because only might need a bit more boilerplate for currying or partial applications, doesn't prevent writing FP libraries in modern Java.
Disregarding “comfort”, or rather, how much the language lends itself to a style, makes the notion of programming paradigms meaningless, and we’re still left with something we all know exists, but have no way to express.
For example, I would say that GTK+ is written in an object-oriented style. But, despite that, I would not say that OOP is a member of C's repertoire of language paradigms.
OO languages and functional languages support each other's paradigms (more easily) than a pure imperative language would support either. And both OO and FP languages support declarative styles (like relational or logic languages) better than C would.
EDIT: It's also worth pointing out that C++ really achieved OO (initially) by using macros on top of C. So having a sufficiently expressive meta-language is also important to this. Via such a meta-language, you can achieve many more paradigms in a language than using the language alone.
But many languages lack a meta-language or don't have a standard meta-language which people can rely on.
And this is extremely relevant to the point. If the primary complaint against function programming in Java is "comfort" and "boilerplate"... Macros address both those problems very well. If Java had macros, it would be very simple to isolate and minimize that boilerplate.
Edit: Oz is the language. Sorry.
At runtime? like Scheme?
Perhaps this is how programming language designers ought to vet language ideas: do the proposed changes make certain patterns redundant? Looking at Rust through this lens makes it clear that it aims to eliminate C-style manual memory management.
However, it does make me wonder: How do languages such as JavaScript and Python stand out from their predecessors, and what problems do they uniquely solve?
Erm, no. Some consequences: non-obvious control flow, RAII, constructors, move semantics, exception safety, efficiency, stack unwinding...
A better way to deal with the "problem" of unchecked error codes is to have the compiler check that something happens to the result value. This is what Chandler Carruth suggests.
An even better (but orthogonal) way is to structure the code so it does only one thing at a time, and does any one thing only in one place (ideally).
How exactly do you distinguish "exceptional" from "non-exceptional"?
There's no need to be dismissive. I refer to simple as simple to use, not simple to conceptualise.
I would say exceptional is a situation that is rare and unexpected. When saving a record to a table with a unique constraint, I would expect the constraint to prevent saving but I would not usually attempt to handle/recover the app running out of memory.
So it's situational and I'd say the distinction is between whether you will be explicitly handling the event as part of normal behaviour. The more stable the app needs to be, the more situations need to be handled.
And it's not only the caller, but also the caller of the caller, and so on, who needs to be prepared for exceptions. This all has severe ramifications on the code structure.
> There's no need to be dismissive. I refer to simple as simple to use, not simple to conceptualise.
Sorry. I need to control myself. It's not about you. As another commenter stated there is an agreed upon distinction between simple and easy. The distinction is important especially among programmers, and what you described might be easy but it's not simple.
The GP, along with many other people, are trying to get software professionals to standardize on a definition of simple that means something like "composed of a single element; not compound" or "easy to reason about".
Under such a standard, it makes no sense to talk about "simple to use" vs "simple to understand". You can talk about "easy to use" vs "easy to understand". The latter might mean "simple". But "simple" never means the former. And you never say it's "simple to X" for any value of X.
Under such a standard. The degree to which people are converging on this standard in actuality is not clear to me.
It's really not an arbitrary definition if you think about it etymologically. Very freely sim-ple is "unfolded" (c.f. pliers, plier qc). Whereas com-plex is "folded together". "Easy" relates more to a state of ignorance. It's about the user of the thing, not about the thing itself. (NB I'm just making this up. I'm not a linguist so I might be wrong).
His definition depends on the notion of Design by Contract. An exception is an event that causes a function/method to fail because it is unable to satisfy its contract. The caller is then responsible for cleaning things up (so it can satisfy its contract) or it also triggers an exception resulting it its failure.
In the context of Design by Contract, the list of things that can cause an exception include:
* hardware/OS errors
* calling a method on a null reference
* calling a method that itself fails
* discovering a pre-condition isn't true
* discovering that a post-condition isn't true
* discovering that a class-invariant isn't true
* loop invariant failure or lack of progress
* assertion failures
* explicit triggering of an exception
I highly recommend Object Oriented Software Construction.What counts as intended is a question of definition, but in concrete examples of local reasoning within a function this question is usually easier to answer. Thinking about it in terms of (explicit or implicit) contracts, like Bertrand Meyer, sounds like a smart idea.
I'd beg to disagree.
As for the general statement of your post, I like how you're talking about stakeholders and (specification) bounds but I don't think it's an actionable viewpoint. The idea of "Bubbling up errors" inside a program is caught in the software reuse mindset but it's not possible to take action on encountering a true error (here "true" means "outside specifiation"). That's because the state of your whole program is undefined after the encounter. Which means that the best you can do is to call abort() and hope that the program will end with some info for debugging.
(Unless we are talking about a VM or sandboxed code, in which case again you could just call some equivalent of abort() from the code inside and "catch" that in the host code and restart the VM or something).
To clarify, in the context of the article, "making errors observable from the outside" is not meant at the per-function level, but meant at the level of the whole program, which includes abort(). I should improve the wording there.
abort() is perfectly legitimate if you can't recover within the same process. It's not at odds with routing to the right stakeholder, as long as the parent process catches the crash and does appropriate error handling (e.g. "Send a crash report" dialogs or other watchdogs).
Regarding textual logging, I'm not sure exactly which part you disagree with. :) I'm aware that people commonly write regex-based extractors, and I've done the same, but having the choice I'd always go for a more schema-ful error reporting channel. That's more lightweight both for reading and for writing it, and avoids a whole class of regex bugs.
Regarding textual logging, I think it works wonderfully and I don't think it's at odds with schema-ful reporting. One good way is to include error codes. Informal messages are at least as important. They are ergonomic to humans and their meanings are easy to look up with a web search.
Of course textual logging can always be supported by other means, like crash dumps.
I do, too. I had read most of the book [1] some years ago. It is really good. But readers should know that they need to be ready to put in enough time to read, understand and digest it, not just because it is a thick book, although it is that, but because, true to Bertrand Meyer's style, it is detailed, systematic, thorough, etc. It's not one of those books where you can read it in a few days and then start applying its stuff to your work, nor is it for casual programmers who are only into the field to make some quick bucks.
[1] https://en.wikipedia.org/wiki/Object-Oriented_Software_Const...
BTW, the book may have an Easter egg in it.
Exceptions are in no way simpler than C-style error codes, but in many cases they might be easier to work with.
However, exceptions do simplify some situations, such as when an unrecoverable error happens at the bottom of a deep call stack. Exceptions make it simple to inform the user/system that an error has occurred without adding error checks to every function in the call stack. I believe this use-case justifies the feature, especially if you can tolerate the performance loss which might not even be that bad [0].
Having the compiler check error codes is one of the reasons I enjoy Haskell so much: the type system makes you handle failure (amongst other things).
[0] http://www.open-std.org/jtc1/sc22/wg21/docs/TR18015.pdf - Section 5.4.1.2 - the table approach to exception handling has no run-time cost during normal (non-exceptional) program flow.
The actual problem is the ramifications on the code structure. A deep call stack with a lot of implicit context is a problem in itself. Exceptions imply a temporal coupling from error occurrence to error handling. This is a subtle but severe problem. It might start out as a noticeable maintainability problem but it's likely to quickly become a performance problem as well...
Could you expand on this? I understood your statement as "Exceptions sort-of force you to handle an error the moment it occurs", but I'm having trouble seeing why this would be specific to exceptions and not the case with other error handling solutions.
(I don't mean to sound like a big exception-defender – I prefer ML/Rust-style Result types)
`(Optional<A>, Optional<B>, Optional<C>)`
– each can fail or succeed independently. But you can't do that if you're using exceptions to signal errors – it's like returning `Optional<(A, B, C)>`
, you get either all the results or nothing. Good example, thanks! function f (...) {
val a = something.a();
val b = something.b();
val c = something.c();
return (a,b,c);
}
If the exceptions can occur in the calls assigning to a, b, or c and are handled locally (here, in f) then you can construct that first case. It's only if you throw the exception up one level higher that you end up in the latter case. As written, it is more like your latter example. But with a modication: function f(...) {
val a = default_a;
val b = default_b;
val c = default_c;
try {
a = something.a();
} catch {
a = error_value_a;
}
...
}
You can get the former case.Looking at errors as just data avoids the "exceptional vs non-exceptional" hair splitting, and gives a lot of flexibility for code structure. I don't necessarily disagree with the Result type viewpoint as a return type from functions, but I also find it pretty pointless. Very often the best action is to separate out errors from successes into different tables immediately. A built-in Result type couples them, and encourages keeping them coupled.
Now you could argue that you can do that with Exceptions, too, by catching them immediately and treating them as data. In which case I want to ask "what's the point then?" and also refer to my topmost comment. Exceptions have a significant cost in infrastructure even if you don't actually use them...
What is the point of sorting or grouping them?
Any chance you have another example?
Could you share another example where accumulating errors makes sense?
A real life example: Asynchronous webservers usually have worker threads for I/O. I/O errors need to be routed to the originators of the I/O requests (which are in other threads). Exceptions cannot do that.
Here's the general point: Whether any given thing is an "Error" is highly subjective and context dependent. But clearly the error is data, so why take away the possibility to process it like any other data?
IMO, using exceptions doesn't preclude treating errors as data.
In your example with parallel processing, you could throw an error as exception, let it unwind the whole stack, catch it and return as the result of the task.
This way you don't couple the code that does the useful work with the code that handles errors.
In other words, I don't understand why you dislike exceptions.
IMO they are a good tool for any task that doesn't require different actions to handle different errors.
They introduce additional control paths which are hard to reason about. In many languages these are not even explicit. Exceptions require an additional syntax to handle them. And having exceptions requires significant language infrastructure and comes with a huge toll on the structure of software projects. See my topmost comment.
> In your example with parallel processing, you could throw an error as exception, let it unwind the whole stack, catch it and return as the result of the task.
Which would catch only the first encountered error per thread. And not catch all the other errors that we could encounter and report after that.
"And not catch all the other errors" I think, in most cases after an unexpected error happens it doesn't make sense to continue the task and report more errors, most likely induced by the first error.
Don't get me wrong, there are plenty of cases when returning errors as values is the best fit and using the exceptions would be a disaster, but my point is that, IMO, the situation when errors abort tasks and are handled in more or less the same way is much more common.
Basically the first class languages on a given platform, or having official bindings for a specific set of libraries that must be used.
My experience has proven it is the best path for lowest attrition.
Every time I decided to do otherwise I repented later on.
> In 1995, Netscape Communications recruited Brendan Eich with the goal of embedding the Scheme programming language into its Netscape Navigator.[11] Before he could get started, Netscape Communications collaborated with Sun Microsystems to include in Netscape Navigator Sun's more static programming language Java, in order to compete with Microsoft for user adoption of Web technologies and platforms.[12] Netscape Communications then decided that the scripting language they wanted to create would complement Java and should have a similar syntax, which excluded adopting other languages such as Perl, Python, TCL, or Scheme. To defend the idea of JavaScript against competing proposals, the company needed a prototype. Eich wrote one in 10 days, in May 1995.
It had to look vaguely like Java (hence ALGOL-derived brace delimited blocks rather than Scheme-derived prefix S-expressions, and the confusing name), and it had to be implemented quickly (hence the lack of typechecker and other advanced features), and it was intended to be beginner-friendly (hence all the truthiness/falsiness stuff).
Javascript only succeeds because it's the only scripting language supported by web browsers, and the brief attempt at getting VBScript into browsers was even worse.
Since then there has been more than enough time to implement optional static types.
In fact, the EcmaScript 4 proposal had those, before it was trashed in 2007 or so and the TC39 started EcmaScript 5 from scratch.
> object-oriented programming is best for problems with a large number of related data abstractions organized in a hierarchy
In OO books, maybe. In practice, OO is the way to compose very large systems out of big components. For hierarchies of data abstractions, very often OO is far from best.
> Popular mainstream languages such as Java or C++ support just one or two separate paradigms.
They support most of them, esp. modern C++, and C#. E.g. quite recently I was programming C++ in monotonic data flow paradigm, because MS media foundation.
Not always: I'm a jerk 22.7% of the time.
> Similar reasoning explains why Baskin-Robbins has exactly 31 flavors of ice cream. We postulate that they have only 5 flavors, which gives 25 − 1 = 31 combinations with at least one flavor. The 32nd combination is the empty flavor. The taste of the empty flavor is an open research question.
I honestly can’t tell if this is a good joke or serious & bad logic. The 31 flavors are obviously not a mix of 5 base flavors. If that were the case, each base flavor would be in 16 of the mixed flavors, and you wouldn’t have any unique flavors at all, like mint or cookie dough. It’s just a coincidence that the longest months in the year are 2^5-1 days.
I'll help out: It's a joke.
But then again it gives the joke a special nonplussing hilarity that it wouldn’t acquire otherwise.
/s.
Of course, many programmers would never need this stuff and many have no formal qualifications but they still produce what they need to without problems. They certainly do not need to know about paradigms vs concepts vs models etc. even if it is interesting.
Of course, there ARE people who need to know this stuff to do their job well but they are quite up the food chain compared to most of us. They might also be corporate, who of us sits down and thinks, shall I do this in OO or functional? Which paradigm fits? Most of us know few languages and use what we know.
You're going to have to offer a great deal more useful critique than "utter rubbish" to gain any meaningful agreement here.
"This chapter is partly based on the book [50], familiarly known as CTM, which gives much more information on many of the paradigms and concepts presented here. But this chapter goes further and presents ideas and paradigms not covered in CTM."
I poked around a bit on Van Roy's website, but couldn't find the source. It would be interesting to know what it is.
As attested by this: http://lambda-the-ultimate.org/node/3465 and the author's own intervention in these comments.