[Langage] Jade
| Mod |
Posté le 28 Août 2011 à 23:24
|
|
![]() Messages : 4954 GCPoints : 2100823 |
@SEB : à part les types de base, tout est pointeur, oui, c'est la condition sine qua none au passage par référence de tous les objets. La méthode que tu cites, si je l'ai bien comprise, pose le même soucis que celle du C++. Jade étant un langage compilé, il est soumis aux contraintes du code machine, à savoir que la façon d'accéder à une valeur n'est pas la même que la façon d'accéder à un objet, et que la façon d'accéder à un entier n'est pas la même que d'accéder à un byte (taille différente) ou un float (instructions ASM différentes). Dès lors, les façons de re-typer sont aussi différentes. Pour un langage interprété cela ne poserait pas de problème (c'est ce que fait Java), mais pour un langage compilé, soit il faut compiler un template pour chaque type (considérant qu'il reste possible de factoriser les types possédant la même taille sous une même compilation), soit trouver autre chose. Je dois avouer qu'après quelques heures à réfléchir sur le sujet, il s'avère que c'est assez corsé. A part inventer une généricité qui n'existe pas encore, pas moyen d'éviter la compilation pour chaque type contenu >_<. @Darktib : eh, eh, content que ça passe ! Ca n'est pas quelque chose de coûteux en temps processeur, et c'est très pratique à l'usage, donc si ça peut être bien perçu, tant mieux :) Et désolé pour la taille des posts, c'est assez difficile d'être à la fois précis et concis, surtout quand du code d'illustration vient s'intercaler |
|
| Mod |
Posté le 31 Août 2011 à 13:21
|
|
![]() Messages : 4954 GCPoints : 2100823 |
Pour ce que cela intéresserait d'avoir d'autres points de vue sur le langage que ceux évoqués ici, j'ai posté une présentation du projet sur le Site du Zéro : http://www.siteduzero.com/forum-83-684543-p1-langage-jade.html. Par ailleurs, le message en tête de sujet est à jour.
Dernière édition le 31 Août 2011 à 13:24
|
|
| Mod |
Posté le 01 Sep 2011 à 17:30
|
|
![]() Messages : 4954 GCPoints : 2100823 |
Hop, quelques nouvelles. Cela fait maintenant plusieurs jours que je travaille en semaine - lorsque j'ai le temps, donc en pause midi ^^ - sur l'IDE qui sera présent de base avec Jade. Rien d'exceptionnel pour le moment, un simple éditeur de texte avec coloration syntaxique, mais que j'essaie de rendre des plus pratiques grâce à des petits plus tels que l'indentation automatique lors du retour à la ligne, l'auto-complétion des structures (par exemple écrire "class Test" puis effectuer un retour à la ligne indente la nouvelle ligne, et complète la structure par un "end"), etc. J'ai aussi regardé du côté de la background compilation. La background compilation étant la capacité d'un IDE de compiler le code en même temps qu'il est écrit et d'en récupérer la sortie d'erreur. Ca peut s'avérer très pratique, puisque l'on peut corriger son code au fur et à mesure de son écriture (avec un décalage tout de même, suivant la taille du projet). C'est utilisé dans un certain nombre d'IDE, notamment Visual Studio lorsqu'il est utilisé pour les langages .Net. L'IDE intègrera également la gestion de projet, l'auto-complétion, les signets. Outre ce qui a été cité, quelles fonctions vous intéresseraient dans cet IDE ? |
|
| Darktib |
Posté le 05 Sep 2011 à 11:39
|
|
![]() Messages : 4017 GCPoints : 347288 |
C'est une bonne idée. J'ai regardé le topic du sdz, je pense que tout ce que j'aime dans un IDE à déjà été dit là-bas. J'ai cependant plusieurs questions: - Pourquoi ne pas faire un plugin pour Eclipse ou Code::Blocks (peut être plus rapide à faire) ? - Avec quelle bibliothèque coderas-tu l'IDE ? Qt ? Bonne chance! |
|
| Mod |
Posté le 05 Sep 2011 à 14:00
|
|
![]() Messages : 4954 GCPoints : 2100823 |
L'IDE est codé en C#, sachant que j'essaie de le garder au niveau de .Net 2.0, pour bénéficier de la compatibilité Mono. Développer un plugin pour une autre IDE aurait été beaucoup moins intéressant techniquement et pas forcément plus rapide avec l'obligation de comprendre le fonctionnement des plugins au sein de ces IDE (et la lenteur d'Eclipse à chaque utilisation, non merci ^^). | |
| Mod |
Posté le 21 Sep 2011 à 20:27
|
|
![]() Messages : 4954 GCPoints : 2100823 |
Update ! J'ai été assez saturé de choses à faire ces derniers jours. Je prends toujours un certain temps pour poster convenablement (ce qui finit généralement en pavé ^^'), et je n'avais malheureusement que peu de pauses à accorder à ce sujet. Il m'a du coup fallut plusieurs jours pour rédiger ce qui suit, à coups de petites touches. Ceux intéressés par les méthodes théoriques entrant dans la conception du langage liront sans doute dans le détail, les autres passeront au-dessus. S'agissant là d'une réflexion sur l'orientation objet, je pense que ça peut toujours être intéressant à lire pour quiconque s'intéresse au sujet. Sinon, vous pouvez aller directement sur le dernier paragraphe du message qui résume en quelques mots les modifications apportées à Jade. Plus généralement, j'espère que ce que je vais présenter conviendra au plus grand nombre, car c'est une mécanique bien ficelée (du moins je le pense) qui permet de coder de façon réellement "fluide". Amélioration de l'orientation objet Comme vous le savez sans doute si vous connaissez un peu la théorie, l'orientation objet repose principalement sur trois piliers : l'encapsulation, l'héritage et le polymorphisme. L'encapsulation désigne la capacité d'une classe à masquer ses données et mécaniques internes au profit d'une interface constante, connue et donc exploitable. L'héritage, la capacité à spécialiser une classe à partir d'une autre. Et enfin le polymorphisme, la capacité des objets d'une sous-classe à se substituer à des objets de la classe parent dont ils dérivent. Notez qu'il est bien ici question de l'orientation objet uniquement, les fonctions/méthodes polymorphiques n'étant pas stricto sensu liées à l'orientation objet. A partir de cette base, il s'agit de se poser la question suivante : comment appliquer cela ? Une partie de la réponse est naturelle, du fait qu'elle découle clairement des principes évoqués ci-dessus : classe, instanciation en objet, héritage entre classes, polymorphisme de celles-ci, on a, je pense, aisément une vision concrète de ce que cela représente au sein d'un langage. Peut-on en dire autant de l'encapsulation ? Il ne s'agit pas que de masquer la donnée derrière des private ou protected, mais aussi (je tendrai à rajouter "et surtout") de donner à l'utilisateur une vision fixe de la classe, permettant à son programme d'être indépendant au maximum de l'implémentation de celle-ci. La question des propriétés peut être intéressante à poser dans ce contexte. Dans de nombreux cas, lorsqu'on lit des codes sources, on peut en effet s'apercevoir que les getters/setters ou propriétés tendent à être systématiquement écrits. Dans un nombre de cas non négligeable, ces propriétés ne servent au final qu'à exposer des données de la classe en lecture et écriture. Quel intérêt à cette forme d'encapsulation ? Il n'y en a pas de réel, on ne prévoit que du "au cas où", qui la plupart du temps ne vient jamais, on empâte inutilement son programme de lignes de code qui en complexifie la lecture, tout en pensant suivre les sacro-saintes bonnes pratiques. Ce paragraphe sur les propriétés maintenant fait, gardez en tête mes remarques, je vais y revenir plus tard. Continuons dans le domaine de l'encapsulation, sous un autre angle. On peut également se poser la question de la différenciation entre les appels de champs et méthodes : au-delà de l'aspect technique entourant celle-ci, existe-t-il un réel intérêt à appeler une méthode sans paramètres avec une paire de parenthèses vides, alors qu'il est on ne peut plus simple de s'en passer ? Après tout, on le voit bien avec le C# : les propriétés, qu'elles fassent référence à un champ ou soient purement "virtuelles", fonctionnent comme des méthodes, mais sont appelées comme des champs. Ça n'en complexifie aucunement la compréhension, et à l'inverse, la réduction du nombre de parenthèses éclaircit de façon significative le code. Même s'il m'a l'air assez peu connu, certains ici connaissent peut-être ce dont je veux parler : le principe d'accès uniforme. Ce principe veut que la manière d'accéder à un attribut, une propriété, une méthode, ne change pas de l'un à l'autre, offrant un accès similaire. Cela pousse donc encore plus loin l'encapsulation. Bon, ne soyons pas non plus trop ouvert à ce principe : il reste assez utopique, tout au plus peut-on s'en approcher si l'on souhaite conserver une syntaxe satisfaisante. Trouver une syntaxe réalisant à la fois l'appel d'une méthode, l'assignation et la récupération d'un champ n'est pas impossible, mais guère esthétique (à titre d'exemple, la liste d'arguments peut le faire, l'assignation étant alors de la forme a(5) au lieu de a=5, imaginez du a(b(c(d(5))))...). Rapportons maintenant tout ceci à Jade, et notamment à l'application qui peut en être faite aux propriétés. Premier point : Jade prend maintenant en compte le principe d'accès uniforme, ce qui modifie quelque peu la syntaxe. Une classe pourrait désormais se présenter de la sorte : Code : Jade class Test
champ as int
def new
...
end
def methode
...
end
end
t = Test.new
t.methode // Appel de la méthode
Console.Print(t.champ) // Accès au champPas de changement réellement étonnant : comme indiqué précédemment, il s'agit essentiellement de supprimer les parenthèses là où elles sont inutiles. De cette manière l'accès aux champs et méthodes sans arguments est strictement identique. Imaginons maintenant que l'on ait besoin d'un getter sur "champ" : Code : Jade class Test
def champ as int
return ...
end
def new
...
end
def methode
...
end
end
t = Test.new
t.methode // Appel de la méthode
Console.Print(t.champ) // Accès au getter (méthode)Rien de bien complexe ici non plus : on transforme notre champ en méthode sans arguments, et le résultat c'est qu'aucun changement de l'interface de la classe ne traduit le changement de l'implémentation de celle-ci. Le couplage entre le code appelant et le code appelé est réellement réduit au minimum commun qu'est le nom. Quid des setters maintenant ? La syntaxe est celle-ci : Code : Jade class Test
def champ = value as int
...
end
end
t = Test.new
t.champ = 42 // SetterQuelques petites explications : on est ici dans le cadre d'un setter par assignation (à opposer au setter par argument), qui par définition n'est pas censé retourner de valeur. De fait, l'espace syntaxique libre du type de retour est ici exploité par l'ajout du "= value as int" en complément de la définition de méthode. Concrètement, on ne fait ici que canoniser en définition une syntaxe employée pour l'assignation, chose qui - passée la surprise de voir apparaître un égal dans une définition de fonction - apparaît comme particulièrement lisible. Cette forme qui fait le parallèle avec la syntaxe concrète de l'assignation peut qui plus est être étendue : si la syntaxe d'assignation remplace le type de retour, cela n'exclut en rien la possibilité d'ajouter des paramètres à cette méthode servant de setter : Code : Jade class Test
def champ(index as int) = value as int
...
end
end
t = Test.new
t.champ(0) = 42Ceux qui auront un peu prêté attention au cas des tableaux sous Jade sauront que l'indexation se fait par parenthèses et non crochets. De fait, on pourrait changer l'implémentation ci-dessus par celle-ci sans rien changer : Code : Jade class Test
champ as int()
def new
champ = int(10)
end
end
t = Test.new
t.champ(0) = 42Comme vous pouvez le constater : implémentation différente, accès similaire. Le principe d'accès uniforme se trouve donc respecté à nouveau. Petit retour sur les getters dans le cas où l'on souhaite avoir un accès similaire à celui d'un tableau. Rien de plus simple, puisqu’il suffit de préciser les paramètres souhaités, c'est de la forme d'une méthode tout ce qu'il y a de plus classique : Code : Jade class Test
def champ(index as int)
return ...
end
end
t = Test.new
Console.Print(t.champ(0))Qui présente strictement le même accès que ceci : Code : Jade class Test
champ as int()
def new
champ = int(10)
end
end
t = Test.new
Console.Print(t.champ(0))A partir de ces quelques exemples, vous devriez pouvoir imaginer toutes les possibilités qui découlent de ces syntaxes, sachant que la surcharge d'arguments et le polymorphisme sont bien sûr toujours valables pour les méthodes faisant office de getter et setter. Pour achever le tour d'horizon sur les propriétés, je conclurai par un point sur le lien entre celles-ci et les champs. Comme je l'ai déjà précisé, il est communément accepté, assez souvent indépendamment du langage, qu'un champ doit être privé, et accédé à partir d'un getter ou setter (chose que je critiquais au début du message car trop souvent il ne s'agit que d'une sorte de wrapper du champ). La raison à cela, évoquée par certains pontes de l'orienté objet, c'est qu'en ouvrant un champ à l'extérieur, on ne l'ouvre pas qu'en lecture, mais aussi en écriture (sauf dans certains langages comme Eiffel). Et cette possibilité d'écriture rompt avec le principe d'encapsulation. La solution que plusieurs langages ont mis en place (Ruby par exemple) c'est l'implémentation de propriétés dites "automatiques" qui sont liées à un champ donné. Mais au final le problème reste entier : on implémente la possibilité de passer une variable en public et de la modifier, mais on ne l'utilise pas car ce n'est pas comme ça qu'il faut faire. Autant mettre toutes les propriétés en protected à l'instar du Smalltalk, dans ce cas, non ? La réponse de Jade n'en est pas vraiment une, puisqu'en réalité ce problème ne concerne pas le langage. En ayant prit en compte le principe d'accès uniforme, la façon d'accéder à un champ, une méthode, s'est vue mise en commun. Si bien que l'on peut considérer de l'extérieur qu'accéder à un champ est accéder à une propriété en lecture/écriture. En fait on ne peut pas le savoir à moins d'avoir accès à la classe appelée. Et ce n'est pas important de le savoir, dès lors qu'on sait comment utiliser celle-ci. Et c'est cela la force de l'accès uniforme à mon sens : permettre d'oublier les contraintes à la fois de la technique et des "bonnes pratiques" au profit de l'accessibilité. Références non nulles Il a été précédemment cité dans le sujet le cas des références non nulles, ce à quoi j'avais répondu que l'on perdait en flexibilité. Ce qui est vrai, mais à la réflexion, ça l'est en fait seulement dans un cas précis : celui où l'on considère que la référence non nulle est l'exception. C'est ce que l'on retrouve le plus souvent, l'exemple typique étant C# + SQL Server. La solution à cela se trouve en fait dans l'autre sens : le cas où c'est l'objet nul qui est l'exception. Dès lors, le travail est simplifié : par défaut, toutes les références sont sécurisées. Et celles qui ne le sont pas peuvent être trouvées au moment de la compilation. On peut donc totalement reporter les erreurs de type pointeur nul de l'exécution à la compilation, ce qui est un atout assez considérable étant donné que la plupart des langages ne détectent ce type d'erreurs qu'à l'exécution. Plus en détail, comme ça marche ? L'état "nullable" se retrouve associé au type (on peut imaginer qu'une méthode retourne un objet pouvant être nul). La notation associé à cet état est relativement logique, en ignorant si le contenu est nul ou non, on se pose une question, une interrogation sur son type, est-ce un objet de tel type ou un nul ? Dès lors : Code : Jade class Obj
champ as int
end
class Test
a as Obj? // a peut être nul
b as Obj // b n'est jamais nul et doit être initialisé dans le constructeur
def new
b = Obj.new
end
endOn peut d'ors et déjà noter, si vous aviez un doute, que la question de la référence nul ne se pose que lorsque l'on parle d'objets. Les types de base, passés par valeur, ne sont pas concernés. A l'utilisation, ça donne ceci : Code : Jade class Obj
champ as int
def get as bool
return this != null
end
end
class Test
a as Obj?
def methode
Console.Print(a.champ) // erreur : appel non sécurisé
if (a) // on teste l'objet pour savoir s'il est nul ou non
Console.Print(a.champ) // ok, dans la portée du if seulement
end
end
endExplications : au sein de la méthode, le premier appel à Console.Print retournera une erreur lors de la compilation. L'appel de 'a' n'est en effet pas sécurisé. On ignore si cette variable est nulle ou non. Le second appel à Console.Print compilera. En effet, le test précédent cet appel permet de déterminer si oui ou non 'a' est nul. De fait, au sein de la portée du if, l'accès à 'a' est sécurisé, permettant à celle-ci d'être appelée de manière classique. Couplage minimum Last but not least, le problème du couplage. L'un des soucis majeurs de la conception des programmes orientés objets tient en effet en un couplage fort des différents éléments de ce programme, qu'il s'agisse de module ou de classes. En renforçant les liens de dépendance, on perd de l'intérêt de l'orienté objet, en exposant les mécaniques internes des classes. Histoire d'être un peu plus concret, imaginons ceci : on réalise un jeu de plateforme, et l'on a une image d'un arbre en vue de côté à afficher. Ce n'est pas qu'un élément de décor quelconque, on lui donnera donc une classe dédiée : Code : Jade class Tree
sprite as Sprite
def new(file as string)
sprite = Sprite.new(file)
end
def cut
...
end
def grow
...
end
endSi je souhaite maintenant afficher le sprite de cet arbre (méthode Sprite.draw), l'une des solutions qui serait communément adoptée c'est ceci : Code : Jade t = Tree.new
do
t.sprite.draw(0, 0) // 0 et 0 sont les coordonnées à l'écran.
loopCa fonctionne, rien à dire sur ce point-ci. En revanche, en procédant de la sorte, la maintenabilité du programme s'en trouve diminuée par un couplage élevé. Imaginez que notre arbre ne soit plus dessiné à l'aide d'un sprite, mais à l'aide d'un objet 3D, qui ajoute une dimension. Avec le code ci-dessus, deux réécritures sont nécessaires : celle d'une partie de la classe, et celles du code d'affichage. Que le code de la classe soit à modifier : c'est on ne peut plus normal ! En revanche que dire du code d'affichage ? Sur cet exemple, c'est tolérable. Imaginez que l'on ait à modifier cela plusieurs fois... Ça l'est déjà beaucoup moins. En réponse à cette problématique, une règle de conception connue sous le nom de loi de Déméter a été inventée. On peut résumer celle-ci à des assertions telles que "tell, don't ask" ou "only talk to your friends" Qui peuvent être traduites par "dites, ne demandez pas" et "ne parlez qu'à vos amis". En termes de code, cela signifie que l'on ne doit pas accéder à un élément d'une classe pour accéder à un autre dont on n'aurait pas connaissance sans cette relation commune. Chose dont on ne tient pas compte dans l'appel t.sprite.draw(0,0). Ce cas aurait du être écrit ainsi pour suivre la loi de Déméter : Code : Jade class Tree
sprite as Sprite
def new(file as string)
sprite = Sprite.new(file)
end
def cut
...
end
def grow
...
end
def draw(x as integer, y as integer)
sprite.draw(x, y)
end
end
t = Tree.new
do
t.draw(0, 0) // on affiche l'arbre aux coordonnées demandées.
loopEn procédant de la sorte, remplacer le sprite par un objet n'aurait pas eu d'impact sur le code appelant : Code : Jade class Tree
object as Object
def new(file as string)
object = Object.new(file)
end
def cut
...
end
def grow
...
end
def draw(x as integer, y as integer)
object.draw(x, y, 5)
end
end
t = Tree.new
do
t.draw(0, 0) // on affiche l'arbre aux coordonnées demandées.
loopOn peut tout de même voire poindre le gros problème de la loi de Déméter : si l'on souhaite l'appliquer, elle oblige à rédiger une multitude de petites méthodes intermédiaires pour propager les appels d'objet en objet. Imaginez ceci : Code : Jade class A
def methode
...
end
end
class B
truc as A
def methode
truc.methode
end
end
class C
bidule as B
def methode
bidule.methode
end
end
c = C.new
c.methodeRésultat : pour accéder à c.bidule.truc.methode, il nous en coûtera la bagatelle de trois appels de méthodes... Difficilement acceptable en termes de performances. Je ne vous aurait bien évidemment pas parlé de cela s'il n'y avait pas une solution derrière ça. En effet la loi de Déméter entre en synergie parfaite avec les modifications présentées tout au long de ce long message. Voyez ceci : Code : Jade class A
def methode
...
end
end
class B
truc as A
def methode alias truc.methode
end
class C
bidule as B
def methode alias bidule.methode
end
c = C.new
c.methodeAvec cette syntaxe, on ne fait ici qu'attribuer un alias d'une méthode à une élément d'un champ. En appelant c.methode, on accède à bidule, qui recherche methode, qui accède à truc, qui recherche methode et l'exécute. Au final, on ne fait qu'exécuter le code c.bidule.truc.methode, tout en conservant 100% de la maintenabilité de l'application et en respectant la loi de Déméter. Si au niveau des noms, cela est rendu possible grâce à l'accès uniforme, au niveau de la technique, c'est la référence non nulle qui entre ici en jeu. Considérant que le champ bidule de la classe C n'a pas été déclaré comme potentiellement nul, on peut y accéder en toute confiance. Et pour garder un peu d'uniformité, on pourra remarquer que la syntaxe de déclaration de l'alias reste identique à celle utilisée pour importer des fonctions d'un .lib. Règles de choix des mots-clés J'ai décidé d'établir trois petites "règles" qui me serviront à déterminer les mots-clés du langage. Vous avez sans doute remarqué jusqu'à maintenant que beaucoup de ceux-ci étaient des raccourcis de mots plus longs : "struct" pour "structure", "enum" pour "enumeration", "def" pour "definition/define", etc. C'était purement arbitraire. Les trois points qui me serviront maintenant sont par ordre d'importance : - si ce mot-clé est déjà présent dans un autre langage, utiliser les solutions existantes, - un mot-clé de plus de six lettres doit être raccourci, - plus le mot-clé est présent fréquemment, plus celui-ci doit être court Cela ne change que peu de chose actuellement, seuls deux mots-clés sont modifiés : "integer" devient "int" et "boolean" devient "bool". Je ne pense pas que beaucoup de monde y verra un problème au vu de l'usage très répandu de "int" et "bool". TL;DR Pour résumer les modifications apportées à Jade (tout ça pour ça !) : - prise en compte du principe d'accès uniforme mettant en commun les appels aux champs, méthodes, propriétés en supprimant les parenthèses inutiles - références non nulles des objets par défaut - support de la loi de Déméter permettant de propager les appels d'objet en objet Voilà tout pour le pavé. J'ai vraisemblablement passé pas loin d'une centaine d'heures cumulées à travailler sur cela quand j'en avais la possibilité (midi, pauses, bus, soirées/week-end...), mais je suis certain de ne pas encore avoir pensé à tous les aspects apportés (ou retirés) par ces évolutions. Toujours est-il que j'en suis très satisfait à l'heure actuelle. Il ne me reste plus qu'à implémenter tout ça (ce qui devrait demander bien moins de temps :). |
|
Page précédente

