Des robots en goguette
L'exemple de ce paragraphe illustre l'utilisation d'objets ainsi que
la bibliothèque graphique. Nous verrons comment Objective CAML reprend les
notions d'héritage simple, d'héritage multiple, de redéfinition
de méthode et de liaison dynamique. Nous verrons également comment
les classes paramétrées peuvent être mises à profit.
L'application comprend deux catégories principales d'objets : un
monde et des robots. Le monde est un ensemble de cases sur lesquelles
évoluent des robots. Nous aurons plusieures classes de robots.
Chacune d'elles possédera sa propre stratégie de déplacement
dans le monde. Le principe d'interaction du monde et des robots est
ici extrêmement simple. Le monde est entièrement maître du jeu :
il demande tour à tour à chacun des robots qu'il connaît
quelle est sa prochaine position. Chaque robot
détermine sa prochaine position à l'aveugle : il ne
connaît ni la géométrie du monde, ni les autres robots
présents. Si la position demandée par un robot est
légale et libre alors le monde l'y déplace.
Le monde matérialisera l'évolution des robots par une interface.
La complexité (toute relative) de la
conception et du développement de cet exemple est dans la toujours
nécessaire séparation entre un traitement (ici : l'évolution des
robots) et son interface (ici : la trace de cette évolution).
Description générale.
L'application est développée en deux temps.
-
un ensemble de définitions donnant des classes de calcul pur
pour le monde et pour divers robots envisagés.
- un ensemble de définitions, utilisant les précédentes et
ajoutant ce qui est nécessaire à la mise en place d'une
interface. Nous donnerons deux exemples d'interfaces : une
rudimentaire sous forme de texte ; une, plus élaborée, utilisant la
bibliothèque graphique.
Robots << éthérés >>
Dans un premier temps, nous nous intéressons aux robots hors de
toute considération sur l'environnement qui les entourent, c'est à
dire de l'interface qui les affiche.
# class
virtual
robot
(i0:
int)
(j0:
int)
=
object
val
mutable
i
=
i0
val
mutable
j
=
j0
method
get_pos
=
(i,
j)
method
set_pos
(i'
,
j'
)
=
i
<-
i'
;
j
<-
j'
method
virtual
next_pos
:
unit
->
(int
*
int)
end
;;
En toute généralité, un robot est une entité connaissant,
ou croyant connaître, sa position (i et j),
capable de la donner à qui la lui demande (get_pos),
susceptible de modifier cette connaissance si on la lui précise
(set_pos) et sachant décider d'un éventuel mouvement
vers une nouvelle position (next_pos).
Figure 17.9 : hiérarchie de classes des robots purs
Pour améliorer la lisibilité du programme, nous définissons les
mouvements relatifs à une direction absolue :
# type
dir
=
North
|
East
|
South
|
West
|
Nothing
;;
# let
walk
(x,
y)
=
function
North
->
(x,
y+
1
)
|
South
->
(x,
y-
1
)
|
West
->
(x-
1
,
y)
|
East
->
(x+
1
,
y)
|
Nothing
->
(x,
y)
;;
val walk : int * int -> dir -> int * int = <fun>
# let
turn_right
=
function
North
->
East
|
East
->
South
|
South
->
West
|
West
->
North
|
x
->
x
;;
val turn_right : dir -> dir = <fun>
Du schéma induit par la classe virtuelle des robots, nous
définissons quatre espèces de robots distinctes (voir la figure
17.9) en précisant leur manière de se déplacer :
-
les robots fixes qui ne bougent jamais :
# class
fix_robot
i0
j0
=
object
inherit
robot
i0
j0
method
next_pos()
=
(i,
j)
end
;;
- les robots fous qui se déplacent au hasard :
# class
crazy_robot
i0
j0
=
object
inherit
robot
i0
j0
method
next_pos
()
=
(
i+
(Random.int
3
)-
1
,
j+
(Random.int
3
)-
1
)
end
;;
- les robots obstinés qui conservent la même direction tant
qu'ils peuvent avancer,
# class
obstinate_robot
i0
j0
=
object(self)
inherit
robot
i0
j0
val
mutable
wanted_pos
=
(i0,
j0)
val
mutable
dir
=
West
method
private
set_wanted_pos
d
=
wanted_pos
<-
walk
(i,
j)
d
method
private
change_dir
=
dir
<-
turn_right
dir
method
next_pos
()
=
if
(i,
j)
=
wanted_pos
then
let
np
=
walk
(i,
j)
dir
in
(
wanted_pos
<-
np
;
np
)
else
(
self#change_dir
;
wanted_pos
<-
(i,
j)
;
(i,
j)
)
end
;;
- les robots téléguidés qui obéissent à un opérateur
extérieur :
# class
virtual
interactive_robot
i0
j0
=
object(self)
inherit
robot
i0
j0
method
virtual
private
get_move
:
unit
->
dir
method
next_pos
()
=
walk
(i,
j)
(self#get_move
())
end
;;
Le cas du robot interactif est différent des autres car son
comportement est lié à l'interface qui permettra de lui
communiquer des ordres. En attendant, nous nous appuyons sur
une méthode qui, virtuellement, communique cet ordre et en
conséquence la classe interactive_robot demeure
abstraite.
Remarquons que non seulement les quatre classes de robots
spécialisés héritent de la classe robot mais que de
surcroît elles en ont le type. En effet, les seules méthodes que
nous ayons ajoutées sont des méthodes privées et donc
n'apparaissent pas dans le type des instances de ces classes
(voir page ??). Ce point nous est
indispensable si nous souhaitons considérer tous les robots comme
des objets de même type.
Monde pur
Un monde pur est un monde indépendant de l'interface. Il y est
connu l'ensemble des positions qu'un robot est susceptible d'occuper.
Cela prend la forme d'une grille de taille
l×h, d'une méthode is_legal
assurant qu'un couple d'entiers est bien une position dans le monde, et
d'une méthode is_free indiquant si un robot occupe ou non
une position donnée.
En outre, un monde dispose de la liste des robots présents sur sa surface
robots ainsi que d'une méthode add permettant de
faire rentrer de nouveaux robots.
Pour finir, un monde est pourvu de la méthode run lui
permettant de prendre vie.
# class
virtual
[
'robot_type]
world
(l0:
int)
(h0:
int)
=
object(self)
val
l
=
l0
val
h
=
h0
val
mutable
robots
=
(
[]
:
'robot_type
list
)
method
add
r
=
robots
<-
r::robots
method
is_free
p
=
List.for_all
(fun
r
->
r#get_pos
<>
p)
robots
method
virtual
is_legal
:
(int
*
int)
->
bool
method
private
run_robot
r
=
let
p
=
r#next_pos
()
in
if
(self#is_legal
p)
&
(self#is_free
p)
then
r#set_pos
p
method
run
()
=
while
true
do
List.iter
(function
r
->
self#run_robot
r)
robots
done
end
;;
class virtual ['a] world :
int ->
int ->
object
constraint 'a =
< get_pos : int * int; next_pos : unit -> int * int;
set_pos : int * int -> unit; .. >
val h : int
val l : int
val mutable robots : 'a list
method add : 'a -> unit
method is_free : int * int -> bool
method virtual is_legal : int * int -> bool
method run : unit -> unit
method private run_robot : 'a -> unit
end
Le système de type d'Objective CAML ne permet pas de laisser le type des
robots non déterminé (voir page
??). Pour résoudre ce problème, nous
avions la possibilité de restreindre ce type à celui de la classe
robot. Mais dans ce cas, nous nous interdisions de pouvoir
peupler un monde d'autres objets que ceux ayant exactement le même
type que robot. Donc, nous avons choisi de paramétrer la classe
world par le type des robots qui le peuplent. Nous pourrons
ensuite instancier ce paramètre de type par des robots
textuels ou par des robots graphiques.
Robots textuels
Des objets texte
Pour obtenir des robots gérables par une interface texte, nous
définissons la classe des objets textuels (txt_object).
# class
txt_object
(s0:
string)
=
object
val
name
=
s0
method
get_name
=
name
end
;;
Une classe de spécification : les robots textuels abstraits
Par héritage double de robots et txt_object, nous
obtenons la classe abstraite txt_robot des robots textuels.
# class
virtual
txt_robot
i0
j0
=
object
inherit
robot
i0
j0
inherit
txt_object
"Anonymous"
end
;;
class virtual txt_robot :
int ->
int ->
object
val mutable i : int
val mutable j : int
val name : string
method get_name : string
method get_pos : int * int
method virtual next_pos : unit -> int * int
method set_pos : int * int -> unit
end
Cette classe nous sert pour définir un monde à interface texte
(voir page ??). Les habitants de ce monde ne seront
ni des objets de txt_robot (puisque cette classe est abstraite)
ni des héritiers de cette classe. La classe txt_robots est en
quelque sorte une classe de spécification permettant au
compilateur d'identifier les types des méthodes (calcul et
interface) des habitants du monde à interface texte. L'utilisation
d'une telle classe de spécification vient de la séparation que
nous voulons maintenir entre les calculs et l'interface.
Les robots concrets en mode texte
Ils s'obtiennent simplement par double héritage;
la figure 17.10 donne leur hiérarchie de classes.
Figure 17.10 : hiérarchie de classes des robots en mode texte
# class
fix_txt_robot
i0
j0
=
object
inherit
fix_robot
i0
j0
inherit
txt_object
"Fix robot"
end
;;
# class
crazy_txt_robot
i0
j0
=
object
inherit
crazy_robot
i0
j0
inherit
txt_object
"Crazy robot"
end
;;
# class
obstinate_txt_robot
i0
j0
=
object
inherit
obstinate_robot
i0
j0
inherit
txt_object
"Obstinate robot"
end
;;
Les robots interactifs doivent pour devenir concrets
définir leur méthode d'interaction avec l'utilisateur.
# class
interactive_txt_robot
i0
j0
=
object
inherit
interactive_robot
i0
j0
inherit
txt_object
"Interactive robot"
method
private
get_move
()
=
print_string
"Which dir : (n)orth (e)ast (s)outh (w)est ? "
;
match
read_line()
with
"n"
->
North
|
"s"
->
South
|
"e"
->
East
|
"w"
->
West
|
_
->
Nothing
end
;;
Monde textuel
Le monde à interface texte se dérive du monde pur par
-
héritage de la classe générique world en
instanciant son paramètre de type par la classe de spécification
txt_robot,
- redéfinition de la méthode run pour y inclure
les différents affichages textuels.
# class
virtual
txt_world
(l0:
int)
(h0:
int)
=
object(self)
inherit
[
txt_robot]
world
l0
h0
as
super
method
private
display_robot_pos
r
=
let
(i,
j)
=
r#get_pos
in
Printf.printf
"(%d,%d)"
i
j
method
private
run_robot
r
=
let
p
=
r#next_pos
()
in
if
(self#is_legal
p)
&
(self#is_free
p)
then
begin
Printf.printf
"%s is moving from "
r#get_name
;
self#display_robot_pos
r
;
print_string
" to "
;
r#set_pos
p;
self#display_robot_pos
r
;
end
else
begin
Printf.printf
"%s is staying at "
r#get_name
;
self#display_robot_pos
r
end
;
print_newline
()
;
print_string"next - "
;
ignore
(read_line())
method
run
()
=
let
print_robot
r
=
Printf.printf
"%s is at "
r#get_name
;
self#display_robot_pos
r
;
print_newline
()
in
print_string
"Initial state :\n"
;
List.iter
print_robot
robots;
print_string
"Running :\n"
;
super#run()
(* 1 *)
end
;;
Nous attirons l'attention du lecteur sur l'appel à la méthode
run de la classe ancêtre (marqué (* 1 *)
dans le
code) dans la redéfinition de cette
même méthode. Nous avons là une illustration des deux types de
liaison des méthodes possibles : statique ou dynamique (voir page
??). L'appel à super#run
est statique; c'est l'intérêt de nommer la superclasse que de
pouvoir appeler ses méthodes alors qu'elles ont été redéfinies.
Par contre, dans cette méthode super#run se trouve un
appel à self#run_robot. C'est ici une liaison dynamique
qui a lieu; c'est la méthode définie dans la classe
txt_world qui est exécutée et non celle de
world, sans quoi nous n'obtiendrions aucun affichage.
Le monde plan rectangulaire textuel
s'obtient en implantant la
dernière méthode encore abstraite : is_legal.
# class
closed_txt_world
l0
h0
=
object(self)
inherit
txt_world
l0
h0
method
is_legal
(i,
j)
=
(0
<=
i)
&
(i<
l)
&
(0
<=
j)
&
(j<
h)
end
;;
Figure 17.11 : hiérarchie de classes du monde plan rectangulaire en mode texte
On peut procéder à un petit essai en tapant :
let
w
=
new
closed_txt_world
5
5
and
r1
=
new
fix_txt_robot
3
3
and
r2
=
new
crazy_txt_robot
2
2
and
r3
=
new
obstinate_txt_robot
1
1
and
r4
=
new
interactive_txt_robot
0
0
in
w#add
r1;
w#add
r2;
w#add
r3;
w#add
r4;
w#run
()
;;
Nous allons passer à présent à la réalisation de l'interface
graphique pour notre monde de robots. En fin de course, nous
obtiendrons une application ayant l'apparence de la figure
17.12.
Figure 17.12 : Le monde graphique des robots
Robots graphiques
Nous obtenons des robots en mode graphique en suivant le même
schéma que le mode texte :
-
définition d'un objet graphique générique,
- définition d'une classe abstraite de robots graphiques par
double héritage des robots et des objets graphiques
(analogue de la classe de spécification du
paragraphe 17),
- définition par double héritage des robots possédant un
comportement particulier.
Objets graphiques génériques
Un objet graphique simple est un objet possédant une méthode
display qui prend en argument les coordonnées d'un pixel
et s'affiche.
# class
virtual
graph_object
=
object
method
virtual
display
:
int
->
int
->
unit
end
;;
De cette spécification, il est possible de tirer des objets
graphiques extrêmement complexes. Nous allons nous contenter ici
d'une classe graph_item affichant le bitmap qui sert à la
construire.
# class
graph_item
x
y
im
=
object
(self)
val
size_box_x
=
x
val
size_box_y
=
y
val
bitmap
=
im
val
mutable
last
=
None
method
private
erase
=
match
last
with
Some
(x,
y,
img)
->
Graphics.draw_image
img
x
y
|
None
->
()
method
private
draw
i
j
=
Graphics.draw_image
bitmap
i
j
method
private
keep
i
j
=
last
<-
Some
(i,
j,
Graphics.get_image
i
j
size_box_x
size_box_y)
;
method
display
i
j
=
match
last
with
Some
(x,
y,
img)
->
if
x<>
i
||
y<>
j
then
(
self#erase
;
self#keep
i
j
;
self#draw
i
j
)
|
None
->
(
self#keep
i
j
;
self#draw
i
j
)
end
;;
Un objet graph_item conserve la portion d'image sur laquelle
il est affiché pour la restaurer lors de l'affichage suivant. De
plus, si l'image n'a pas bougé elle n'est pas réaffichée.
# let
foo_bitmap
=
[|[|
Graphics.black
|]|]
;;
# class
square_item
x
col
=
object
inherit
graph_item
x
x
(Graphics.make_image
foo_bitmap)
method
private
draw
i
j
=
Graphics.set_color
col
;
Graphics.fill_rect
(i+
1
)
(j+
1
)
(x-
2
)
(x-
2
)
end
;;
# class
disk_item
r
col
=
object
inherit
graph_item
(2
*
r)
(2
*
r)
(Graphics.make_image
foo_bitmap)
method
private
draw
i
j
=
Graphics.set_color
col
;
Graphics.fill_circle
(i+
r)
(j+
r)
(r-
2
)
end
;;
# class
file_bitmap_item
name
=
let
ch
=
open_in
name
in
let
x
=
Marshal.from_channel
ch
in
let
y
=
Marshal.from_channel
ch
in
let
im
=
Marshal.from_channel
ch
in
let
()
=
close_in
ch
in
object
inherit
graph_item
x
y
(Graphics.make_image
im)
end
;;
Nous avons spécialisé les graph_item en carrés, disques
et bitmaps lus depuis un fichier.
Le robot graphique abstrait
est à la fois un robot
et un objet graphique.
# class
virtual
graph_robot
i0
j0
=
object
inherit
robot
i0
j0
inherit
graph_object
end
;;
Les robots graphiques fixes, fous et obstinés
sont
des objets graphiques spécialisés.
# class
fix_graph_robot
i0
j0
=
object
inherit
fix_robot
i0
j0
inherit
disk_item
7
Graphics.green
end
;;
# class
crazy_graph_robot
i0
j0
=
object
inherit
crazy_robot
i0
j0
inherit
file_bitmap_item
"crazy_bitmap"
end
;;
# class
obstinate_graph_robot
i0
j0
=
object
inherit
obstinate_robot
i0
j0
inherit
square_item
1
5
Graphics.black
end
;;
Le robot graphique interactif
utilise les primitives key_pressed et read_key du module
Graphics pour l'acquisition du déplacement. On reconnaîtra
les touches 8, 6, 2 et 4 du pavé
numérique (touche NumLock
active). De cette façon,
l'utilisateur n'est pas obligé de donner une indication de
déplacement à chaque interrogation du monde.
# class
interactive_graph_robot
i0
j0
=
object
inherit
interactive_robot
i0
j0
inherit
file_bitmap_item
"interactive_bitmap"
method
private
get_move
()
=
if
not
(Graphics.key_pressed
())
then
Nothing
else
match
Graphics.read_key()
with
'8'
->
North
|
'2'
->
South
|
'4'
->
West
|
'6'
->
East
|
_
->
Nothing
end
;;
Monde graphique
On obtient un monde à interface graphique par héritage du monde pur
en instanciant le paramètre 'a_robot
avec la classe abstraite
des robots graphiques graph_robot. Comme pour le monde
en mode texte, le monde graphique redéfinit la méthode
run_robot de traitement d'un robot et la méthode d'activation
générale run.
# let
delay
x
=
let
t
=
Sys.time
()
in
while
(Sys.time
())
-.
t
<
x
do
()
done
;;
# class
virtual
graph_world
l0
h0
=
object(self)
inherit
[
graph_robot]
world
l0
h0
as
super
initializer
let
gl
=
(l+
2
)*
1
5
and
gh
=
(h+
2
)*
1
5
and
lw=
7
and
cw=
7
in
Graphics.open_graph
(" "
^
(string_of_int
gl)^
"x"
^
(string_of_int
gh))
;
Graphics.set_color
(Graphics.rgb
1
7
0
1
7
0
1
7
0
)
;
Graphics.fill_rect
0
lw
gl
lw
;
Graphics.fill_rect
(gl-
2
*
lw)
0
lw
gh
;
Graphics.fill_rect
0
(gh-
2
*
cw)
gl
cw
;
Graphics.fill_rect
lw
0
lw
gh
method
run_robot
r
=
let
p
=
r#next_pos
()
in
delay
0
.
0
0
1
;
if
(self#is_legal
p)
&
(self#is_free
p)
then
(
r#set_pos
p
;
self#display_robot
r)
method
display_robot
r
=
let
(i,
j)
=
r#get_pos
in
r#display
(i*
1
5
+
1
5
)
(j*
1
5
+
1
5
)
method
run()
=
List.iter
self#display_robot
robots
;
super#run()
end
;;
Notez que la fenêtre graphique est créée à l'initialisation
d'un objet de cette classe.
Le monde plan rectangulaire et graphique
s'obtient
de la même manière que pour le monde plan rectangulaire et textuel.
# class
closed_graph_world
l0
h0
=
object(self)
inherit
graph_world
l0
h0
method
is_legal
(i,
j)
=
(0
<=
i)
&
(i<
l)
&
(0
<=
j)
&
(j<
h)
end
;;
class closed_graph_world :
int ->
int ->
object
val h : int
val l : int
val mutable robots : graph_robot list
method add : graph_robot -> unit
method display_robot : graph_robot -> unit
method is_free : int * int -> bool
method is_legal : int * int -> bool
method run : unit -> unit
method run_robot : graph_robot -> unit
end
On peut alors tester l'application graphique en tapant
let
w
=
new
closed_graph_world
1
0
1
0
;;
w#add
(new
fix_graph_robot
3
3
)
;;
w#add
(new
crazy_graph_robot
2
2
)
;;
w#add
(new
obstinate_graph_robot
1
1
)
;;
w#add
(new
interactive_graph_robot
5
5
)
;;
w#run
()
;;
Pour en faire plus
L'implantation de la méthode run_robot des
différents mondes sous-entend que les robots sont potentiellement
capables de se rendre en tout point du monde du moment que celui-ci est
libre et légal. De plus, rien n'interdit à un robot de modifier sa
position sans en prévenir le monde. Une amélioration possible
consiste à faire gérer l'ensemble des positions des robots par le
monde; lors du déplacement d'un robot, le monde vérifie d'une part
si la nouvelle position est légale mais aussi si elle constitue un
déplacement autorisé. Dans ce cas, le robot devra être capable
de demander au monde sa propre position; ce qui entraîne que la
classe des robots devra être dépendante de la classe du monde. On
pourra définir une classe robot prenant comme paramètre de type
une classe de monde.
Cette modification permet alors de définir des robots capables
d'interroger le monde qui les entoure et donc de se comporter en
fonction de celui-ci. Nous pourrons réaliser des robots qui suivent
ou qui fuient d'autres robots, qui tentent de les bloquer, etc.
L'étape suivante est de permettre aux robots de communiquer entre
eux pour s'échanger des informations et constituer ainsi des
équipes de robots.
Les chapitres de la partie suivante de l'ouvrage permettent de
libérer l'exécution des robots les unes des autres : soit en ayant
recours aux Threads (voir page ??) pour que
chacun s'exécute sur un processus distinct, soit en profitant des
possibilités de l'informatique distribuée (voir page
??) pour que les robots soient des clients
s'exécutant sur des machines distantes qui annoncent leur
déplacement ou demandent des informations à un monde qui serait un
serveur. Ce problème est traité à la page ??.