
OOP, IP, FP and Hanoi Towers
This is some attempt to illustrate the significant differences between OOP, FP
(functional programming) and PI (imperative) and prove why PI
is the preferred style of thinking when solving problems.
I will try to do all this using the example of solving the problem of Hanoi towers.
Naturally, I do not pretend to be called an experienced OO, F or I
programmer, but still I have some skills, and I will try to
apply them to this task, but the course of my thoughts, most likely, is possible and, more
Moreover, it will be necessary to criticize, preferably constructively, in order
to achieve the truth.
First, let me remind you what the essence of the problem is. Initially given three rods: 1, 2, 3 - on
one of which is strung a turret of N circles of different radius so that a
circle of a smaller circle always lies on a circle of a larger radius. The program must make a
plan for shifting these circles from the rod to the rod so that the turret is
on another rod. At the same time, each shifting should preserve the
order of the circles - any circle can only lie on a circle of a larger
radius, or on an empty rod and only one of the
top circles on the rods can be shifted .
So, if a tower is initially given with a height of two circles lying on 1 rod,
and if you need to shift it to rod number 2, then one of the correct
sequence of
moves will be: 1 -> 3, 1 -> 2, 3 -> 2. B rules N -> M
only the numbers of the rods are used and they mean: shift the topmost
circle from rod N to rod M.
Functional approach. Considered first, as it provides a classic
'school' solution to the problem.
We need to build a function.
A function that, according to the height of the turret, the initial rod a, the final rod b
and the additional rod c will produce a list of moves. Hmm ... Well, and from what other
functions can move be concocted? Compiling some functions from others is the essence of the
approach. We can come up with a function
which transfers the circle from one rod to another, but there
will be no sense from it , because it is not clear how to call it. You need to
meditate a little and overshadow your brain with a brilliant functional concept:
we look at the first example and see, to transfer the turret, we must first
transfer its top to the auxiliary rod, move the lower circle to the
target, and then move the top of the turret to the same rod. At the same time, the
upper circles can be shifted to any rods, because everything
below them has a larger radius.
Hurrah. The problem is
solved. It remains only to understand what the function move must return and get down
to business. But here everything is simple, the FP does not give us any alternative. So for
a business. The implementation is sketchy and resembles what you can write in Haskell.
Elegant, challenging.
But now an object oriented approach. First you need to select
objects. Well, with this, everything is simple: circles and rods, as well as the turret itself, because
we need a solution in the form of sending a message to some object. Something
like that.
Excellent. We have a turret, mugs and rods. And how to connect all this together
through sending messages? Hmm ... Honor the back of the head and decide that there is
no sense from the rods , from the one chip too, because it alone does not
affect anything . It remains to concentrate on the turret, scratch the pumpkin and say that the
turret, it turns out, consists of two objects: the top, which is also
a turret or missing, and a large circle at the base. Cool.
If we build the object like this, then the solution is again obvious. There is a turret asking to
jump to another rod using some auxiliary, then it
first asks to jump to the top of its auxiliary rod, then
moves its base to the target rod, and then asks to jump to
the top of this target rod. Great, now we’ll roll a couple of classes.
Everything is schematic and similar to Java. All methods are open and data is private.
Wow, that was cool. They have lost elegance, but we have a cool
lens that can check errors for itself, is encapsulated, and
in general all that kind of person is pretty. The supposedly imperative approach will
merge even more elegance and the lenses will not give pretty ones, therefore, well,
it’s in the fig, in theory. But we, gritting our teeth and break through it.
So, what do we have in an imperative approach? Nothing: no functions, no objects.
Only the states of a certain region of memory and a description of the changes in these
states. In the state we can have lists of circles on each of the disks.
Hmm ... Not very optimistic. Well, lists and lists, but what to do with them? Hmm ...
Lists. Eureka, we still have a list of moves, some, but which?
For example, we have a list of moves for moving a turret two circles in height from the
first rod to the third through the second: 1 -> 2, 1 -> 3, 3 -> 2.
Excellent. What can be done with it? Well, for example, replace the column numbers.
For example, like this: 1 = 1, 2 = 3, 3 = 2. And get a program to transfer our
economy in two circles to the second core: 1 -> 3, 1 -> 2, 3 -> 2. Do you feel
what I mean?
If now we have a program for transferring a two-level economy to the second
column, then we can now put the third largest pancake on the third one, and
then, using the already known program for transferring a 2-economy, now with
the first rod to the second through the third, making a substitution in it:
1 = 2, 3 = 1, 2 = 3, we get a transfer program from the second rod to the
third through the first. And in the end, we will become the owners of a wonderful program to
transfer the 3-economy from the first rod to the third through the second. Etc.
It remains only to understand how to start such a development of the program. But
this is obvious: if the turret is of odd height, then the first move should be made
on the target rod, otherwise on the auxiliary one. The solution is written
in something similar to iscript.
Hm. It will be longer than the option of FP or OOP. But let's compare this code and those
two options, and they can be considered similar to each other. Attention can be
paid to a couple of features.
Firstly, the program generation starts immediately, you do not have to wait a long
time until the recursion, which can be quite
long, ends , the complexity of the task in the moves grows exponentially. Meanwhile, well-known
moves can be sent for further parallel processing, for example, a
program for visualizing this very Hanoi game.
Secondly, the imperative code is able to work with an endless pyramid.
He generally doesn’t really need N, unless in order to determine the first
move.
Two recursive constructions cannot do this. Hmm ... A slight philosophical
digression: perhaps this is not within the power of a recursive construction just because of the
uncertainty with the first move. Recursion must always be defined. But
this is a remark for future analysis.
As a result, I personally (I emphasize: I personally do
not pretend to truth in the last resort ), the above reasoning leads to the next look at
programming approaches.
Programs should be considered precisely as dynamic systems, that is,
systems described by state changes.
When we start trying to
formulate functionality in the form of static mappings, as in
functional programming, we are forced to limit ourselves
1. in dynamics, since the value of the function must be calculated before we
can use it
2. in efficiency and distribution: if the calculated value
can potentially be used before its full calculation is completed, then in As part of the
classical FP, we have no way to do this.
Of course, solutions to these problems have been found: in ML there is the ability to write
imperative programs, in Haskell there are monads that
match the dynamics of imperative calculations with endless lists of values that are the
trajectories of this dynamics - not the most convenient solution, imho.
When we try to look at programs as interacting through
special interfaces, objects, then we again lose dynamism. Because
objects in our intuitive representation correspond to processes that are
not at all, not to dynamic structures at all. In this example, in a natural
way (in my opinion, of course, but supported by many textbooks on
OOP, in which similar tasks, about 8 phrases, for example, are solved exactly this way),
an object was chosen: a turret. And we see what this abstraction leads to - the
immediate construction of a recursive algorithm, with all the ensuing
drawbacks.
Well, of course, in the case of implicit OOP, data generation for the visualizer
You can start immediately, as recursion reaches the first bottom, but it can
reach it for a very long time. In the framework of the FI, this can also be done, but somewhat more
confusing. In addition, we must not forget that we will not
be able to solve the problem with an infinite turret in the system of objects so chosen.
Due to the fact that OOP and FI limit the programmer in the ability to
see the dynamics in the task, I personally never use them in the design.
I focus on how the data should flow in the program and how it should
change, and this leads, in my opinion, to more beautiful and simple
solutions. Because the world is dynamic, because such design is much
closer to physical reality than objects or functions, because there are no
objects or functions in nature. There are only dynamic systems, as
modern physics is trying to prove to us. So why limit your mind to
focus on static models of reality, which are FP and
OOP? FP by its mathematical nature, and OOP for the reason that in a
modern form fixes consciousness on analogies with passive
material objects.
In fairness, it should be noted that in our example, the object could just
select a chain of moves, and build a
program similar to the imperative solution , BUT the OO programmer begins with a search for obvious objects, and not with
analysis of dynamics, which limits its design capabilities,
and this often leads to solutions more similar in
structure to FP. Within the framework of the FP approach, this could also potentially be
done, but much more difficult, and the infinite tower could
only be calculated in functional languages with completely lazy execution; within the framework of the
lamda abstraction itself, such a calculation would be non-computable. Which again, by the way,
proves that a computer is much more than a Turing machine.
In addition, programming languages
that call themselves OO are also prone to such design . Objects in them are not
dynamic entities exchanging messages, as it was conceived back in
Smalltalk, namely static
abstract data types, no more. A dynamic object that lives its own
life is rather difficult to create. This is opposed even in distributed
technologies, the same CROBA, in
which objects come to life only upon request, and just the very possible
implementation method : launching a separate thread, and accessing shared data with the interface
using synchronization. This is already creating a full-fledged client-server
application. But OOP again inclines to create it not as a process,
but as a set of objects, which again turn out to be dead and non-dynamic
in nature. Between them it’s difficult to organize a real data stream, their
It’s difficult to run in parallel, because for this they need to be done by
servers again .
Meanwhile, some languages, which began as purely functional solutions,
eventually developed to provide the ability to create just such entities
- processes that exchange messages. Erlang and O'Caml, for example, but this
possibility is based on the imperative functionality of these
languages: monads or an explicit ability to store states. With a pure,
imperative structural approach, there are no problems at all:
Limbo, Ada, or something like IPC on UNIX.
Actually, the appeal is based on this (not only mine, by the way:
http://www.lysator.liu.se/c/pikestyle.html) abandon OOP and FI wherever
possible, and focus on analyzing data changes in
programs, on state properties, and on the logic of changes.
Another
retreat: modern physics considers it existent, and it is intuitively
clear that it preserves properties in the process of changes, without focusing on the
analysis of the dynamics in the program, it is impossible to accurately identify the stored properties, and
therefore objects. Such an analysis leads to more flexible solutions, and to more effective ones.
Interfaces, functions, modules, classes will arise naturally in the process
of program implementation. And they can be reused.
(functional programming) and PI (imperative) and prove why PI
is the preferred style of thinking when solving problems.
I will try to do all this using the example of solving the problem of Hanoi towers.
Naturally, I do not pretend to be called an experienced OO, F or I
programmer, but still I have some skills, and I will try to
apply them to this task, but the course of my thoughts, most likely, is possible and, more
Moreover, it will be necessary to criticize, preferably constructively, in order
to achieve the truth.
First, let me remind you what the essence of the problem is. Initially given three rods: 1, 2, 3 - on
one of which is strung a turret of N circles of different radius so that a
circle of a smaller circle always lies on a circle of a larger radius. The program must make a
plan for shifting these circles from the rod to the rod so that the turret is
on another rod. At the same time, each shifting should preserve the
order of the circles - any circle can only lie on a circle of a larger
radius, or on an empty rod and only one of the
top circles on the rods can be shifted .
So, if a tower is initially given with a height of two circles lying on 1 rod,
and if you need to shift it to rod number 2, then one of the correct
sequence of
moves will be: 1 -> 3, 1 -> 2, 3 -> 2. B rules N -> M
only the numbers of the rods are used and they mean: shift the topmost
circle from rod N to rod M.
Functional approach. Considered first, as it provides a classic
'school' solution to the problem.
We need to build a function.
move N abc
A function that, according to the height of the turret, the initial rod a, the final rod b
and the additional rod c will produce a list of moves. Hmm ... Well, and from what other
functions can move be concocted? Compiling some functions from others is the essence of the
approach. We can come up with a function
movetoken ab
which transfers the circle from one rod to another, but there
will be no sense from it , because it is not clear how to call it. You need to
meditate a little and overshadow your brain with a brilliant functional concept:
we look at the first example and see, to transfer the turret, we must first
transfer its top to the auxiliary rod, move the lower circle to the
target, and then move the top of the turret to the same rod. At the same time, the
upper circles can be shifted to any rods, because everything
below them has a larger radius.
Hurrah. The problem is
solved. It remains only to understand what the function move must return and get down
to business. But here everything is simple, the FP does not give us any alternative. So for
a business. The implementation is sketchy and resembles what you can write in Haskell.
move N abc // transfer one circle to the target turret move 1 abc = (a, b) // transfer the top to the rod c, transfer the big circle to the rod b, // transfer the top from rod c to rod b move N abc = move (N - 1) acb: (a, b): move (N - 1) cba // well, and call something like this print move 16 1 3 2
Elegant, challenging.
But now an object oriented approach. First you need to select
objects. Well, with this, everything is simple: circles and rods, as well as the turret itself, because
we need a solution in the form of sending a message to some object. Something
like that.
tower.move (a, b, c)
Excellent. We have a turret, mugs and rods. And how to connect all this together
through sending messages? Hmm ... Honor the back of the head and decide that there is
no sense from the rods , from the one chip too, because it alone does not
affect anything . It remains to concentrate on the turret, scratch the pumpkin and say that the
turret, it turns out, consists of two objects: the top, which is also
a turret or missing, and a large circle at the base. Cool.
If we build the object like this, then the solution is again obvious. There is a turret asking to
jump to another rod using some auxiliary, then it
first asks to jump to the top of its auxiliary rod, then
moves its base to the target rod, and then asks to jump to
the top of this target rod. Great, now we’ll roll a couple of classes.
Everything is schematic and similar to Java. All methods are open and data is private.
class Tower { // a - this is the rod on which the turret stands, useful for checks on // invalid requests, e.g. move (1, 3) if the turret is at 1 // pivot. For FIG knows what request will come, one must be reliable and // encapsulated. But there are no checks in the code. int N, a; Tower top; Tower (N, position) { if N> 1 { top = new Tower (N - 1, a); } else { top = null; } this.N = N; this.a = a; } List move (b, c) {// b - where to move, c - auxiliary rod List l; if top { l.append (top.move (c, b)); } l.append (a.toString + "->" + b.toString); if top { l.append (top.move (b, a); } a = b; // moved to a new rod return l; } } // launch System.out.print ((new Tower (N, 1)). Move (2, 3) .toString);
Wow, that was cool. They have lost elegance, but we have a cool
lens that can check errors for itself, is encapsulated, and
in general all that kind of person is pretty. The supposedly imperative approach will
merge even more elegance and the lenses will not give pretty ones, therefore, well,
it’s in the fig, in theory. But we, gritting our teeth and break through it.
So, what do we have in an imperative approach? Nothing: no functions, no objects.
Only the states of a certain region of memory and a description of the changes in these
states. In the state we can have lists of circles on each of the disks.
Hmm ... Not very optimistic. Well, lists and lists, but what to do with them? Hmm ...
Lists. Eureka, we still have a list of moves, some, but which?
For example, we have a list of moves for moving a turret two circles in height from the
first rod to the third through the second: 1 -> 2, 1 -> 3, 3 -> 2.
Excellent. What can be done with it? Well, for example, replace the column numbers.
For example, like this: 1 = 1, 2 = 3, 3 = 2. And get a program to transfer our
economy in two circles to the second core: 1 -> 3, 1 -> 2, 3 -> 2. Do you feel
what I mean?
If now we have a program for transferring a two-level economy to the second
column, then we can now put the third largest pancake on the third one, and
then, using the already known program for transferring a 2-economy, now with
the first rod to the second through the third, making a substitution in it:
1 = 2, 3 = 1, 2 = 3, we get a transfer program from the second rod to the
third through the first. And in the end, we will become the owners of a wonderful program to
transfer the 3-economy from the first rod to the third through the second. Etc.
It remains only to understand how to start such a development of the program. But
this is obvious: if the turret is of odd height, then the first move should be made
on the target rod, otherwise on the auxiliary one. The solution is written
in something similar to iscript.
nextblock (cmdnew, cmdold, f) { a = f [0]; // the current program is a permutation from a to b through c b = f [1]; // N top circles c = f [2]; // move the (N + 1) circle from a to c listappend (cmdnew, {.from = a; .to = b;}); f [0] = b; // generate a permutation from b to c through a f [1] = c; f [2] = a; i = cmdold.head; on i; i = i.next { listappend (cmdnew, {.from = f [i.from]; .to = f [i.to];}); } f [0] = a; // now we have a permutation from a to c through b f [1] = c; // (N + 1) -farm f [2] = b; } solve (N, a, b, c) { f [0] = a; if N% 2 { f [1] = b; f [2] = c; } or { f [1] = c; f [2] = b; } on N; N = N - 1 { nextblock (fl, sl, f); i = fl.head; on i; i = i.next { print ("% 0 ->% 0 \ n", i.from, i.to); } listmove (sl, fl); // move everything from the first list to the second } } solve (16, 2, 0, 1);
Hm. It will be longer than the option of FP or OOP. But let's compare this code and those
two options, and they can be considered similar to each other. Attention can be
paid to a couple of features.
Firstly, the program generation starts immediately, you do not have to wait a long
time until the recursion, which can be quite
long, ends , the complexity of the task in the moves grows exponentially. Meanwhile, well-known
moves can be sent for further parallel processing, for example, a
program for visualizing this very Hanoi game.
Secondly, the imperative code is able to work with an endless pyramid.
He generally doesn’t really need N, unless in order to determine the first
move.
Two recursive constructions cannot do this. Hmm ... A slight philosophical
digression: perhaps this is not within the power of a recursive construction just because of the
uncertainty with the first move. Recursion must always be defined. But
this is a remark for future analysis.
As a result, I personally (I emphasize: I personally do
not pretend to truth in the last resort ), the above reasoning leads to the next look at
programming approaches.
Programs should be considered precisely as dynamic systems, that is,
systems described by state changes.
When we start trying to
formulate functionality in the form of static mappings, as in
functional programming, we are forced to limit ourselves
1. in dynamics, since the value of the function must be calculated before we
can use it
2. in efficiency and distribution: if the calculated value
can potentially be used before its full calculation is completed, then in As part of the
classical FP, we have no way to do this.
Of course, solutions to these problems have been found: in ML there is the ability to write
imperative programs, in Haskell there are monads that
match the dynamics of imperative calculations with endless lists of values that are the
trajectories of this dynamics - not the most convenient solution, imho.
When we try to look at programs as interacting through
special interfaces, objects, then we again lose dynamism. Because
objects in our intuitive representation correspond to processes that are
not at all, not to dynamic structures at all. In this example, in a natural
way (in my opinion, of course, but supported by many textbooks on
OOP, in which similar tasks, about 8 phrases, for example, are solved exactly this way),
an object was chosen: a turret. And we see what this abstraction leads to - the
immediate construction of a recursive algorithm, with all the ensuing
drawbacks.
Well, of course, in the case of implicit OOP, data generation for the visualizer
You can start immediately, as recursion reaches the first bottom, but it can
reach it for a very long time. In the framework of the FI, this can also be done, but somewhat more
confusing. In addition, we must not forget that we will not
be able to solve the problem with an infinite turret in the system of objects so chosen.
Due to the fact that OOP and FI limit the programmer in the ability to
see the dynamics in the task, I personally never use them in the design.
I focus on how the data should flow in the program and how it should
change, and this leads, in my opinion, to more beautiful and simple
solutions. Because the world is dynamic, because such design is much
closer to physical reality than objects or functions, because there are no
objects or functions in nature. There are only dynamic systems, as
modern physics is trying to prove to us. So why limit your mind to
focus on static models of reality, which are FP and
OOP? FP by its mathematical nature, and OOP for the reason that in a
modern form fixes consciousness on analogies with passive
material objects.
In fairness, it should be noted that in our example, the object could just
select a chain of moves, and build a
program similar to the imperative solution , BUT the OO programmer begins with a search for obvious objects, and not with
analysis of dynamics, which limits its design capabilities,
and this often leads to solutions more similar in
structure to FP. Within the framework of the FP approach, this could also potentially be
done, but much more difficult, and the infinite tower could
only be calculated in functional languages with completely lazy execution; within the framework of the
lamda abstraction itself, such a calculation would be non-computable. Which again, by the way,
proves that a computer is much more than a Turing machine.
In addition, programming languages
that call themselves OO are also prone to such design . Objects in them are not
dynamic entities exchanging messages, as it was conceived back in
Smalltalk, namely static
abstract data types, no more. A dynamic object that lives its own
life is rather difficult to create. This is opposed even in distributed
technologies, the same CROBA, in
which objects come to life only upon request, and just the very possible
implementation method : launching a separate thread, and accessing shared data with the interface
using synchronization. This is already creating a full-fledged client-server
application. But OOP again inclines to create it not as a process,
but as a set of objects, which again turn out to be dead and non-dynamic
in nature. Between them it’s difficult to organize a real data stream, their
It’s difficult to run in parallel, because for this they need to be done by
servers again .
Meanwhile, some languages, which began as purely functional solutions,
eventually developed to provide the ability to create just such entities
- processes that exchange messages. Erlang and O'Caml, for example, but this
possibility is based on the imperative functionality of these
languages: monads or an explicit ability to store states. With a pure,
imperative structural approach, there are no problems at all:
Limbo, Ada, or something like IPC on UNIX.
Actually, the appeal is based on this (not only mine, by the way:
http://www.lysator.liu.se/c/pikestyle.html) abandon OOP and FI wherever
possible, and focus on analyzing data changes in
programs, on state properties, and on the logic of changes.
Another
retreat: modern physics considers it existent, and it is intuitively
clear that it preserves properties in the process of changes, without focusing on the
analysis of the dynamics in the program, it is impossible to accurately identify the stored properties, and
therefore objects. Such an analysis leads to more flexible solutions, and to more effective ones.
Interfaces, functions, modules, classes will arise naturally in the process
of program implementation. And they can be reused.