I've hesitated to provide an answer for this since it would be, necessarily, very long. But I've spent more than 20 years on this basic question and suppose I need to bite the bullet and try to clear up misconceptions that should have been dispelled generally years ago. My focus has long been on how to teach programming effectively, as well as programming languages in general.
And for starters, you don't teach programming by teaching a lot of syntax. Nor is the difference between languages primarily their syntax. Not if they represent different paradigms, at least.
I apologize in advance for the denseness of this. I hope it is accurate. My writing in general has been described as accurate but too dense. Because of this, it may be hard to grok the implications of some of it without reflection.
When we teach a first course in programming, per se, the course has to answer the fundamental question "What is a program?". Giving a concise definition to a beginner will just lose them, but, by the end of the course, they should have a good understanding of an answer as well as guidance in How To Program. If the course leaves them confused about that you have done a disservice and they will, mentally, need to start over to get an understanding of what is a program and how do we construct one.
Everything we do in that first course should be directed to that end. What is a program? How do we create one?
Sadly, too many people think of a programming language as a bunch of stuff thrown together, features, that can be used in any way you like. While that is literally true, it leads to terrible, unmaintainable, impossible to understand, cruft. We need to do better and our students need to have proper guidance.
In fact, language designers don't think that way at all.
However, there are different conceptions about what a program is, and those different conceptions (paradigms) lead us to different answers to the question "What is a program?" and the question "How do we create one?" Especially, how do we create a beautiful, useful, maintainable, understandable, program?
My university once declined to hire a very smart guy because he admitted that the program he created as the basis of his dissertation was so complex that he couldn't really understand it and was afraid to modify and extend it because it was so fragile. A naive understanding of programs and programming will leave you in exactly that place. It's creator has lost control of the creation. A Frankenstein Program.
There are others, but the three main paradigms are Functional, Procedural, and Object-Oriented. I will try to explain each of these in turn and also explain why one shouldn't try to mix them with beginners (and mix them only very carefully for experts - C++, say). Each of these paradigms has a different answer to the question "What is a program?" They also give guidance about how to construct a program within that paradigm.
Note that for all of these paradigms, the descriptions and the definitions are highly recursive, even when the programs written in them are not.
And note that once you have answered the question What is a Program for any paradigm, you can write any program at all. All the paradigms are Turing Complete hence equivalent in power. If you master any of the paradigms, and have adequate language tools, then you don't need any other paradigm to be able to write programs. Some may be more suitable for some kinds of problems, but all are capable.
Since this paradigm is less important to the current question I won't expand it so deeply. But a functional program in something like Scheme, or even Standard ML, is an expression that, when evaluated, gives the answer to a problem. Normally that expression is a function, expressed usually as a list, and the evaluation usually results in another list.
But that brings us to the question of what is a function and an expression. An expression could be simple, like a value (42), or it could be a list, or it could be the result of applying a function to some arguments, which are, themselves, expressions. Note the recursion.
The question of How to Program is, then, you create an expression to represent the solution. If the expression requires function evaluations then those functions are, themselves, written to evaluate expressions that are smaller problems within the larger problem. When the overall function is evaluated it starts a cascade of evaluations of functions solving the smaller problems and then the original function constructs a solution from the solutions of the sub problems.
And no, that description isn't suitable for a novice. They need seasoning and practice so that the definition starts to emerge in a way that they can eventually (end of the course) understand.
But note that this is recursive. An expression is composed from expressions, many of which are the results of function invocations.
There is more that can be said, and I could probably refine this quite a bit, but that is really the essence of functional programming. What is a functional program? It is an expression. How do I create a program? By creating an expression that represents the answer I want, probably by writing functions that are themselves compositions of functions.
But note that this "definition" means nothing to a novice. Gibberish. They learn it by writing simple expressions and simple functions, then progressing to more complex situations in which an expression needs to be composed from the invocations of several functions. If you look at a well-formatted Scheme program (i.e. properly indented) the indentation shows the structure.
Procedural Programming (aka imperative)
Procedural programming was probably best defined by Edsger Dijkstra for the Algol family of languages, such as Algol, Pascal, and Modula-2. In this paradigm a Program is a procedure that when executed produces a solution to a problem. How do you create a program (procedure)? If the problem is simple enough, you just write down the solution as the body of the procedure.
But mostly, trying to do that results in a mess since the problem is too hard to understand (and solve) in a single pass without decomposition. Therefore, in order to write that procedure (solve that problem) you factor out sub problems that, collectively, have three characteristics:
The sub problem is a problem in its own right. The nature of the problem and what would constitute a solution is known.
The sub-problem is solvable with a procedure, in theory.
There is a known way to recompose the solutions to the sub problems into a solution of the overall problem.
Then, the sub problems need procedures to solve them. To do that, you proceed recursively decomposing those problems as needed into smaller problems with the above three characteristics. The recursion stops when you can actually solve all the smallest problems with (simple) procedures. Then you recompose those procedures into the solution you initially desired.
This is why procedures can be nested in languages like Pascal and why C isn't ideal as a Procedural Programming Language. Additionally, the ability to nest functions implies (in these languages, at least) that variables defined at one level are visible at lower levels (scoping), but not elsewhere. This disciplined approach to information hiding/revealing is very powerful. In particular it is used to reduce coupling and increase cohesion in programs. (I can expand this if needed.)
What is a Procedural Program? It is a tree of procedures where each node in the tree represents the solution of a sub problem, with lower nodes representing the decomposition of a problem and leaves of the tree represent simply written procedures needing no further decomposition.
How do you create a Procedural Program? You construct the tree from the top down, decomposing as needed to extract simpler problems and then re-composing solutions to solve the overall problem.
Again. This explanation means nothing to a novice. They learn it from practice. Start with relatively simple problems/programs that don't require decomposition and progress to more complicated ones to get practice with both the decomposition of problems and the recomposition of solutions.
And none of what I have said has anything to do with if-statements and while-statements and integer variables and such. That isn't the essence. Those are used only for structure at the very lowest level of the program. If you try to define the overall (complex) program using only these you will fail all of the criteria of what makes a program good. In particular, as little as two pages of such code can be unintelligible even to its creator. (I have evidence of this from a very important - mission critical - project at a major US company. And, well, student projects, of course.)
So, to think as a procedural programmer, you think in terms of procedures (some of which are functions) and, if the problem is difficult, imagine all of the simpler problems that, if they could be solved, would represent the solution you desire. Solve the simpler problems with procedures.
In both procedural and functional paradigms the text of the program is tree-like. Namely nested structures. But the running program is also tree-like. A procedure or function can invoke its "child" nodes generating a run-time tree of execution. Call down the structure. Return up the structure. The tree is traversed (in time) as the program executes. And in both of these paradigms, the program is thought of as a process, not a thing. It answers a "how does it work?" question, not a "what is it?" sort of question. And most of the structure of such a program is a description of an algorithm. The overall program does something. It's parts do some part of that. The structure models the process. This will be different in object-oriented programming.
Until recently procedural programming in the above model was impossible in Java, since functions couldn't be nested. The introduction of Lambdas in Java 8 has changed that, but for a different purpose. And I'm pretty doubtful anyone has yet used lambdas in this way. And if you do, you come to something closer to a functional program than a procedural one. But if you want to solve a large and complex problem with static methods in Java, the structure is flat, not nested. All of those functions are at the same level in the program. This is actually the programming model of FORTRAN. One function that solves part of the problem of another can't be nested inside that other function.
In a procedural program, at any time that a procedure is called, the actual text of the program will tell you precisely which code will be executed. But in a functional program this may not be the case. This is because functions are, themselves, values that can, for example, be stored in lists and passed around as arguments to other functions and then invoked. In other words, you may need to actually execute a functional program to determine what it will do next. We shall see this again with object-oriented programs, but in a different way.
What is an object oriented program? It is the orchestration of a web of interacting encapsulated values (objects). The web of interactions produces the solution to the problem at hand. The interactions are modeled as services that an object provides to other objects.
How do you write an OO program? You first analyze the internal structure of the problem and determine its "parts" you model these parts as faithfully as necessary for your purposes (objects). You also define the web of interactions, partly externally (function main), but mostly internally as determined by the needs of the objects.
Example: If you want to solve problems about traffic flow (self driving), you model the transportation system to some necessary level of detail. A transportation system has parts: it has roads and bridges, cars, busses, intersections, signals, detours, ... These "parts" are modeled by objects, which are, in turn, described by classes. There is only one "system" but it has several "cars", etc. The cars interact, on occasion, with each other and with other objects,
Example: A student information system has parts: courses, students, instructors, records, rules and exceptions, ... The parts interact: students enroll (a service) in classes, etc.
Example: A calculator has parts: one or more displays, keys, internal stacks, ... The parts interact. Key presses (a service) cause changes in the display (sometimes). ....
What, then, is an object? An object is an encapsulated actor that is comprised of both data and behavior. The behavior models services that the object can provide. The data can only be acted on by requesting a service of it. In a complex program, the "data" within an object is made up of other (lower level) objects, that provide services to the containing object. (Note the recursion here).
What services can an object provide? These services are defined by an interface, that might be stated explicitly or not. Each service is modeled as a protocol for a procedure or function (collectively called methods). In smalltalk derived languages (Java, Python, ...) the protocols are defined in classes, that also specify the data used by objects created from the class.
Objects are created dynamically as the program runs, not statically from the program syntax. A class is a description of a certain type of object, but the objects are distinct. Most classes can be used to create many, independent, objects of the same type, but individually encapsulated. Objects (via their services) can create other objects, both internal to their own structure and externally.
Communication (the web of interactions) in an OO system is via messages. A message requests a specific object to perform a specific service. Some services return data (functional). Some update internal structures of the objects or request further services of other objects without returning data (procedural). While possible to do both, it is discouraged as it makes the program more complex and harder to maintain. It isn't forbidden, but novices should be discouraged until they have more experience. (A saw can be used as a hammer, but the novice is discouraged.)
It is a feature of OO programming that the text of a program cannot predict, precisely, the code to be executed when a message is sent. This is similar to the situation in Functional programming where functions are values. Objects from different classes can be sent the same messages, provided that the classes implement the same interface (In Java you need explicit implementation of a defined interface, but not in Python.) The implication of this is that when a reference variable "points to" some object that is defined by an interface, the code to be executed is only "known" to the object - and the object doesn't exist in the text of the program but only at runtime. And, the implication of that is that you program to interfaces, not to specific class structures. The same is true in subclassing. Objects from the subclass implement (at least) the interface of the superclass, but may do so in different ways. (See the notes on polymorphism, below.)
---- Complete for now, but I'll think some more. -----
Good and Bad OO Programs
Here are some rules for creating good programs. I'll focus on Java. In Python and Ruby they would be a bit different. The rules in those other languages still hold, but those languages give you a bit less support for them since they are dynamically typed where Java is statically (compile time) typed.
True experts can break these rules, but normally consider them. But in the first course, insist on them and teach students that dangers lie outside the safe zone provided by the rules. "Here there be monsters!"
Don't write big classes: classes should be small with only a few methods. They should be single purpose. Do one thing well on request.
Don't write long methods: methods should be short with very low Cyclomatic Complexity. I start to get itchy when a method is more than three statements or when I need nested statements. The complexity of an application isn't in the individual methods (or classes) but in the interactions between objects. This skill, factoring a complex program into small and simple parts, is one of the most important and hardest to develop. It needs to be practiced.
Write a lot of classes: This is a consequence of the first two rules. A system is made up of a lot of parts, but the parts, themselves are simple.
Use subclassing very sparingly and never deeply. Novices tend to break this rule, thinking that subclassing is the Big Idea of OO programming. It actually isn't. There is much more power in using composition and polymorphism than in subclassing.
Write classes whose internal data is mostly other objects, not language primitives: Complex parts are made from simpler parts. This is the purpose of inner classes. You can hide the structure of an object by hiding the structure of its parts. This is encapsulation and data hiding done well. A car object has an engine object. An engine object has piston objects. A piston has bushing objects. Complex things made up of simpler things. But a car (as a whole) doesn't need to "know" about pistons. Only the engine does. And certainly the transportation system can ignore pistons. This is programming by composition. Composition is really the Big Idea of OO.
Write a lot of interfaces, even if only one class will implement a given interface: The interface documents your intent. Understanding the intent, and programming only to that intent, is what makes complexity manageable.
If you create a subclass, don't also extend its implicit interface: That is, don't add additional public methods. You will be forced at some point to use static polymorphism to resolve things if you do. If you need new "features" then extend the formal interface instead and have the subclass implement that interface. (I've found this to be one of the most important measures of code comprehensibility.) A circle isn't a subclass of (special case of) a point. Use subclassing for specialization, not extension.
Most reference variables should be typed with an interface, not a class: This also helps you avoid the trap of being forced into static polymorphism (and warned, at least) when you fall into it.
Use only public and private for class features. The others are for experts. Protected, in Java, is a land mine. All data elements, other than constants, should be private. Among the methods, only the service-defining ones are public. Protected is intended for large teams building complex software over several packages. Unspecified visibility increases the possibility of linking between elements of a class, complicating maintenance and extension. Just. Say. No.
Use static methods sparingly and only for their intended purpose. A method that tells you the number of cars created by the Car class is static. A method that tells you the color of an individual car is not static and returns information separately by each individual object.
Don't build classes speculatively, thinking that you might want them later. (Reuse is overrated). Classes are there for purpose-build (bespoke) objects, not for general principles. Don't build the Car class in your transportation system model until you know you need it. Don't commit much to its interface until you know how it will be used and what actual services it must provide. A mutator to change the color might be completely superfluous. Build what you need, when you need it.
Achieve polymorphism in a program, not via subclassing as such, but by judiciously replacing one object with another. Both have the same interface, but each performs some process in a different way. See the Strategy and Decorator Design Patterns.
A note on OO libraries
One would like to use the libraries supplied with a language as models for good programming in the language. Unfortunately it doesn't work out so well. Library code breaks many of the rules I suggest above as you are probably aware. There are two aspects.
First is the the goals of a library writer are very different from those of an application programmer. The former is providing general purpose services for programmers, rather than purpose built solutions for users. The modeling problem, in particular, is simpler.
And second, the library writers are experts who interact with other experts to define the libraries. When they break a rule they do so very carefully and after long thought and testing.
Class nesting and using protected features are often used in libraries. But so are lots of interfaces and extensions of interfaces. But these are expert-only tools.
“Do not meddle in the affairs of wizards, for they are subtle and quick to anger.”
Polymorphism: static and dynamic
Selection statements in a programming language are a form of polymorphism. They provide different actions, depending on circumstances. But this is static polymorphism. Almost all programmers are familiar with it, even if not by that name.
Dynamic polymorphism, however, is something that happens only at run time. If a variable references an object, then a message to the object names the variable, not the object directly. If the variable refers to different objects of different classes (but the same interface) then different things can occur when the same message statement is re-executed. And the text of the program doesn't normally reveal this. In Java, however, you can subvert the system by forcing static polymorphism using something like the instanceof operator. Through it you can learn the specific class of an object (usually) and you can then know the precise code that will be fired in a message. But this usage represents a failure. You should never need to write code that does this, and if you really need it then your program is broken in other ways. Perhaps you used subclassing naively. This sort of programming is a code smell that indicates something sick or wounded elsewhere. Let your program heal to avoid it. Let dynamic polymorphism be your friend. If you need to fight it, you need to rethink how you are using your tools. Hammer, meet Saw.
And don't confuse polymorphism with subclassing.
How Abstraction fits this scheme
Abstraction (simple names for complex phenomena) are vital to all three paradigms. But different languages have more or fewer ways to do it. Procedural abstraction (naming a complex process) is about all that is available in Pascal (or C). But class based abstraction (naming of things) can be a very powerful adjunct. So powerful, in fact, that it becomes the main tool. Model the things. Give them good names. And, the rules I've suggested (lots of small classes and methods) means that you create abstractions for many things in an OO program when built as described above. You become a namer of things. Which, itself, is a kind of wizard.
What emerges from programming OO in the way described here, when mastered, is programs that are expressed directly in the language of the model - which is closely related to the language of the problem and in terms of our intent. The opposite is programs expressed mostly in terms of language primitives which, at best, are mere surrogates for our intent.
Given my insistence that students learn from repetition (reinforcement) and feedback, if I want to teach them to program in Java, I have to do objects first. Otherwise they get too little practice on the most important things. But I never start with an empty class and an empty main method. Students in a course I'd be willing to teach will start with a substantial amount of scaffolding that lets them focus on the important things, rather than low level syntactic details.
Students can work to extend an existing system. With care, a simulation can be created that allows the students to program in a Turing Complete environment while emphasizing the essence of "What is a program?" and "How do I build it?". The Greenfoot programming system provides a fairly rich set of starting points. The Greenroom site, linked from there, is the specific teacher resource. (I know the developers and have contributed to Greenfoot, but have no connection.)
If you are using an object-oriented programming language, learn (and teach) how to do it well. Don't use it for things (other paradigms) for which it is ill suited. Teach students in the first course some paradigm, but only one. paradigm, so that they have a meaningful answer to the two questions: What is a program? How do I create one?
So, for a formal answer to the question posed, no, I wouldn't do that. It is actually more like C programming. If I wanted to teach C and its concepts (and have done), I'd use C. C is a better C, anyway.