J'ai commencé un premier essai en Java pour appliquer l'ECS mais je suis arrivé à un hic.
Si j'applique un ECS complet, je crée des composants, des systèmes et des entités. Les entités contiennent différents composants pour définir ce qu'elles sont et les systèmes agissent sur ces entités en modifiant ces composants.
Les systèmes représentent les comportements, les composants les caractéristiques d'une entité et l'entité… l'entité.
Mais pour les comportements reproducteurs, par exemple, je n'arrive pas à trouver de solution viable.
Imaginons une entité qui contient le composant HermaphroditeOpportuniste. Le système ComportementReproducteur doit comprendre qu'il s'agit d'une entité hermaphrodite opportuniste.
1er hic : dans le système ComportementReproducteur, je ne vois pas comment faire autrement qu'avec une série de conditions pour faire reproduire l'entité en fonction de son composant de reproduction (ici HermaphroditeOpportuniste). Si j'ajoute un nouveau type de reproduction, j'ajoute une nouvelle condition avec le code correspondant dans la méthode evolve() (ou update(), ou compute(), fin bref, nommez-là comme vous voulez). On ne peut donc pas vraiment ajouter un nouveau type de reproduction de façon simple et efficace.
2e hic : si j'utilise une suite de condition (ou un switch()), j'aurais la logique de tous les différents type de reproduction dans une seule et même méthode, qui contiendra plus de code à chaque nouveau type de reproduction. C'est donc pas très viable.
Je dois avoir un problème dans mon interprétation de l'ECS, peut-être arriverez-vous à m'éclairer.
La solution que j'ai prise pour l'instant est de contenir les systèmes (les comportements) dans l'entité et de ne pas utiliser de composants. Chaque entité contient donc une instance de ComportementReproducteur ou ComportementAlimentaire, par exemple. Ce qui est en fait le pattern Strategy décrit dans le tuto Java, mais j'aimerais réussir à appliquer un ECS complet.
Et une petite question : l'ajout d'une entité en cours de route (dans un bête menu console à chaque tour) doit se faire avant de gérer l'évolution des entités déjà existantes, ou l'inverse ?
Je pense que trop peu de personne maîtrisent vraiment le sujet pour pouvoir vraiment te répondre. Cependant je peux essayer de te répondre un peu
1er et 2e hic :
Il s'agit là d'un problème de conception indépendant de la modélisation ECS. Pour moi il faudrait que tu utilises le polymorphisme (que ce soit du pointeur de fonction, de l'héritage on s'en fiche). En fait, même si tu suis un ECS, les systèmes sont (pour moi) ni plus ni moins que des programmes objets standards. Si tu as un système de reproduction, il s'agit (encore pour moi hein, pense pas que c'est absolument comme ça) en réalité d'une façade) simplifié qui va faire les traitements de création de poisson (ici c'est une factory ou ses variantes), de choix d'algorithme (le pattern strategy) et un peu de polymorphisme, ce qui va te permettre de gérer de manière propre toute tes façons de reproduire, tout en créant les poissons qu'il faut. Comme une classe ne doit avoir qu'une fonction, les systèmes sont (toujours imho) des classes qui cachent tout un tas de classe qui elles n'ont qu'un seul rôle.
Bon, je m'ennuyais un peu et je me prenais la tête avec l'orienté-objet en Fortran, quand je suis retombé sur cet exercice. Voici donc un début d'implémentation du Fortraquarium !
J'utilise le standard F2003, qui a justement ajouté plein de choses cool pour faire de l'OO. Ce code compile avec gfortran 4.9, et sans doutes d'autres compilateurs, que je n'ai pas sous la main. La partie 1.1 (remplissage et affichage) m'a pris 20min.
Le seul problème est que les tableaux Fortran ont une taille fixe, sauf à jouer avec des allocate/deallocate. Donc pour l'instant j'ai une constante en dur qui fixe le maximum de poisson/algue dans un aquarium. On verra si j'ai la foi de refaire un bout de liste chainée un jour.
EDIT: j'ai rien dit, on peut utiliser move_alloc pour faire croitre un tableau.
Bonjour, j'ai fais un projet dans le cadre de mes cours et je me suis très fortement inspiré de cette exercice. Je ferais d'ailleurs un poste d'ici peu
Je dois faire un dossier et je veux parler de cet exercice, ce qui parait évident. J'aimerais donc avoir quelques infos dessus que je n'ai pas trouvé.. Premièrement l'auteur s'il y en a un ^^. Et à celui (ou ceux) qui ont créé l'exercice, quel était le but ?
Dernière petite question aux membres cette fois, qu'avez vous appris en le faisant, si vous avez appris quelque chose ?
L'auteur, c'est moi. Le but était d'avoir un exercice Java qui forcerait plus ou moins à utiliser les objets correctement, puis qui montrerait que les objets ne permettent pas forcément de tout faire facilement (surtout avec les limitations de Java).
J'ai justement eu à monter un cours OO au boulot, et merci !!
Je ne vous dis pas à quel point il est difficile de trouver des exemples/exercices OO qui ne soient pas des exos de modélisations de BdDs (genre, de quoi sont foutus livres, auteurs, client, bibliothèques).
Personne ne l'a vraiment terminé avec l'ECS. Je me suis au départ lancé là-dedans mais depuis j'ai arrêté et suis passé à autre chose. Je pense pas prendre le temps pour l'instant mais peut-être que j'y retournerai un jour.
L'ECS permet de repousser les limites de l'objet (et c'est aussi valable pour le fonctionnel) quand on a besoin d'avoir des comportements extrêmement variés, composables et changeant sur des objets "équivalents". Le truc c'est que tout ce qui était implicite (typiquement la résolution de type pour l'appel de fonctions) ne l'est plus et donc c'est à toi d'implémenter tout la mécanique derrière.
Ce qui prend finalement du temps, ce n'est pas modéliser le problème pour l'ECS (ça, c'est quasiment simple), non le problème c'est d'implémenter un ECS générique et flexible.
J'ai récemment découvert un langage, Pony, et j'ai réalisé le Javaquarium pour prendre en main la partie objet. A priori, peu d'entre vous connaissent ce langage, une petite présentation s'impose donc.
Pony est un langage orienté acteur (à la manière d'Erlang), avec une killer feature nommée « reference capabilities ». Pour résumer, c'est un attribut qui définit ce qu'il est possible de faire avec la référence, et quels sont les alias de la référence qui peuvent exister. Quelques exemples :
R est une référence box dans un acteur A
R peut être utilisée uniquement en lecture
Des alias de R en lecture et en écriture peuvent exister, mais uniquement dans A
R est une référence iso dans un acteur A
R peut être utilisée en lecture et en écriture
Aucun alias de R ne peut exister
R est une référence val dans un acteur A
R peut être utilisée uniquement en lecture
Des alias de R en lecture peuvent exister dans d'autres acteurs
Tout cela permet de garantir l'absence de data races à la compilation (une preuve mathématique est en cours de réalisation). Ce système de capabilities est obscur au début, je vous conseille de regarder les conférences faites par l'auteur du langage (liens sur le site), il explique vachement bien.
Pony offre aussi des garanties sur la validité des objets (pas de références nulles), les exceptions, etc.
Avec tout ça, on pourrait penser que le langage est hyper restrictif et inutilisable en pratique, mais il n'en est rien. On a accès à des éléments extrêmement flexibles et expressifs. Par exemple, l'héritage n'existe pas en Pony. A la place, un système de composition de types associé à du pattern matching permet de faire du polymorphisme.
Bref, vous l'aurez probablement compris, mon impression actuelle du langage ressemble à peu près à ceci :
Voici donc le Javaquarium en Pony, accompagné de commentaires à propos des particularités du langage. Je n'ai pas utilisé d'acteurs, je ne parle donc pas de ceux-ci dans les commentaires.
use "collections"
use "promises"
use "random"
use "term"
use "time"
// Une primitive est similaire à une classe, mais ne peut pas contenir
// de variables membres.
// De plus, toutes les instances d'une primitive ont la même identité
// (toutes les références référencent le même objet)
// Les variables et fonctions globales sont interdites en Pony.
// On utilise des primitives pour représenter des constantes
primitive HPAtBirth fun apply(): U64 => 10
primitive MaxAge fun apply(): U64 => 20
primitive MassPerSeaweed fun apply(): U64 => 10
primitive SeaweedGrowth fun apply(): U64 => 1
primitive HungerDamage fun apply(): U64 => 1
primitive HungerThreshold fun apply(): U64 => 5
primitive HerbHeal fun apply(): U64 => 3
primitive CarnHeal fun apply(): U64 => 5
primitive HerbDamage fun apply(): U64 => 2
primitive CarnDamage fun apply(): U64 => 4
primitive MaxSeaweedMass fun apply(): U64 => 50000
primitive MaxFish fun apply(): U64 => 100
// Définition des espèces de poissons
primitive Flounder
primitive Bass
primitive Carp
// Union de types. Un herbivore est une sole ou un bar ou une carpe
type Herbivorous is (Flounder | Bass | Carp)
primitive Grouper
primitive Tuna
primitive ClownFish
type Carnivorous is (Grouper | Tuna | ClownFish)
type Species is (Herbivorous | Carnivorous)
type Mono is (Carp | Tuna)
type Sequential is (Bass | Grouper)
type Opportunistic is (Flounder | ClownFish)
// Une classe
class Fish
// Variables membres, ou champs.
// Un champ est déclaré sous la forme
// var/let nom: Type (= initialiseur)
// var désigne une référence réassignable, let une référence fixe
var _name: String
var _is_male: Bool
let _specie: Species
// Sucre syntaxique : Type() <=> Type.create().apply()
var _health: U64 = HPAtBirth()
var _age: U64 = 0
let _aquarium: Aquarium
// Constructeur
// On doit obligatoirement initialiser tous les champs qui n'ont pas d'initialiseur par défaut
new create(nm: String, sp: Species, aq: Aquarium, ml: Bool) =>
_name = nm
_specie = sp
_aquarium = aq
// Pattern matching. On peut matcher sur des valeurs, des types ou les deux.
match _specie
| let l: Sequential => _is_male = true
else
_is_male = ml
end
// ref est une des 6 "reference capabilities"
// Pour simplifier, une méthode déclarée ref ne peut être appelée que sur des objets mutables
fun ref update() =>
_health = _health - _health.min(1)
_age = _age + 1
// x == y <=> identité structurale (les champs des objets ont la même valeur)
if _age == (MaxAge() / 2) then
match _specie
| let l: Sequential => _is_male = false
end
end
if dead() then
return
end
if _health <= HungerThreshold() then
let hp =
match _specie
// En Pony, tout est expression, et toute expression retourne une valeur
// Un if retourne la dernière expression dans son corps
| let l: Herbivorous => if _aquarium.eat_seaweeds() then HerbHeal() else 0 end
| let l: Carnivorous => if _aquarium.eat_fish(_specie) then CarnHeal() else 0 end
else
0
end
_health = _health + hp
else
// Bloc pouvant lever une exception
// Les exceptions de Pony n'ont pas de type dynamique.
// Elles doivent obligatoirement être attrapées
try
let partner = _aquarium.search_partner()
// x is y <=> égalité identitaire (les références désignent le même objet)
if (this is partner) or not (_specie is partner.specie()) then
return
end
match _specie
| let l: Opportunistic => if male() == partner.male() then _is_male = not _is_male end
else
if male() == partner.male() then
return
end
end
_aquarium.birth(_specie)
end
end
fun ref damage() =>
_health = if _health <= CarnDamage() then 0 else _health - CarnDamage() end
// Une fonction qui retourne une valeur retourne également la dernière expression dans son corps
fun name(): String => _name
fun ref rename(new_name: String) => _name = new_name
fun male(): Bool => _is_male
fun specie(): Species => _specie
fun dead(): Bool => (_health == 0) or (_age >= MaxAge())
fun print(out: StdStream) =>
out.print(_name + ", " + if _is_male then "male " else "female " end +
match _specie
| Flounder => "flounder"
| Bass => "bass"
| Carp => "carp"
| Grouper => "grouper"
| Tuna => "tuna"
| ClownFish => "clown fish"
else
"unknown"
end +
", " + _health.string() + " hp, age " + _age.string()
)
class Aquarium
var _seaweed_mass: U64 = 0
let _fishs: Array[Fish] = Array[Fish]
let _births: Array[Fish] = Array[Fish]
let _deads: Array[String] = Array[String]
var _turn: U64 = 0
var _i: U64 = 0
var _should_advance: Bool = true
let _rand: Dice
var _anon_count: U64 = 0
new create() =>
let tm = Time.now()
let seed: U64 = (tm._1 xor tm._2).u64()
_rand = Dice(MT(seed))
fun ref add_seaweeds(count: U64) =>
_seaweed_mass = (_seaweed_mass + (MassPerSeaweed() * count)).min(MaxSeaweedMass())
fun ref remove_seaweeds(count: U64) =>
_seaweed_mass = if _seaweed_mass <= (MassPerSeaweed() * count) then
0
else
_seaweed_mass - (MassPerSeaweed() * count)
end
fun ref add_fish(name: String, sp: Species, male: Bool = false) =>
_fishs.push(Fish(name, sp, this, male))
// Le "?" indique une fonction partielle. Une exception peut sortir de la fonction
fun ref remove_fish(idx: U64) ? =>
_fishs.delete(idx)
fun ref rename_fish(idx : U64, new_name: String) ? =>
// Sucre syntaxique : objet(foo) <=> objet.apply(foo)
_fishs(idx).rename(new_name)
fun ref birth(sp: Species) =>
if (_fishs.size() + _births.size()) < MaxFish() then
_anon_count = _anon_count + 1
_births.push(Fish("Anonymous" + _anon_count.string(), sp, this, (_rand(1, 2) - 1) == 0))
end
fun ref update() =>
_deads.clear()
_births.clear()
_seaweed_mass = (_seaweed_mass + (SeaweedGrowth() * (_seaweed_mass / MassPerSeaweed()))).min(MaxSeaweedMass())
_i = 0
while _i < _fishs.size() do
try
_fishs(_i).update()
if _fishs(_i).dead() then
_update_on_death(_i)
end
end
// Un assignement retourne l'ancienne valeur de la variable
if _should_advance = true then
_i = _i + 1
end
end
for bir in _births.values() do
_fishs.push(bir)
end
_turn = _turn + 1
fun ref eat_seaweeds(): Bool =>
if _seaweed_mass >= HerbDamage() then
_seaweed_mass = _seaweed_mass - HerbDamage()
true
else
false
end
fun ref eat_fish(specie: Species): Bool =>
let i = _rand(1, _fishs.size()) - 1
try
if _fishs(i).specie() is specie then
false
else
_fishs(i).damage()
if _fishs(i).dead() then
_update_on_death(i)
end
true
end
else
false
end
fun ref search_partner(): Fish ? =>
let i = _rand(1, _fishs.size()) - 1
_fishs(i)
fun ref _update_on_death(i: U64) =>
try
_deads.push(_fishs(i).name())
_fishs.delete(i)
end
_should_advance = i > _i
fun print(out: StdStream) =>
out.print("Turn " + _turn.string())
out.print("Seaweeds : " + (_seaweed_mass / MassPerSeaweed()).string() + " (Total mass : " + _seaweed_mass.string() + ")")
out.print("Fishs :")
var i = U32(0)
for fish in _fishs.values() do
out.write("#" + i.string() + " ")
fish.print(out)
i = i + 1
end
if _births.size() > 0 then
out.print("")
for bir in _births.values() do
out.print(bir.name() + " is born!")
end
end
if _deads.size() > 0 then
out.print("")
for dead in _deads.values() do
out.print(dead + " is dead!")
end
end
out.print("")
// Les entrées/sorties sont asynchrones en Pony.
// Cette classe gère les entrées console
class CommandHandler is ReadlineNotify
let _commands: Array[String] = Array[String]
let _aq: Aquarium
let _out: StdStream
new create(aq: Aquarium, out: StdStream) =>
_commands.push("quit")
_commands.push("print")
_commands.push("update")
_commands.push("add")
_commands.push("kill")
_commands.push("rename")
_aq = aq
_out = out
fun ref apply(line: String, prompt: Promise[String]) =>
// recover permet de changer la capability d'une expression.
// Il y a bien sûr des restrictions sur les possibilités.
// Ici, on transforme un iso (référence unique) en ref (référence pouvant avoir des alias)
let args = recover ref line.split() end
var i = U64(0)
while i < args.size() do
try
if args(i) == "" then
args.delete(i)
else
i = i + 1
end
end
end
try
match args(0)
| "quit" => prompt.reject()
| "print" => _aq.print(_out)
| "update" => _aq.update(); _aq.print(_out)
| "add" => _handle_add(args)
| "kill" => _aq.remove_fish(args(1).u64())
| "rename" => _aq.rename_fish(args(1).u64(), args(2))
else
error
end
else
_out.print("Invalid command")
end
prompt("> ")
fun ref tab(line: String): Seq[String] box =>
let r = Array[String]
for command in _commands.values() do
if command.at(line, 0) then
r.push(command)
end
end
r
fun ref _handle_add(args: Array[String]) ? =>
match args(1)
| "fish" =>
let name = args(2)
let specie = match args(3)
| "flounder" => Flounder
| "bass" => Bass
| "carp" => Carp
| "grouper" => Grouper
| "tuna" => Tuna
| "clownfish" => ClownFish
else
error
end
let sex = match args(4)
| "male" => true
| "female" => false
else
error
end
_aq.add_fish(name, specie, sex)
| "seaweeds" =>
let count = args(2).i64()
if count >= 0 then _aq.add_seaweeds(count.abs()) else _aq.remove_seaweeds(count.abs()) end
else
error
end
// Le programme commence dans le constructeur de l'acteur Main
actor Main
// env contient des éléments comme les arguments du programme, les flux d'entrée/sortie standard, etc
new create(env: Env) =>
env.out.print("Use 'quit' to quit")
// On va transmettre ces objets à un autre acteur, on doit donc créer des références uniques.
let aq = recover Aquarium end
let term = recover ANSITerm(Readline(recover CommandHandler(consume aq, env.out) end, env.out), env.input) end
term.prompt("> ")
let notif = recover
// Déclaration d'objet anonyme
object is StdinNotify
let term: ANSITerm = consume term
fun ref apply(data: Array[U8] iso) =>
term(consume data)
fun ref dispose() =>
term.dispose()
end
end
env.input(consume notif)
Ben, le problème c'est que dans tous leurs papiers la preuve est mentionnée dans les "future works" et que si preuve il y a, c'est difficile de la trouver (et pas de précision non plus sur le moyen de preuve utilisé).
Effectivement, j'ai relu les papiers, la question des data races n'est pas prouvée. J'ai été un peu trop enthousiaste.
Néanmoins, le modèle est cohérent en pratique (et délicieux à utiliser). Certes, la différence entre théorie et pratique est variable (surtout en preuve), donc je n'irai pas jusqu'à dire « y'a plus qu'à », mais ça me semble en très bonne voie.
Ouais mais du coup ça me paraissait bizarre . J'ai pu rencontrer des doctorants/profs qui bossent sur différents projets plus ou moins liés (à une école d'été cette année). Entre autres le langage Encore et le GC Orca qui doit venir s'intégrer à Pony. Et ils n'avaient pas parlé de preuve complète et encore moins de preuve outillées (Coq, Isabelle, etc …).
En fait le plus casse-pied c'est pas tant l'héritage, le stratégies (régime alimentaire, reproduction) ou quoi, c'est d'arriver à trouver un moyen élégant d'éviter les concurrent modification exceptions
On a envie de juste parcourir une liste d'être-vivants et de leur dire "vivez votre vie". Dans le même temps, on aimerait bien que quand ils font qqch en rapport avec l'aquarium, ça mette à jour l'aquarium en conséquence (typiquement qu'il se nettoie des cadavres tout seul, que les petits enfants s'ajoutent instantanément).
Ça oblige à "cloner" les listes de poissons/plantes au début pour travailler sur la liste telle qu'elle était lorsque le tour a démarré.
J'trouve ça un peu moche
EDIT : mais en même temps ça a du sens… On fait évoluer les poissons qu'on avait au départ. Mais bon c'est juste en termes de code que fishes.each {} c'est plus joli que fishes.findAll{}.each{}
EDIT² : Autre truc extrêmement casse-pied, c'est l'aspect aléatoire qui ne permet pas d'écrire de petits tests unitaires. Du coup on est obligé de mettre la partie tirage aléatoire à part pour pouvoir écrire des tests déterministes (style : un mérou ne peut pas manger une sole).
Connectez-vous pour pouvoir poster un message.
Connexion
Pas encore membre ?
Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte