Seventh, le micro langage impératif de ZDS !

Parce que les lisp-like, c'est cool, mais on aime aussi l'impératif nous !

a marqué ce sujet comme résolu.

Je comprends que l'on puisse les utiliser pour d'autres choses. Je voulais juste revenir sur le fait qu'il dise que ce soit pour ce genre de problèmes que c'est fait à la base. Je t'avoue que je chipote un peu. ;) Source: Wikipédia

mehdidou99

Et une erreur de compilation, tu ne considères pas ça comme un cas exceptionnel, qui mérite donc son exception ?

Pour moi, une erreur de compilation fait partie du fonctionnement normal d'un compilateur, mais ce n'est que mon avis, basé sur la connaissance de… rien en ce qui concerne la création de compilateurs. :euh:

Plus sérieusement, je ne voyais pas ça sous cet angle, c'est vrai que c'est une façon de voir les choses. Mais est-ce la meilleure ? C'est pourquoi j'ai posé ma question.

D'ailleurs, je suis très intéressé par la réponse d'Eusèbe, vivement qu'il repasse ici pour l'expliquer ! :)

EDIT : Après avoir réfléchi un peu plus de deux secondes, je crois comprendre ce que propose Eusèbe, mais je ne suis pas sûr.

+0 -0

Un petit exemple pour voir si j'ai compris ce qu'avait dit Eusèbe (je doute fortement d'avoir compris) :

 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
71
72
73
74
75
76
77
78
79
80
#include <functional>

template<typename T>
bool error(T const& value)
{
    return value.error();
}

template<typename T>
class ErrorChecker
{
public:
    using ValueType = T;
    using CallbackType = std::function<ValueType(ValueType const&)>;

    ErrorChecker(T const& value)
    : m_value{value}
    {
    }

    ErrorChecker(T&& value)
    : m_value{std::move(value)}
    {
    }

    ValueType get(CallbackType const& onError) const
    {
        if(error(m_value))
            return onError(m_value);

        return m_value;        
    }

private:
    T m_value;
};

// exemple :

#include <iostream>

struct MySuperType
{
    ValueType value;

    bool errorFlag;

    bool error() const
    {
        return errorFlag;
    }
};

MySuperType mySuperFunction1()
{
    MySuperType value{};

    value.value = computeValue();

    if(somethingWrongDetected())
        value.errorFlag = true;

    return value;
}

void mySuperFunction2(ErrorChecker<MySuperType> errorChecker)
{
    auto printError = [](MySuperType const& value)
                        {
                            std::cout << "Error detected !" << std::endl;
                            return value;
                        };

    doSomethingWithValue(errorChecker.get(printError))
}

void mySuperFunction3()
{
    mySuperFunction2(mySuperFunction1());
}

(Vous voulez des commentaires ou c'est compréhensible ?)

Eusèbe, est-ce quelque chose comme ça que tu décrivais ?

EDIT : A améliorer : ce serait bien de permettre l'utilisation d'un type plus souple que bool pour représenter l'état d'erreur (facile à faire, deux-trois templates et une politique et c'est fini).

+0 -0

Qu'est-ce que cette solution apporte par rapport aux exceptions ? Avec cette technique tu devras constamment vérifier si la valeur est erronée ou pas. C'est exactement ce qu'on fait dans des langages fonctionnels comme en Haskell ou en OCaml (et même Rust) mais le système de type de C++ rend la tâche plus fastidieuse (en Haskell t'as juste à faire >>= et c'est géré pour toi). Surtout que C++ gère nativement les exceptions, tu sembles juste créer un système d'exception qui n'interrompent pas le cours du programme, alors qu'une erreur de compilation devrait.

Avec cette technique tu devras constamment vérifier si la valeur est erronée ou pas.

AlphaZeta

Non, il n'y a pas besoin de vérifier. Au lieu d'accéder à la valeur, on appelle ErrorChecker::get.

tu sembles juste créer un système d'exception qui n'interrompent pas le cours du programme, alors qu'une erreur de compilation devrait.

AlphaZeta

Une erreur de compilation ne doit pas interrompre le programme (le compilateur doit, s'il peut, il essayer de détecter le maximum d'erreurs).

Par contre, il faudrait que la callback soit une variable membre publique pour qu'on puisse la régler à long terme au lieu de la passer en paramètres à chaque fois ; sans ça, je suis d'accord qu'on perd un peu l'intérêt de la chose.

Enfin, je ne suis pas sûr non plus que cet exemple a un quelconque intérêt, j'essaie juste d'approcher la solution proposée par Eusèbe, que je n'ai pas bien compris. Et très franchement, je pense que je n'ai toujours pas compris. :)

EDIT : Je viens de penser, si, en variable membre, on ne met pas une callback mais une liste de callbacks ? Ça pourrait devenir intéressant, car à chaque "niveau de complexité" du compilateur, le niveau supérieur pourrait ajouter une callback en plus pour gérer l'erreur de manière à pouvoir continuer la compilation. Qu'en pensez-vous ?

+0 -0

Non, là ton compilateur ne vérifie rien : si tu enlèves les lignes de gestion d'erreur, ton implémentation en C++ compilera quand même (et ça plantera à l'exécution, quand tu la lanceras sur un programme zlang qui contient des erreurs). Par ailleurs, il se trouve qu'effectivement c'est nettement moins élégant que dans un langage comme OCaml, où on pourrait écrire en simplifiant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type result =
  | Ok of int
  | Error

let div(result1, result2) =
  match (result1, result2) with
  | Ok a, Ok b ->
    if b = 0
    then Error
    else Ok (a / b)
  | Error, _ 
  | _, Error -> Error

Le type result (une sorte d'énumération) décrit des valeurs qui sont en gros « soit un entier n avec le tag Ok, soit le tag Error tout seul ». La fonction div prend en paramètre deux résultats, et si ces résultats sont bien deux entiers (c'est ce que vérifie le match), renvoie le résultat de leur division avec l'étiquette Ok (sauf si le deuxième est nul, auquel cas elle renvoie une erreur). Si un des deux résultats passés en paramètre est lui-même une erreur, alors la fonction renvoie une erreur (attention, Error est une valeur du langage, pas une erreur qui fait planter le programme). Le type de div est alors result * result -> result

Le compilateur va s'assurer pour moi que j'ai bien traité les cas d'erreur possibles : si j'oublie dans les cas du match ceux qui correspondent à une erreur sur un des deux résultats, il m'avertira (par contre, la division d'OCaml renvoie une exception en cas d'erreur, donc je suis obligé de faire la vérification b = 0 moi-même, et ça le compilateur ne m'y force pas).

Alors bien sûr, écrit comme ça ça ne fait pas forcément envie, parce que ça paraît un peu lourdingue à manipuler. En pratique on définirait une fonction qui factorise un peu la vérification des erreurs, et ça ressemblerait à ça :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
let ( >>= ) result f =
  (* L'opérateur qui factorise la gestion des erreurs *)
  match result with
  | Ok x -> f x
  | Error -> Error

let division a b =
  (* Une fonction qui prend deux entiers (sans étiquette) et renvoie le résultat de la division *)
  if b = 0
  then Error
  else Ok (a / b)

let div2 result1 result2 =
  (* Une fonction qui prend deux résultats et renvoie un résultat *)
  result1 >>= fun a ->
  result2 >>= fun b ->
  division a b

Si tu n'as jamais fait de programmation fonctionnelle typée, tu risques d'avoir un peu de mal à comprendre ce code là. L'important, c'est la dernière fonction (div2), qui correspond exactement à la fonction div de tout à l'heure, mais écrite de façon beaucoup plus lisible (on peut la lire comme « si result1 contient l'entier a, et si result2 contient l'entier b, alors je renvoie le résultat de la division de a par b » (et les cas d'erreur sont gérés tout seuls par >>=).

Pour la culture, ce genre d'interface (le type + la fonction >>=) est (une partie de) ce qu'on appelle une « monade ». Les haskelleux en sont fan et pensent que c'est une solution magique à tout un tas de problèmes, mais maintenant tu sais que c'est aussi simple que ça :P

+2 -0

Certains savent ô combien j'aime les monades et la programmation fonctionnelle, je pense que c'est une erreur de mimer ce comportement en C++. Les méthodes de résolution d'un problème donné sont propres à un paradigme. Pour bien programmer en un langage, il faut en adopter la philosophie, sinon ça devient vite un peu crade (en tout cas trop lourd, après ça peut-être plus typesafe). Il faut apprendre à bien utiliser les outils de son langage. C++ possède déjà un système d'exception, à mon avis c'est une erreur (sans mauvais jeu de mot) de ne pas l'utiliser.

Encore une fois, je n'ai pas dit que je ne utiliserai pas les exceptions, j'ai juste demandé si quelqu'un avait une meilleure solution. J'attendais donc de voir la solution proposée par Eusèbe, car je n'avais pas compris qu'elle était spécifique aux langages fonctionnels. Je vais donc m'orienter vers les exceptions (sauf si je trouve moi-même un meilleur moyen).

N.B : Non pas que le paradigme fonctionnel n'existe pas en C++, mais ne nous mentons pas : ça ne serait pas terrible d'appliquer la solution d'Eusèbe en C++ car certains principes/outils de la programmation fonctionnelle ne sont pas forcément très pratiques à utiliser en C++ (voire n'existent pas). Cela dit, ça peut être marrant à implémenter pour rigoler, à l'occasion.

+0 -0

je n'avais pas compris qu'elle était spécifique aux langages fonctionnels

C'est pas spécifique aux langages fonctionnels, tu pourrais reproduire le type result de cette manière:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
template<typename T>
class Result {};

template<typename T>
class Error : public Result<T> {};

template<typename T>
class Ok : public Result<T> {
public:
    Ok(T value) : m_value{value} {}

    T getValue() {
        return m_value;
    }
private:
    T m_value;
};

… mais le code sera lourd parce que comme tu l'as dit C++ n'est pas fait pour ce genre de chose.

C++ possède déjà un système d'exception, à mon avis c'est une erreur (sans mauvais jeu de mot) de ne pas l'utiliser.

Je ne sais pas si c'est ce que tu veux dire, mais ton message laisse penser que « En OCaml on peut écrire une monade, mais en C++ ce n'est pas la peine parce qu'on a des exceptions ». Attention, ce n'est pas ce qu'il faut comprendre : ce sont deux solutions différentes, qui ont des propriétés différentes. Le programme avec les exceptions considère qu'a priori tout se passe bien, et que si éventuellement ça ne se passe pas bien c'est au programmeur de déclencher une erreur (et on ne vérifiera pas ça pour lui), alors que celui qui encore les erreurs au niveau des types considère qu'il est parfaitement possible qu'une fonction rencontre un problème, et qu'il faut gérer ces cas (et il vérifiera statiquement que cette gestion est faite correctement - au prix d'un programme légèrement plus verbeux, parce qu'on explicite le fait qu'on puisse rencontrer des erreurs).

Indépendamment du langage, si la première solution était la meilleure, la question ne se poserait même pas : on utiliserait des exceptions et voilà, roulez jeunesse. Mais en règle générale, il est évidemment préférable de savoir dès la compilation si notre programme comporte des erreurs ou non. La question se pose donc en OCaml : est-ce que cette sûreté supplémentaire justifie le petit coût supplémentaire à l'écriture du programme ? Ça dépend de plusieurs choses, et c'est à voir au cas par cas.

En C++, c'est différent. La bonne question n'est pas tellement « est-ce que c'est une bonne idée de simuler une monade alors qu'on a déjà les exceptions ? » : comme je l'ai dit plus haut, ce sont deux systèmes différents. La vraie question, c'est « est-ce qu'il est possible pour pas trop cher d'avoir des garanties statiques sur la gestion des erreurs dans mon interpréteur ? ». Et là, j'ai bien peur que la réponse soit non : il me semble que c'est techniquement possible d'écrire quelque chose qui ressemble en C++, mais ça fait appel à des utilisations très complexes des templates qui ne rentrent très clairement pas dans le « léger coût en verbosité ». Partant de là, il vaut mieux effectivement utiliser les exceptions : non pas parce que « c'est la philosophie », mais parce qu'on n'a pas tellement d'autre choix.

Edit :

N.B : Non pas que le paradigme fonctionnel n'existe pas en C++, mais ne nous mentons pas : ça ne serait pas terrible d'appliquer la solution d'Eusèbe en C++ car certains principes/outils de la programmation fonctionnelle ne sont pas forcément très pratiques à utiliser en C++ (voire n'existent pas). Cela dit, ça peut être marrant à implémenter pour rigoler, à l'occasion.

Attention, le « paradigme fonctionnel » est un concept très vague qui recouvre beaucoup de choses. En particulier, les langages mainstream commencent à en intégrer certaines idées (les fonctions anonymes « lambda », parfois les fonctions d'ordre supérieur, les fermetures, la récursivité avec un support plus ou moins correct des récursions terminales), mais c'est encore particulièrement rare d'y voir le composant essentiel du code que j'ai écrit, à savoir les types algébriques « à la ML » avec un filtrage de motif qui comprend un test d'exhaustivité. Ce n'est donc effectivement pas parce que « le paradigme fonctionnel existe en C++ » que n'importe quel programme exprimable dans un langage fonctionnel précis le sera aussi en C++ (typiquement, dès qu'il y a un typage un peu sioux, ça ne passe plus).

+2 -0

je n'avais pas compris qu'elle était spécifique aux langages fonctionnels

C'est pas spécifique aux langages fonctionnels, tu pourrais reproduire le type result de cette manière:

AlphaZeta

Comme tu le dis juste après, c'est surtout l'utilisation qui est assez spécifique au fonctionnel, et qui risque de devenir vite lourde.


Sinon, je suis tout à fait d'accord avec Eusèbe. Je vais tout de même essayer de réfléchir à un système de vérification statique (ça tombe bien, j'adore les templates ! :) ), et si je ne trouve pas mieux, il faudra me tourner vers les exceptions.

EDIT :

Attention, le « paradigme fonctionnel » est un concept très vague qui recouvre beaucoup de choses. En particulier, les langages mainstream commencent à en intégrer certaines idées (les fonctions anonymes « lambda », parfois les fonctions d'ordre supérieur, les fermetures, la récursivité avec un support plus ou moins correct des récursions terminales), mais c'est encore particulièrement rare d'y voir le composant essentiel du code que j'ai écrit, à savoir les types algébriques « à la ML » avec un filtrage de motif qui comprend un test d'exhaustivité. Ce n'est donc effectivement pas parce que « le paradigme fonctionnel existe en C++ » que n'importe quel programme exprimable dans un langage fonctionnel précis le sera aussi en C++ (typiquement, dès qu'il y a un typage un peu sioux, ça ne passe plus).

Eusèbe

Certes.

+0 -0

Sinon, je suis tout à fait d'accord avec Eusèbe. Je vais tout de même essayer de réfléchir à un système de vérification statique (ça tombe bien, j'adore les templates ! :) ), et si je ne trouve pas mieux, il faudra me tourner vers les exceptions.

Ouais, alors c'était une digression intéressante pour la culture, mais ce n'est vraiment pas le genre de question à se poser dans le cadre de ton projet. Tu peux évidemment t'amuser à chercher à faire ce genre de choses par ailleurs, mais si tu veux avancer ton zlang, fais au plus simple et ne cherche pas à te rajouter d'autres problèmes (et si ça peut te rassurer, si je devais le faire en OCaml, j'utiliserais probablement des exceptions).

+2 -0

Je suis en vacances, je vais donc pouvoir m'y mettre un peu plus sérieusement ! :)

Enfin, après avoir résolu mes problèmes avec mon archlinux…

EDIT : Je pars sur ce qu'a proposé Kje. Comment s'appelle cette version ? :)

H.S : D'ailleurs, je me suis toujours demandé : comment ça se prononce, "Kje" ? Ça me taraude depuis que je suis inscrit. :D

+1 -0

C'est peut-être le moment d'envisager autre chose qu'une compilation vers Python : ok, ça permet d'avoir un résultat qui marche, mais du coup vous vous retrouvez à simplement définir une syntaxe alternative pour python, et tout votre travail revient à écrire un analyseur syntaxique, ce qui est loin d'être la partie la plus intéressante d'un compilateur (et en plus, ce n'est même pas un travail d'analyse syntaxique très intéressant, parce que vous avez cherché à le simplifier au maximum).

Pour reformuler, vous vous êtes dits successivement :

  • « On va compiler vers Python, comme ça la partie après l'analyse syntaxique sera très facile »
  • « On va prendre une syntaxe très simple, comme ça l'analyse syntaxique sera très facile »

Et de fait vous avez un résultat, mais vous avez appris quoi ? :-)

+2 -1

H.S : D'ailleurs, je me suis toujours demandé : comment ça se prononce, "Kje" ? Ça me taraude depuis que je suis inscrit. :D

mehdidou99

Dans la mesure où il s’agit de la forme raccourcie de son ancien pseudo « Dr. Kristofjé », et sachant que son petit nom (je ne dévoile rien, il l’utilise sur GitHub) est Christophe Gabard, soit Kristof G, je pense pouvoir affirmer que ça se dit /kjé/, bien à la française.

Par ailleurs, je soupçonne que l’orthographe avec -j- plutôt qu’avec -g- soit une référence au lieutenant Kijé. ^^

+0 -0

Par ailleurs, je soupçonne que l’orthographe avec -j- plutôt qu’avec -g- soit une référence au lieutenant Kijé. 

Non. En fait mon "plus ancien pseudo" était ChristopheG (Origina, non ?). Un membre du staff sur OC m'avait nommé kristofjé un jour en référence a mon orthographe déplorable. J'ai trouvé ça pas mal et je l'ai adopté avant de le faire évoluer vers Kje aujourd'hui.

Mais très honnêtement je ne me suis jamais posé la question de la prononciation.

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