IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

Langages de programmation Discussion :

« Home » : réflexions sur un nouveau langage


Sujet :

Langages de programmation

  1. #101
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Avancée et exemple sur la POO en .Home


    Après qlq jours de "vacances", et une "réflexion" plus approfondie, voici
    comment j'ai solutionné le problème (utiliser la bonne méthode sur le bon objet).

    Je vous invite à lire et analyser ce code, faire toute remarques, critiques
    ou erreurs détectées, de ne pas hésiter a demander plus d'explications si
    celles que je donne ne sont pas claires, si certaines sont manquantes où s'il
    reste une erreur de raisonnement dans ma solution.

    Le tout, et je n'en doute pas, en restant poli et courtois.

    Merci à vous, et bonne lecture.

    DECLARATION DES APIS

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    api apiActions: jump, fire
    api apiAnims: explode, fall
    CREATION DE TYPES REVENDIQUANT L'UTILISATION D'UNE OU PLUSIEURS API(S)

    Ici, 2 types sont crées, le type player et le type superplayer.
    Tout 2 déclarent utiliser au minimum l'api apiActions, via l'opérateur
    |= suivit d'une liste d'apis qu'ils disent implémenter. Je n'explique
    pas ici le choix du symbole |= en lieu est place du mot-clef impl,
    mais j'aborderai ce point ultérieurement ou en réponse à vos éventuelles.
    questions.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    type player |= apiActions:
        int8 x, y
    end
    
    type superplayer |= apiActions, apiAnims:
        int16 x
    end
    UTILISATION

    Ici, on instancie p comme étant de type player. Il appelle l'api
    jump() exactement de la même manière que s'il appelait une méthode de
    son type qui ne serait pas une api, mais une simple méthode.

    Il n'y a dans le code source, aucune différence entre l'implémentation d'une
    api ou l'implémentation d'une simple method.

    Une api est juste une method listée faisant partie d'une api.

    On voit aussi qu'une api peut appeler une autre api ou une
    method du type.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    mtd player.stop(int8 a) <- int16 value:
        me.y := a + 1 ; access type player x variable using "me".
        value := my.y ; load value with the result.
    end
    
    mtd player.fire():
    end
    
    mtd player.jump():
        int8 local_value
        local_value := 5
        me.y := me.y + 3 + me.x + local_value
        me.fire()
        me.stop(5)
    end
    
    player p
    p.jump()
    
    int16 result
    result := p.stop(10)
    Le type superplayer implémente lui 2 apis, et on crée une instance
    sp de ce type.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    type superplayer |= apiActions, apiAnims:
        int16 x
    end
    
    mtd superplayer.jump(): end
    mtd superplayer.explode(): end
    mtd superplayer.fire(): end
    mtd superplayer.fall(): end
    
    superplayer sp
    POO "static"

    On crée ici 2 instances de type objects, a savoir actors_1 et actors_2.
    Un objects est un type contenant une liste d'instances de types.

    On crée aussi 1 instances de type actions, a savoir actions_1.
    Une actions est un type contenant une liste de noms d'api.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    objects actors_1[sp, p]
    objects actors_2[p, sp]
    actions actions_1[jump, fire]
    Et voici comment on utilise les [objects] et les [actions]:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    using actors_1: actions_1 end
    using actors_2: actions_1 end
    using est un mot-clef, suivit du nom d'1 objects, du symbole
    ':', et d'1 actions (mais il pourrait en avoir plusieurs séparés
    par une 'virgule ,'), puis clôturée via le mot-clef end.

    En code "IRC" (Intermediate Represation Code), voici comment le using
    est implémenté:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    ;----------------------------------------------- USING - 
    :CONST-using-1: 
        !int16 = _using-1-objs=2 
        !int16 = _using-1-apis=2 
    :CONST-using-1-OBJS-actors_1: 
        (:sp:) 
        (:p:) 
    :CONST-using-1-APIS-actions_1: 
        (:CODE-superplayer-jump:) 
        (:CODE-superplayer-fire:) 
        (:CODE-player-jump:) 
        (:CODE-player-fire:) 
    :DATA-using-1: 
        @int16 = _using-1-i_obj=0 
        @int16 = _using-1-i_api=0 
        @int16 = _using-1-adr_obj=(:CONST-using-1-OBJS-actors_1:) 
        @int16 = _using-1-adr_api=(:CONST-using-1-APIS-actions_1:) 
    :CODE-using-1: 
        asm_SUB _using-1-adr_obj 2 
        asm_SUB _using-1-adr_api 2 
    :LOOP-1: 
        asm_ADD _using-1-adr_obj 2 
    :LOOP-2: 
        asm_ADD _using-1-adr_api 2 
        asm_PUSH int16 (:AFTER-5:) 
        asm_PUSH int16 _using-1-adr_obj 
        asm_JUMP TO _using-1-adr_api 
    :AFTER-5: 
        asm_ADD _using-1-i_api 1 
        asm_CMP _using-1-i_api _using-1-apis 
        asm_BRNEQ (:LOOP-2:) 
        asm_ADD _using-1-i_obj 1 
        asm_CMP _using-1-i_obj _using-1-objs 
        asm_BRNEQ (:LOOP-1:) 
        
    ;----------------------------------------------- USING - 
    :CONST-using-2: 
        !int16 = _using-2-objs=2 
        !int16 = _using-2-apis=2 
    :CONST-using-2-OBJS-actors_2: 
        (:p:) 
        (:sp:) 
    :CONST-using-2-APIS-actions_1: 
        (:CODE-player-jump:) 
        (:CODE-player-fire:) 
        (:CODE-superplayer-jump:) 
        (:CODE-superplayer-fire:) 
    :DATA-using-2: 
        @int16 = _using-2-i_obj=0 
        @int16 = _using-2-i_api=0 
        @int16 = _using-2-adr_obj=(:CONST-using-2-OBJS-actors_2:) 
        @int16 = _using-2-adr_api=(:CONST-using-2-APIS-actions_1:) 
    :CODE-using-2: 
        asm_SUB _using-2-adr_obj 2 
        asm_SUB _using-2-adr_api 2 
    :LOOP-3: 
        asm_ADD _using-2-adr_obj 2 
    :LOOP-4: 
        asm_ADD _using-2-adr_api 2 
        asm_PUSH int16 (:AFTER-6:) 
        asm_PUSH int16 _using-2-adr_obj 
        asm_JUMP TO _using-2-adr_api 
    :AFTER-6: 
        asm_ADD _using-2-i_api 1 
        asm_CMP _using-2-i_api _using-2-apis 
        asm_BRNEQ (:LOOP-4:) 
        asm_ADD _using-2-i_obj 1 
        asm_CMP _using-2-i_obj _using-2-objs 
        asm_BRNEQ (:LOOP-3:)
    C'est complètement statique, mais une approche similaire pourrait être utilisée
    avec une liste dynamique d'objects et d'actions.

    Ici, la table d'indirection est "codée en dur", mais est courte, et l'accès (une fois
    l'IRC transformé pour un assembleur pourrait écrire le même genre de code en
    utilisant une adresse de base et un registre d'index. Mais j'ai voulu ici rester
    simple, c'est une "proof of concept".

    Chacun des using pourrait être mit dans une fonction (pour ne pas générer
    le même code si usage multiples), et il pourrait même être possible d'avoir une
    seule fonction gérant tous les using, réduisant fortement la taille du
    code assembleur.

    Je trouve cette approche intéressante (séparation des objects et des actions)
    dans 2 "listes" différentes, pouvant ensuite être "combinées".

    Il y a certainement qlq détails a améliorer, mais cela a le mérite de pouvoir
    assez simplement faire exécuter une liste d'api (les actions) sur des types
    ne dépendant pas l'un de l'autre, c'est à dire une liste d'objects (les objects)
    hétérogènes.

    Tout le principe peut être "validé" à la compilation. Si une api n'est pas implémentée, si un "objects"
    utilise une 'actions' dont il n'implémente pas une des api.

    Merci de votre attention et de vos réactions, qui me sont précieuses.

    BàV et Peace & Love.

  2. #102
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Un petit pas en plus...
    La "syntaxe" a un peut changé:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
        objects actors[sp, p]
        actions oprtns[actionJump, actionFire]
    
        using actors do oprtns end
        do oprtns using actors end
    using ... do ... effectue séquenciellement toutes les actions sur un objet, avant de passer à l'objet suivant.
    do ... using ... effectue séquentiellement une action sur tous les objects, avant de passer à l'action suivante.

    Petite avancée, mais dans le bon sens je pense.

    BàV et Peace & Love.

  3. #103
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    C’est assez confus… avec des notions et appellations empruntées à Java (classes, interfaces), on comprendrait mieux.

    En vis-à-vis de ces 2 notions, on a :
    api (interface),
    type (classes),
    objects … drôle d’appellation pour de simples liste
    actions … il n’est pas clair sur ce que cela fait. Dans l’exemple ce sont des listes de méthodes… comme une API ?

    Pas d’exemples de ce que permet using… comment cela s’utilise ensuite (qu’est-ce que cela permet).

    Par ailleurs, si le language est statique, le objects pose problème : quelle classe d’objet est considérée ?

    En Java, c’est bien plus clair :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    List<Interface> liste = new ArrayList<>();
    liste.add(t1);
    liste.add(t2);
    Et là le compilateur sait que TOUS les membres de la liste sont compatibles avec l’Interface donnée. (Et si par exemple t2 est incompatible, une erreur est émise à la compilation). Pas besoin d’un using après coup.

  4. #104
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Bonjour Floyer


    Merci de votre participation.

    Citation Envoyé par floyer Voir le message
    C’est assez confus… avec des notions et appellations empruntées à Java (classes, interfaces), on comprendrait mieux.
    Je vais tenter d'expliquer ce choix. C'est peut-être confus (je peux le comprendre car c'est une approche différente de ce que l'on voit généralement), mais pourtant, cela a été réfléchi longuement, et c'est assez "clair" dans ma tête.

    Citation Envoyé par floyer Voir le message
    objects … drôle d’appellation pour de simples liste.
    J'ai utilisé objects à la place de list, pour bien marquer la différence. Une list peut contenir un peu n'importe quoi, alors qu'un liste d'objects ne peut contenir que des instances de Types. Une liste objects peut contenir par exemple un "player" ou un "superplayer", mais pas un int8, par exemple, qui est un type de base.

    Citation Envoyé par floyer Voir le message
    actions … il n’est pas clair sur ce que cela fait. Dans l’exemple ce sont des listes de méthodes… comme une API ?
    Exactement, une liste d'actions est une liste d'api implémentées par un type. Une liste d'actions ne peut contenir que des api implémentées par un Type.

    Citation Envoyé par floyer Voir le message
    Pas d’exemples de ce que permet using… comment cela s’utilise ensuite (qu’est-ce que cela permet).
    Voici comment cela s'utilise:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    program demo_vcall:
    
        api apiActions: jump, fire end
        api apiAnims: explode, fall end
    
        type player |= apiActions:
            int8 x, y
        end
    
        mtd player.stop(int8 a) <- int16 value:
            me.y := a + 1 ; access type player x variable using "me".
            value := my.y ; load value with the result.
        end
    
        mtd player.fire():
        end
    
        mtd player.jump():
            int8 local_value
            local_value := 5
            me.y := me.y + 3 + me.x + local_value
            me.fire()
            me.stop()
        end
    
        player p
        p.jump()
    
        int16 result
        result := p.stop(10)
    
        type superplayer |= apiActions, apiAnims:
            int16 x
        end
    
        mtd superplayer.jump(): end
        mtd superplayer.explode(): end
        mtd superplayer.fire(): end
        mtd superplayer.fall(): end
    
        superplayer sp
    
        objects actrs[sp, p]
        actions actns[jump, fire]
    
        using actrs do actns end
        do actns using actrs end
    
        end 0
    Voici: La partie qui nous intéresse ici sont les lignes suivantes:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
        objects actrs[sp, p]
        actions actns[jump, fire]
    On y déclare une liste d'objects (via le mot-clef objects), qui se nomme actrs, et qui contient 2 instances de types différents, sp (superplayer) et p (player). (vérifiable à la compilation).

    On déclare une liste d'actions (via le mot-clef actions), qui se nomme actns, et qui contient 2 api que chacun des types ont déclarés implémenter. (vérifiable à la compilation).

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
        using actrs do actns end
        do actns using actrs end
    using actrs do actns, permet de faire un lien entre des types, et une série d'actions.
    do actns using actrs, permet de faire un lien ente des actions, et une série de types.

    Avec la syntax using ... do ... on va effectuer toutes les actions pour un type avant de passer au type suivant.
    Avec la syntax do ... using ... on va effectuer une action pour chacun des types avant de passer à l'action suivante.

    Ces deux possibilités permettent de lier des objects à des actions (ou un comportement).
    Une liste objects ou une liste actions peut être réutilisées autant de fois que voulus.

    Citation Envoyé par floyer Voir le message
    Par ailleurs, si le language est statique, le objects pose problème : quelle classe d’objet est considérée ?
    Une "table" est créée (lors de la génération du code) pour faire le lien afin d'utiliser la bonne api sur le bon type.

    Voici le code IRC (Intermediate Représentation Code) généré par cet exemple. Je n'ai mis que la partie utilisant "using" et "do", pour que le listing ne soit pas trop long.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    ;----------------------------------------------- USING - 
    :CONST-using-1: 
        !int16 = _using_1_objs=2 
        !int16 = _using_1_apis=2 
    :CONST-using-1-OBJS-actors: 
    (:sp:) 
    (:p:) 
    :CONST-using-1-APIS-oprtns: 
    (:CODE-superplayer-actionJump:) 
    (:CODE-superplayer-actionFire:) 
    (:CODE-player-actionJump:) 
    (:CODE-player-actionFire:) 
    :DATA-using-1: 
        @int16 = _using_1_i_obj=0 
        @int16 = _using_1_i_api=0 
        @int16 = _using_1_adr_obj=(:CONST-using-1-OBJS-actors:) 
        @int16 = _using_1_adr_api=(:CONST-using-1-APIS-oprtns:) 
    :CODE-using-1: 
        asm_SUB _using_1_adr_obj 2 
        asm_SUB _using_1_adr_api 2 
    :LOOP-2: 
        asm_ADD _using_1_adr_obj 2 
    :LOOP-3: 
        asm_ADD _using_1_adr_api 2 
        asm_PUSH int16 (:AFTER-5:) 
        asm_PUSH int16 _using_1_adr_obj 
        asm_JUMP TO _using_1_adr_api 
    :AFTER-5: 
        asm_ADD _using_1_i_api 1 
        asm_CMP _using_1_i_api _using_1_apis 
        asm_BRNEQ (:LOOP-3:) 
        asm_ADD _using_1_i_obj 1 
        asm_CMP _using_1_i_obj _using_1_objs 
        asm_BRNEQ (:LOOP-2:) 
    ;-------------------------------------------------- DO - 
    :CONST-do-1: 
        !int16 = _do_1_apis=2 
        !int16 = _do_1_objs=2 
    :CONST-do-1-APIS-oprtns: 
    (:CODE-superplayer-actionJump:) 
    (:CODE-superplayer-actionFire:) 
    (:CODE-player-actionJump:) 
    (:CODE-player-actionFire:) 
    :CONST-do-1-OBJS-actors: 
    (:sp:) 
    (:p:) 
    :DATA-do-1: 
        @int16 = _do_1_i_api=0 
        @int16 = _do_1_i_obj=0 
        @int16 = _do_1_adr_api=(:CONST-do-1-APIS-oprtns:) 
        @int16 = _do_1_adr_obj=(:CONST-do-1-OBJS-actors:) 
    :CODE-do-1: 
        asm_SUB _do_1_adr_api 2 
        asm_SUB _do_1_adr_obj 2 
    :LOOP-4: 
        asm_ADD _do_1_adr_api 2 
    :LOOP-5: 
        asm_ADD _do_1_adr_obj 2 
        asm_PUSH int16 (:AFTER-1:) 
        asm_PUSH int16 _do_1_adr_api 
        asm_JUMP TO _do_1_adr_obj 
    :AFTER-6: 
        asm_ADD _do_1_i_obj 1 
        asm_CMP _do_1-i_obj _do_1_objs 
        asm_BRNEQ (:LOOP-5:) 
        asm_ADD _do_1_i_api 1 
        asm_CMP _do_1_i_api _do_1_apis 
        asm_BRNEQ (:LOOP-4:)
    Citation Envoyé par floyer Voir le message
    En Java, c’est bien plus clair :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    List<Interface> liste = new ArrayList<>();
    liste.add(t1);
    liste.add(t2);
    Et là le compilateur sait que TOUS les membres de la liste sont compatibles avec l’Interface donnée. (Et si par exemple t2 est incompatible, une erreur est émise à la compilation). Pas besoin d’un using après coup.
    Ce n'est pas la même approche. En java, comme en C++ et pratiquement tous les langage implémentant une forme de POO utilise l'encapsulation des objects et des actions.

    Mon approche est toute autre, il n'y a pas cette notion d'encapsulation.

    Chaque Type est complètement indépendant l'un de l'autre, et le compilateur émet une erreur si dans un using ou un do un object utilise une api qu'il n'a pas implémenté.

    J'utilise plutôt la composition, en liant un/des objects et une/des actions.
    On peut ainsi combiner bien plus facilement des objects et des actions, grâce à cette "non-encapsulation" (qui pousse a utiliser "l'héritage", plutôt que la "composition").

    NOTE: dans le code IRC, les labels ( :blabla: ) seront remplacés par la "valeur" du label :blabla:

    Pour le moment, les listes objects et actions ne peuvent être qu'allouée statiquement, et la "table" faisant le lien" est également implémentée pour chaque using/do. Avec une redondance de "table", certes. On peut actuellement contourner ce soucis en englobant ces derniers dans une methode.

    L'étape suivante, sera de permettre le même résultat avec des objects et/ou actions allouées dynamiquement, et certainement avec une "table" unique.

    J'espère que c'est plus clair pour vous maintenant, mais n'hésitez pas à me pousser dans mes retranchements si vous voyez un souci, une limite, ou une erreur dans mon raisonnement. N'oubliez pas que la version "dynamique" des objects, des actions et de la table est à venir.

    BàV et Peace & Love.

  5. #105
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    Que ce soit en Java, ou OCaml, les listes sont des types génériques. Cela signifie qu’au moment de déclarer une variable liste, on précise la restriction de type. Cela permet par exemple de garantir à la compilation qu’une interface/api est supportée.

    Cela diffère des premières versions de Java, où les types génériques n’existaient pas, si bien que les listes acceptaient n’importe quoi, et on pouvait avoir des erreurs à l’exécution (api non supportée).

  6. #106
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Je comprend bien :-)
    Citation Envoyé par floyer Voir le message
    Que ce soit en Java, ou OCaml, les listes sont des types génériques. Cela signifie qu’au moment de déclarer une variable liste, on précise la restriction de type. Cela permet par exemple de garantir à la compilation qu’une interface/api est supportée.

    Cela diffère des premières versions de Java, où les types génériques n’existaient pas, si bien que les listes acceptaient n’importe quoi, et on pouvait avoir des erreurs à l’exécution (api non supportée).
    On pourrait ici aussi appliquer une restriction tant sur le type d'objects acceptés que sur type d'actions désirées, avec une syntaxe ressemblant à ceci:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    objects[player, superplayer] actrs[sp, p]
    actions[jump, fire, explode] actns[jump, fire]
    ou:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    objects[player, superplayer] actrs[sp, p]
    actions[apiActions, apiAnims] actns[jump, fire]
    ou

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    objects[player, superplayer] actrs[] = sp, p
    actions[jump, fire, explode] actns[] = jump, fire
    ou plus simplement encore:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    objects actrs[] = sp, p
    actions actns[] = jump, fire
    Cela n'est cependant pas nécessaire dans le cas d'une liste d'objects ou d'actions statique.
    Mais ce sera peut-être nécessaire lorsque objects et actions seront déclarés comme "non statique" (allouée sur le heap) via le code suivant:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    objects actrs<> = sp, p
    actions actns<> = jump, fire
    Dans ce cas l'utilisation des '<' et '>' signifient au compilateur que actrs est une liste d'objects dynamique et actns une liste d'actions dynamique également. A l'initialisation, on pourrait ajouter sp, p à actrs (après le =), et jump, fire à actns (après le =).

    On pourrait écrire:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    objects actrs<>
    actions actns<>
    
    actrs.add(sp)
    actrs.add(p)
    
    actns.add(jump)
    actns.add(file)
    On pourrait même permettre d'utiliser des "types" qui n'implémente qu'une "partie" d'une apiActions. Tant que les différents types listés dans une liste objects implémentent les apis utilisées dans une liste d'actions, on pour les combiner lors d'un using .. do ... où un do ... using ...

    Cela serait d'une grande souplesse, et pourtant être "validé" au moment de la compilation lors des using .. do ... où un do ... using .... Aussi, pourquoi empêcher qu'un Type n'implémentant pas complétement une api, ne puisse pas être utilisé dans une combinaison où l'une des api qu'il n'a pas implémenté n'est pas utilisée comme action ? Le tout serait vérifiable à la compilation.

    On se rapprocherait d'un "Duck Typing" à la Python, mais vérifiable à la compilation.

    Pour le moment, je pense me diriger vers cela. Je sais que cela s'éloigne de la POO tel qu'elle est implémentée en C++ ou en Java, mais je trouve l'approche plus souple, simple et même plus puissante, tout en évitant le "couplage" (encapsulation) de la POO, qui "force" a créer les types de "base" d'une hiérarchie tout en essayant de "deviner" comment cette hiérarchie va évoluer. A cause de l'encapsulation, on doit parfois "ajouter" des "méthodes" dans les types de base car "plus loin dans la hiérarchie", cela devient nécessaire.

    Car au final, quel est l'intérêt de cette "encapsulation", si on doit la "contourner" pour faire évoluer une hiérarchie de "class" ? Cela entraine souvent des types de bases trop "gros", car on essaye de "deviner" comment sera utilisé cette hiérarchie pas la suite, donc du code, et du travail inutile.

    Dans mon approche, l'héritage lui-même n'est plus vraiment nécessaire, mais on a pourtant (sauf si je me trompe, ce qui reste possible...) la possibilité de faire exactement la même chose qu'avec la "POO classique" de C++/Java.

    Je sais que je vais à contre-courant des "principes" de la "POO style C++/Java". L'encapsulation n'est plus nécessaire, l'héritage non plus, et le "polymorphisme" n'est pas une nécessité. Le but "principale" de la "POO à la C++/Java (nécessitant la création de 'class'), est de pouvoir traiter d'une manière "uniforme" des objets de "types" différents. Sauf erreur (ce qui reste possible, je ne prétend de rien), mon approche est plus simple, lisible, et permet de traiter également et d'une manière "uniforme" des objets de "types" différents.

    Quels est votre avis sur ce sujet ?

    Encore merci de votre temps.

    BàT et Peace & Love.

  7. #107
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    C’est l’intérêt de l’héritage multiple (classe C++ ou interface Java), une classe peut déclarer plusieurs API, si bien que si tu as vraiment besoin de flexibilité, tu peux déclarer une interface par méthode. (Mais souvent tu as des méthode qui vont ensembles dans un projet si bien que tu ne les sépares pas dans plusieurs interface, mais tu fais ce que tu veux).

    Autre approche, dans OCaml, tu n’as pas à déclarer qu’une classe implémente telle ou telle interface. Mais tu peux tout de même déclarer une fonction qui accepte tout objet implémentant telle ou telle méthode.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     let f x:< jump : unit; fire : unit; .. > = 
       x#jump;
       x#fire;;
    (Il est question de typage structurel… c’est proche du Duck Typing, mais avec une vérifications à la compilation et des types statiques).

    De manière générale, si tu as une classe assez riche (super_player) et une moins riche (player), tu peux écrire :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    let sp = new super_player in
    let p = sp :> player
    Ce qui permet d'utiliser p comme s'il avait un type plus limité que super_player, même si les deux classes sont déclarées indépendamment (sans héritage). Seul compte les méthodes qui sont déclarées ou non. Avec cette construction, p peut être inclus dans une liste en apparence homogène de player. ET une itération sur cette liste peut utiliser les méthodes garantie comme disponibles.

    A propos des itérations, le cas d'usage méthode sans paramètre est vraiment très limité. Il vaut mieux une approche plus générale (for (item : liste) { item.fire(); item.jump(5);} en Java).

  8. #108
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut
    Citation Envoyé par floyer Voir le message
    C’est l’intérêt de l’héritage multiple (classe C++ ou interface Java), une classe peut déclarer plusieurs API, si bien que si tu as vraiment besoin de flexibilité, tu peux déclarer une interface par méthode. (Mais souvent tu as des méthode qui vont ensembles dans un projet si bien que tu ne les sépares pas dans plusieurs interface, mais tu fais ce que tu veux).
    L'héritage multiple, je ne suis pas fan, cela amène d'autres soucis et cela devient vite compliqué à suivre. Enfin, c'est le souvenir que j'en ai, car je n'ai plus utilisé de langage permettant cela depuis un certains temps. Les choses se sont peut-être améliorée, mais je n'ai pas suivi les évolutions dans ce domaine, je l'avoue :-/

    Citation Envoyé par floyer Voir le message
    Autre approche, dans OCaml, tu n’as pas à déclarer qu’une classe implémente telle ou telle interface. Mais tu peux tout de même déclarer une fonction qui accepte tout objet implémentant telle ou telle méthode.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     let f x:< jump : unit; fire : unit; .. > = 
       x#jump;
       x#fire;;
    (Il est question de typage structurel… c’est proche du Duck Typing, mais avec une vérifications à la compilation et des types statiques).
    J'ai l'impression que mon implémentation tiens la route, mais quand on a la tête dans le guidon, on peut vite passer à côté d'un soucis ou d'un blocage future. Je n'en vois pas pour l'instant.

    Pensez-vous que mon approche (pas d'encapsulation, pas d'héritage) de séparer les "Types" (contenant les datas) et les "Actions" (le comportement), est une mauvaise chose ? Je trouve que ce "découplage" évite bien des problèmes, est souple, simple, et que la "combinaisons" lors d'un using ... do ... ou d'un do ... using ... permet d'en faire autant qu'une approche plus "classique" (à la C++/Java), nécessitant des "class', et une hiérarchie parfois difficile a implémenter.

    J'y vois une sorte de "Duck Typing" validable par le compilateur.

    La déclaration des "apis" pourraient être retirée, mais elle permet de créer un "table" plus petite, car cette "table" ne reprend que les méthodes implémentant une "api", et pas toutes les méthode d'un type. Un Type peut très bien n'utiliser aucune apis s'il n'est pas destiné a être utilisé de manière hétérogène.

    Je trouve que l'approche a le mérite d'être simple, que le code est très lisible, que l'on peut facilement "ajouter" une api à un Type (il suffit de déclarer que ce type implémente une api et d'écrire la méthode), le tout sans "class", "héritage", et "encapsulation", et (je peux me tromper) je n'y vois que des avantages. C'est la "combinaison" d'un type et d'action qui est séduisante, car simple, facilement utilisable, laissant chaque type être "indépendant".

    A là limite, le langage Home pourrait ne même pas aborder la notion de "POO" tout en permettant d'en faire autant, me semble-t-il...

    Merci d'avance.

    BàV et Peace & Love.

  9. #109
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    Comme je l'ai écris, le Duck Typing mais avec typage statique et vérification à la compilation que nous n'ayons pas d'erreur "méthode non implémentée" a un nom, c'est le typage structurel (par opposition au typage nominal de C++/Java).

    J'ai illustré dans mon propos comment c'est implémenté dans OCaml. Tu peux décrire toutes les classes que tu veux, indépendamment, et les utiliser comme des interfaces en Java (et ce sans avoir à modifier les classes, qui se trouve implémenter les même méthodes que l'interface, sans en hériter).

    Lorsque tu crées, une liste, normalement, tu sais quelle actions tu souhaites utiliser avec les membre de la liste. Donc tu es en mesure de déclarer une classe/interface. Avec OCaml, il te faut convertir les instances dans le type des objets associés à cette liste, en tapant simplement [obj1:>interface; obj2:>interface]. Tu peux imaginer un principe où le interface est factorisé (ce serait un let l : List<interface> = [obj1;obj2]; par exemple, dans un langage hypothétique). Mais en lisant la liste, le compilateur ne peux pas savoir quelles méthodes doivent être présentes si tu ne le précise pas. Dans ces exemple, s'il manque à obj2 une méthode, le compilateur sait comparer la liste des méthodes de obj2 à celle de l'interface.

    Je cite "tout en évitant le "couplage" (encapsulation) de la POO, qui "force" a créer les types de "base" d'une hiérarchie tout en essayant de "deviner" comment cette hiérarchie va évoluer" en OCaml, grâce au typage structurel, tu n'as pas à "deviner", mais cela n'a rien a voir avec l'encapsulation (qui permet d'empêcher l'accès aux champs interne, privés, sans passer par les méthodes publique).

    La limitation d'OCaml, si on doit en trouver une est le cas, où tu as fait un objet, et tu souhaites l'utiliser dans un contexte où il lui manque une méthode. Certains langages ont des modalité pour ajouter après coup quelque choses, ce sont les traits (Scala, Rust...), ou les mixins. Il est possible de contourner avec des objets facade ou decorator.

  10. #110
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Bonjour floyer


    Citation Envoyé par floyer Voir le message
    Comme je l'ai écris, le Duck Typing mais avec typage statique et vérification à la compilation que nous n'ayons pas d'erreur "méthode non implémentée" a un nom, c'est le typage structurel (par opposition au typage nominal de C++/Java).
    Je ne connaissais pas ce terme "Typage Structurel". Mais je comprend mieux maintenant ce dont vous parlez.

    Citation Envoyé par floyer Voir le message
    J'ai illustré dans mon propos comment c'est implémenté dans OCaml. Tu peux décrire toutes les classes que tu veux, indépendamment, et les utiliser comme des interfaces en Java (et ce sans avoir à modifier les classes, qui se trouve implémenter les même méthodes que l'interface, sans en hériter).
    Malheureusement, je n'ai qu'une connaissance très superficiel d'OCaml :-/. De par mon parcours, et mon diplôme, j'ai plus eu tendance a faire du Basic(sur C64) / Pascal (à lécole) C (dans l'embarqué) / C++ pour des application "Desktop", que j'ai avec bonheur abandonner pour Python (via wxPython). Je voyais (à tors je pense) OCaml, comme Lisp et Oberon, comme des langages "académiques". Ada était "entre les deux".

    Citation Envoyé par floyer Voir le message
    Lorsque tu crées, une liste, normalement, tu sais quelle actions tu souhaites utiliser avec les membre de la liste. Donc tu es en mesure de déclarer une classe/interface. Avec OCaml, il te faut convertir les instances dans le type des objets associés à cette liste, en tapant simplement [obj1:>interface; obj2:>interface]. Tu peux imaginer un principe où le interface est factorisé (ce serait un let l : List<interface> = [obj1;obj2]; par exemple, dans un langage hypothétique). Mais en lisant la liste, le compilateur ne peux pas savoir quelles méthodes doivent être présentes si tu ne le précise pas. Dans ces exemple, s'il manque à obj2 une méthode, le compilateur sait comparer la liste des méthodes de obj2 à celle de l'interface.
    C'est très proche de mon approche, me semble-t-il, sauf que c'est au niveau du type que ce dernier "déclare" implémenter telle ou telle interface. Ceci permet lors de la création de la "table" (pour appeler la bonne fonction sur le bon type) que cette dernière ne prenne en compte que les apis, réduisant sa taille. Aussi, il n'y a pas besoin de "convertir" des instances de classe. Le List<interface> = [obj1, obj2] n'est pas nécessaire (mais pourrait devenir nécessaire lorsque j'implémenterai la version "dynamique" dans Home), car il sait si le type implémente ou pas une des interfaces (api) nécessaire.

    Citation Envoyé par floyer Voir le message
    Je cite "tout en évitant le "couplage" (encapsulation) de la POO, qui "force" a créer les types de "base" d'une hiérarchie tout en essayant de "deviner" comment cette hiérarchie va évoluer" en OCaml, grâce au typage structurel, tu n'as pas à "deviner", mais cela n'a rien a voir avec l'encapsulation (qui permet d'empêcher l'accès aux champs interne, privés, sans passer par les méthodes publique).
    Pour le moment, on peut accéder à toutes les variables qui constituent un Type. J'ai toujours eu du mal avec ce principe. En C, il ne viendrait jamais l'idée à quelqu'un d'aller "changer" ce qui se trouve dans un handler de fichier (FILE). Les "champs" d'une structure sont publics, et pourtant correctement utilisés. Dans Home, comme les "struct" et "types" se ressemblent fort, je pense interdire l'accès aux membres dont le nom se termine par un '_'.

    Citation Envoyé par floyer Voir le message
    La limitation d'OCaml, si on doit en trouver une est le cas, où tu as fait un objet, et tu souhaites l'utiliser dans un contexte où il lui manque une méthode. Certains langages ont des modalité pour ajouter après coup quelque choses, ce sont les traits (Scala, Rust...), ou les mixins. Il est possible de contourner avec des objets facade ou decorator.
    Les mixins sont utilisés en python (mais c'est un langage dynamique au bytecode interprété). Les "traits" (en Scala ou Rust) implique une "notion" supplémentaire (pas forcément compliquée, mais ça reste une "notion" de plus). Pouvoir "contourner" avec des objets "facade" ou "décorator", il faut à la base utiliser des objets (dans le sens qu'il faut les définir via une "class"), une notion (pas compliquée certes), mais une notions de plus quand même.

    J'essaye dans Home de me débarrasser de toutes "notions". Lorsque je me suis mis en tête de développer un "compilateur", je ne voulais pas "refaire" un nième compilateur pour un langage existant. Le fait de définir un "nouveau langage" a pour but exactement de ne pas être "enfermé" dans des "principes/notions", et d'avoir toute la liberté possible de "faire autrement", "plus simplement", c'est là que j'ai commencé a définir Home.

    De mes connaissances en C++, j'ai retenus qu'il fallait (lorsque c'était possible) de privilégier la composition à l'héritage, et qu'il fallait "programmer" pour une "interface" et pas pour un type "concret". Dans Home, la "composition" est faite via la "combinaison" d'un type et d'une action. Et on programme pour une "interface" via les apis.

    Ce découplage complet permet toutes les "combinaisons" possibles, tout en gardant chaque type "indépendant" et "ajouter" des "comportement" se fait très facilement en ajoutant à un type (sans héritage, sans hiérarchie, sans class) une méthode respectant une api. C'est simple et évite d'introduire trop de "concepts", le tout en gardant (AMHA) le langage très simple a apprendre, et son "code source" très lisible. Avoir un code source "lisible" est très important pour moi, car de mon expérience, on "lit" bien plus souvent du "code" qu'on en écrit.

    Je vais continuer le développement de Home, et si je tombe sur un "blocage" dans mon approche, et bien j'ajouterais au langage le minium possible pour faire sauter ce blocage. Le langage Home évolue en // avec le développement de son compilateur, et c'est très instructif et bien plus plaisant, amusant.

    BàV et Peace & Love.

  11. #111
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Bonjour, votre avis svp ...


    Je suis actuellement en train d'analyser comment implémenter un système de "coroutine", j'ai beau chercher, je ne vois pas un seul cas où les coroutines ont un avantage par rapport à un RTOS (donc préemptif et non coopératif).

    Un exemple, si une coroutine se suspend en attendant une ressource, et que l'on a besoin de cette ressource pour continuer, comment savoir quel "coroutine" il faut réveiller, pour faire quoi ? Car généralement, lorsque l'on a besoin d'une ressource, on ne peut rien faire sans l'avoir.

    Par exemple, dans FreeRTOS, les coroutines sont gérées par la task "Idle", la seule tâche qui est toujours à l'état 'READY' mais qui a la priorité la plus faible. C'est à dire qu'on est dans cette task "Idle" tant qu'une autre tâche de plus haute priorité devient "READY".

    Aussi, je ne vois vois pas d'explications "claires" sur ces "coroutines", et il y'a apparemment plusieurs façon de les implémenter et même de les utiliser (tous les langages supportant les coroutines n'offrent pas forcément les mêmes "services").

    Les coroutines "stackless" permettent d'éviter des allocations dynamiques, mais certains cas d'utilisations sont alors proscrits. Les coroutines "stackfull" nécessitent des allocations "dynamiques", ce qui revient (peut-être en un peu moins gourment) à un TCB (Task Control Bloc) dans un RTOS.

    Ok, dans un RTOS, chaque tâches doit avoir sa "stack", mais on peut "calculer" la taille de la stack nécessaire à une task, et on a au final plusieurs petites stack au lieu d'une seule "grande" stack.

    Mais un RTOS a des avantages (en temps de réaction, en terme de priorisation, etc...). Le RTOS nécessite un Scheduler, mais les coroutines aussi...

    Voici un petit exemple de code en Home qui est un "début" de solution pour l'implémentation des coroutines:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    program coroutines:
    
        fct data() <- int8 value:
            value := 69
            produce value
        end
    
        fct calc(int8 x) <- int16 result:
            int16 d[idx=10] = 50
            int16 y=3
            for item in d[idx]:
                result := item + require data()
            next
        end
    
        fct add(int8 a, b) <- int16 sum:
            int16 c
            c := a + 2 * b
            c := calc(10)
            sum := c
        end
    
        int16 sum=0
        sum := add(10,3)
    end
    PS: "require" == "wait ou await" et "produce" == "Yield". (c'est juste une question de vocabulaire)

    Lorsque calc(10) est appelé dans la fonction add, elle prend un élément du tableau (item) et y ajoute "require data()" pour retourner à add une valeur lorsque "data()" aura répondu (là c'est instantané, mais disons que data prenne du temps avant de répondre).

    Que faire ? Calc ne sait rien faire sans que data() n'ait "répondu", et donc add non plus. De plus, comment savoir quelle autre "coroutines" existante on pourrait "réactivé" ? Sur quelle base décider ?

    Dans mon cas, "require" attend une "expression", et "produce" produit cette expression. Le seul avantage, c'est de pouvoir retourner des élément 1 à 1 (un générateur en quelque sorte). L'utilité dans Home, c'est qu'il n'a pas de mot-clef "return" (le return est automatique à la fin d'une fonction, ce qui interdit d'avoir plusieurs "points de sortie" d'une fonction, qui est généralement admis comme étant une mauvaise pratique), c'est de pouvoir "produire" un résultat au beau milieu d'une fonction (comme on ferait avec un return)).

    Actuellement "require" ne "suspend" pas "calc()" et le "produce" de data() non plus. On pourrait les retirer que le code fonctionnerait tout aussi bien (mais en introduisant un "mot-clef" return à la place de "require", et retourner la valeur dans data sans avoir besoin de "produce".

    En fait, je ne vois pas de cas où les coroutines apportent un quelconque avantage par rapport à un RTOS qui lui apporte énormément d'autres avantages, est plus "carré", et même plus simple que le fouillis que sont pour moi (mais comme toujours, je peux me tromper) les coroutines. Un await n'a aucun sens lorsque qu'on utilise un RTOS (car le RTOS a déjà ce genre de fonctionnalité), par exemple.

    Merci d'avance de vos réflexions.

    BàV et Peace & Love.

  12. #112
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    La gestion des coroutines est plus légère, donc avoir des coroutines peut être utile avec des milliers d’entre elles simultanées.

  13. #113
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut oui et non
    floyer,

    Citation Envoyé par floyer Voir le message
    La gestion des coroutines est plus légère, donc avoir des coroutines peut être utile avec des milliers d’entre elles simultanées.
    Elles sont plus légère, ça j'ai encore du mal à avoir une idée précise, mais je te crois et je suis ton raisonnement. Les coroutines doivent (pour éviter des limitations) avoir une "activation frame", qui est (si je comprend bien ce que j'étudie en ce moment) séparée en deux, une CO-FRAME (allouée dans le heap), et une CO-STACK-FRAME sur la stack. Les CO-FRAMES sont allouées et désallouées en permanence (suivant les "suspend-points") durant la vie d'une coroutines.

    S'il y en a des milliers, ça fait énormément de temps "perdu". Aussi, s'il y en a des milliers (simultanées forcément), ça doit consommer bien plus (pour les milliers de CO-FRAME) que quelques task, car une "task" n'a besoin d'allouer sa stack qu'une seule fois (ça peut même être fait statiquement si on sait combien de tasks on a besoin (ou qu'on veut)). Une task peut aussi avoir des centaines d'équivalent de suspend-point (à chaque appel système, ou à charque sortie d'une interruption), mais le Scheduler est lui relativement simple (sauver un contexte dans un TCB (déjà alloué)), remettre le TCB d'une task, et faire le "task-switching"). Simple, efficace (car c'est la task READY de plus haute priorité qui sera toujours activée), bien plus réactif qu'un ensemble de coroutines (car dès qu'une ressource attendue se libère, ce qui se fait par un appel système, la task est immédiatement réactivée).

    La coopération des coroutines me posent aussi problème, car je ne vois pas sur quelle critère le "scheduler" choisit la coroutine a réactiver lorsque celle en cours se suspend (car arrivée sur un suspend-point). On trouve beaucoup d'info sur l'utilisation des coroutines, mais très peut sur la manière dont le "scheduler" les traite, donc sur la manière dont elles coopèrent réellement. Je comprend le principe, mais je ne vois absolument pas comment faire ce "scheduler". Si ce scheduler doit conserver des infos pour des milliers de coroutines, et traiter ces info pour choisir la coroutine a réactiver, c'est beaucoup de temps "perdu".

    J'ai peut-être un biais de raisonnement car je sais comment fonctionne un RTOS pour en avoir déjà réaliser un, alors que les "coroutines" (qui me semblaient élégantes) ne semble pas apporter grand choses si on a un RTOS.

    Un autre avantage que je vois, c'est qu'une "task" peut s'occuper d'un seul fonctionnement comme si elle avait toujours le CPU pour elle toute seule. Cela permet de "facilement" séparer les différentes "fonctionnalités" d'un firmware ou d'un programme.

    Je continue de creuser le sujet (ne fusse que par curiosité), mais plus je creuse, plus je trouve la solution RTOS plus simple et élégante, plus rapide, plus précise, plus "ordonnée" et finalement demandant moins de temps CPU que la coopération des coroutines. Mais je changerais peut-être d'avis si je trouve une explication claire et précise sur la manière dont fonctionne le scheduler des coroutines.

    Merci de ton temps,

    BàT et Peace & Love.

  14. #114
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    Avec les coroutines, tu peux limiter les commutations de contextes au strict minimum, vu que c’est collaboratif. Cela marche bien pour des applications où les temps de calculs sont assez courts pour que la monopolisation du CPU ne soit pas gênante. (Les applications qui font beaucoup d’I/O, attente de base de données… autres serveurs, sont dans ce cadre). Pour une application plus intensive en CPU, où tu t’intéresses plus à la latence qu’au débit, et donc avec une gestion fine des priorités, les threads systèmes sont à privilégier.

    Deux types d’optimisation, deux outils différents…

    Cf https://discuss.ocaml.org/t/eio-libr...amming/13166/5 où Lwt, Async et Eio sont trois implémentations de coroutines.

    Note aussi, qu’une commutation de coroutine est assez légère car elle intervient à un moment où il n’est pas nécessaire de sauver tous les registres… alors qu’une commutation préemptive est obligée de le faire… sans compter le changement de contexte user/system.

    Pour info, Eio utilise des piles natives… et Lwt et Async se basent sur un programmation monadique assez spécifique aux langages fonctionnels. Je ne suis pas sûr qu’il existe beaucoup d’implémentation de coroutines stackless, les approches monadiques font peut-être exception.

  15. #115
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Bonsoir
    re floyer

    Citation Envoyé par floyer Voir le message
    Avec les coroutines, tu peux limiter les commutations de contextes au strict minimum, vu que c’est collaboratif. Cela marche bien pour des applications où les temps de calculs sont assez courts pour que la monopolisation du CPU ne soit pas gênante. (Les applications qui font beaucoup d’I/O, attente de base de données… autres serveurs, sont dans ce cadre).
    Je comprend cela, mais un RTOS lui-aussi, limites les commutations de contexte, pour qu'un changement de contexte se produise, il faut qu'une "ressource se libère", où qu'un temps "déterminé" soit effectué pour provoquer un "task-switch". Dans l'embarqué, j'ai du batailler pour imposer l'utilisation d'un RTOS dans les produits. Là où le CPU était "chargé à 80%" (dans une grosse mainloop), on retombait à "20%" avec des tasks qui n'utilisent le CPU que lorsque nécessaire. Et une fois que tu a utilisé un RTOS, tu n'as plus jamais envie de revenir en arrière, tant son utilisation simplifie et force a bien "modulariser le programme.

    Citation Envoyé par floyer Voir le message
    Pour une application plus intensive en CPU, où tu t’intéresses plus à la latence qu’au débit, et donc avec une gestion fine des priorités, les threads systèmes sont à privilégier.
    Je dois préciser que langage Home fait partie d'un projet plus large, le Homeputer, qui (via une VM) reproduira l'équivalent au minimum d'un micro computer genre C64 jusqu'à un PC-XT utilisant un 8086. Le kernel d'un C64 n'était pas un RTOS, et le MS-DOS d'un XT non plus. Le Homeputer disposera d'un µKernel minimum (Quelques Ko) et pourra "faire tourner" des programmes similaires à ceux de cette époque. Notamment des jeux où la "réactivité" est primordiale. Sont CPU sera un CPU "virtuelle" qui pourrait se situer entre un 6510 et un 8086. Le "task-switch" sera léger car le CPU lui-même aura des capacités limitées. Le Home n'est pas un langage qui "exploiterait" les capacités d'un microprocesseur récent. Ce n'est pas son but. ET donc l'OS (ou le RTOS) sera très très léger. Ce RTOS n'utilisera pas des "process", ni même des "thread", mais simplement des "tasks".

    Citation Envoyé par floyer Voir le message
    Deux types d’optimisation, deux outils différents…

    Cf https://discuss.ocaml.org/t/eio-libr...amming/13166/5 où Lwt, Async et Eio sont trois implémentations de coroutines.
    Merci des liens, je les lirais avec attention.

    Citation Envoyé par floyer Voir le message
    Note aussi, qu’une commutation de coroutine est assez légère car elle intervient à un moment où il n’est pas nécessaire de sauver tous les registres… alors qu’une commutation préemptive est obligée de le faire… sans compter le changement de contexte user/system.
    Le µKernel ne fera usage de deux modes. Pas de "mode user" et de mode "système". Ce ne sera pas nécessaire. Les rôles de ce µKernel/RTOS sera de réaliser les "task-switch", de réagir à des appel système (déclenchés via une INT software), aux demandent des "devices" (via un INT hardware), de gérer les "ressources partagées" et les allocations dynamiques.

    Citation Envoyé par floyer Voir le message
    Pour info, Eio utilise des piles natives… et Lwt et Async se basent sur un programmation monadique assez spécifique aux langages fonctionnels. Je ne suis pas sûr qu’il existe beaucoup d’implémentation de coroutines stackless, les approches monadiques font peut-être exception.
    Je n'ai pas eu l'occasion d'utiliser de langage "fonctionnels" et je vais me renseigner sur ce qu'est une programmation "monadique", dont pour te l'avouer, je n'avais jamais entendu parler, re -

    Merci pour tout et à bientôt.

    BàT et Peace & Love.

  16. #116
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    Les monades nécessitent un language fonctionnel. Il y a plein d’usage, mais dans le cas qui nous intéresse, c’est ce qui permet au développeur de Lwt ou Async d’implémenter des coroutine sans pile, et sans intervention de bas niveau (sauvegarde de registres, état de la pile…),

    Le principe est d’écrire (ici en Haskell) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     do
      line <- getLine    -- lit une ligne depuis l'entrée
      putStrLn line      -- affiche la même ligne
    Et c’est plus compliqué en interne que l’on croit.

    C’est converti en (pseudo-language) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    IO.bind ( getLine, function line-> putStrLn line )
    Ce qui retourne immédiatement une structure qui lorsqu’elle est ordonnancé lit une ligne (getLine) et prévoit d’ordonnancer putStrLn avec le résultat de la lecture. Le coeur de l’ordonnanceur est caché dans IO.bind.

    Cela nécessite une habitude, mais est assez puissant pour faire des contrôles de flux inhabituels. Comme les STM (https://en.m.wikipedia.org/wiki/Soft...ynchronization.) dont j’ai vu une implémentation en Haskell https://book.realworldhaskell.org/re...al-memory.html et en OCaml https://tarides.com/blog/2023-08-07-...for-ocaml-1-2/

    On peut aussi tapper :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    let (let*) = Fun.flip List.concat_map;;
    let* a=[1;2;3] in 
    let* b=[1;10;100] in 
    [a*b];;
    Pour avoir le résultat [1; 10; 100; 2; 20; 200; 3; 30; 300].

    C’est une écriture plus sympa que ce qui suit où >>= est l’opérateur bind pour les listes.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    let (>>=) = Fun.flip List.concat_map
    [1;2;3] >>= fun a ->
    [1;10;100] >>= fun b ->
    [a*b]

  17. #117
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Tu m'épate mon ami...
    floyer,

    Oui, franchement, tu m'épate. Tu as bien plus de connaissance que moi concernant bien plus de langages. Je me sens tout petit.

    Citation Envoyé par floyer Voir le message
    Les monades nécessitent un language fonctionnel. Il y a plein d’usage, mais dans le cas qui nous intéresse, c’est ce qui permet au développeur de Lwt ou Async d’implémenter des coroutine sans pile, et sans intervention de bas niveau (sauvegarde de registres, état de la pile…),

    Le principe est d’écrire (ici en Haskell) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
     do
      line <- getLine    -- lit une ligne depuis l'entrée
      putStrLn line      -- affiche la même ligne
    Et c’est plus compliqué en interne que l’on croit.

    C’est converti en (pseudo-language) :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    IO.bind ( getLine, function line-> putStrLn line )
    Ce qui retourne immédiatement une structure qui lorsqu’elle est ordonnancé lit une ligne (getLine) et prévoit d’ordonnancer putStrLn avec le résultat de la lecture. Le coeur de l’ordonnanceur est caché dans IO.bind.

    Cela nécessite une habitude, mais est assez puissant pour faire des contrôles de flux inhabituels. Comme les STM (https://en.m.wikipedia.org/wiki/Soft...ynchronization.) dont j’ai vu une implémentation en Haskell https://book.realworldhaskell.org/re...al-memory.html et en OCaml https://tarides.com/blog/2023-08-07-...for-ocaml-1-2/

    On peut aussi tapper :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    let (let*) = Fun.flip List.concat_map;;
    let* a=[1;2;3] in 
    let* b=[1;10;100] in 
    [a*b];;
    Pour avoir le résultat [1; 10; 100; 2; 20; 200; 3; 30; 300].

    C’est une écriture plus sympa que ce qui suit où >>= est l’opérateur bind pour les listes.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    let (>>=) = Fun.flip List.concat_map
    [1;2;3] >>= fun a ->
    [1;10;100] >>= fun b ->
    [a*b]
    Je vais tenter d'étudier tout cela, mais cela risque de me rendre du temps. Je vais donc faire cela "petit à petit", mais je dois aussi garder en vue que Home doit rester simple et lisible, et ne doit pas noyer le développeur final sous trop de "concepts". Ceci est lié à la finalité de l'ensemble de mon projet Homeputer. Je peux y mettre des concepts difficiles à implémenter, mais ils doivent rester facile à comprendre et donc a utiliser, par le développeur final. Je dois un peu me "reconcentrer" sur le "core" de mon langage.

    J'ai regardé le principes des coroutines, pour voir s'il pouvait apporter quelque chose au développeur final de Home, mais je ne peux pas faire le même exercice pour touts les "concepts", au risque de me noyer moi-même... J'avais déjà fait le même "exercice" pour comprendre les mécanismes utilisés par Rust pour que Home soit safe & secure, mais en rendant cela plus simple pour l'utilisateur final.

    Je suis persuadé qu'il y a encore des "tonnes" de "concepts" que j'ignore, mais je n'ai pas le temps de me pencher dans le détail sur chacun d'eux, mais je le ferais petit à petit.

    Je ne peux que te remercier infiniment pour tout les conseils, concepts et du temps que tu consacre à me répondre.

    Là, je vais donc me "remettre" dans le "cadre" de l'objectif premier de Home, et peut-être "picorer" quelques "concepts" par-ci, par-là, mais tout en gardant une "cohérence" syntaxique et une "facilité" d'utilisation, même si cela impose des limites.

    Encore merci.

    BàT et Peace & Love.

  18. #118
    Membre éclairé
    Homme Profil pro
    autre
    Inscrit en
    Septembre 2015
    Messages
    542
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : autre

    Informations forums :
    Inscription : Septembre 2015
    Messages : 542
    Par défaut
    Comme il m’a semblé t’avoir intrigué avec les monades, je t’ai mis un petit panoramara du principe et des usages. Mais effectivement, pour ton usage, je pense que tu peux te limiter aux fonctions intégrées à ton RTOS.

  19. #119
    Membre confirmé
    Homme Profil pro
    Développeur en systèmes embarqués
    Inscrit en
    Mai 2015
    Messages
    463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 54
    Localisation : Belgique

    Informations professionnelles :
    Activité : Développeur en systèmes embarqués

    Informations forums :
    Inscription : Mai 2015
    Messages : 463
    Par défaut Et je t'en remercie :-)


    Oui, honnêtement, tes "points de vue" sont toujours intéressants, argumentés, expliqués, et ce sans "condescendance" aucune. Cela en dit long sur la personne que tu es. C'est vraiment un plaisir de dialoguer de la sorte.

    Citation Envoyé par floyer Voir le message
    Comme il m’a semblé t’avoir intrigué avec les monades, je t’ai mis un petit panoramara du principe et des usages. Mais effectivement, pour ton usage, je pense que tu peux te limiter aux fonctions intégrées à ton RTOS.
    Je suis en effet "intrigué" par tout ce que je ne connais pas bien, je ne mets rien de côté sans essayer d'avoir un jugement "honnête" suivant mes objectifs. Je pense comme toi que l'approche RTOS est la meilleur dans mon cas, pour ce que je veux permettre de faire avec Home, et qui s'intègre dans la "philosophie" du projet Homeputer dans son ensemble.

    Dès que j'aurais quelque de d'utilisable, (et il ne faut pas que je me dissipe trop pour cela), tu sera l'un des premiers au courant.

    Merci et à Bientôt.
    BàT et Peace & Love.

Discussions similaires

  1. Réponses: 3
    Dernier message: 04/05/2024, 23h30
  2. Noulith : un nouveau langage de programmation construit sur Rust
    Par Bruno dans le forum Langages de programmation
    Réponses: 1
    Dernier message: 08/01/2023, 21h41
  3. Nouveau langage, conseils sur la grammaire
    Par Christophe Genolini dans le forum ALM
    Réponses: 0
    Dernier message: 21/08/2013, 11h09
  4. Nouveau langage : le D
    Par cheick dans le forum D
    Réponses: 4
    Dernier message: 30/05/2004, 16h56

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo