publicodes
publicodes copied to clipboard
Pouvoir spécifier une inversion de formule
Problème
Le problème à résoudre est le suivant :
a: b * 2
b: a * 50%
On a écrit ici les équations dans les deux sens, afin de pouvoir calculer a
à partir de b
et b
à partir de a
, sans avoir à utiliser inversion
. Problème : si la situation est vide, on abouti à un cycle.
Note : ce cas est rencontré aujourd'hui dans l'écriture des règles pour les indépendant (avec grosso modo
CA = revenu + cotiz
etrevenu = CA - cotiz
)
Limites de l'existant
Aujourd'hui, lorsqu'un cycle est détecté, on affiche un warning dans la console et la valeur retournée par le cycle est mise à null
. L'avantage est que les cycles dus aux équations double sans valeur de départ dans la situation n'aboutissent pas à un crash, et donnent une valeur cohérente avec le problème.
La limite de cette solution est qu'elle abouti à un echec silencieux de tous les cycles. Comme on ne peux pas vraiment différencier les cycles "voulus" (ceux de la situation ci-dessus) de ceux "non-voulus" (ceux qui n'étaient pas prévu, du à une mauvaise écriture des règles), on considère que tous sont corrects.
C'est une mauvaise approche. Il faut considérer au contraire que tous les cycles sont non voulus (sauf ceux qui sont réglés par la fonction résoudre la référence circulaire
, et lever une erreur. Il faut donc supprimer le cycle dans la formule ci-dessus.
Solutions
1. Solution valeur depuis la situation
Pour cela, on propose de pouvoir utiliser directement une valeur de la situation.
a: b * 2
b:
- si:
dans la situation: a
alors: a * 50%
- sinon: manquant
C'est d'ailleurs exactement ce que fait le mécanisme inversion
, qui va vérifier qu'une des valeur listée est définie dans la situation. Ajouter un tel mécanisme permettrai de le réecrire avec des briques de base plus logique.
L'autre possibilité serait de pouvoir directement référencer la valeur de la situation :
a: b * 2
b: situation . a / 2
L'avantage de cette solution, c'est qu'elle est très proche de la façon dont est implémentée la situation en l'état actuel des choses (un namespace spécifique).
Questions en suspend
Quid de les remplacements ? J'avoue avoir commencer à y réfléchir et de m'y être un peu perdu... Bref, à continuer à éclaircir sur la solution, peut-être qu'il y mieux ?
Solution abandonnée car :
La notion de "situation" n'existe pas au niveau du langage, c'est seulement dans les simulateurs que l'on définit une "situation" comme un ensemble de remplacements. Comme Denis, il me semble préférable de parler « d'inversion » de la formule, même si c'est une implémentation différente du mécanisme d'inversion par dichotomie que nous utilisons par ailleurs.
2. Solution définir l'inversion
Pouvoir déclarer deux variables reliées par des fonctions inverses.
Proposition de syntaxe :
chiffre d'affaires:
description: Montant total des recettes brutes
revenu net:
formule: chiffre d'affaires - cotisations
inversions:
chiffre d'affaires: revenu net + cotisations
Questions en suspend :
- Comment uniformiser ce nouveau mécanisme et le mécanisme existant d'inversion (qui revient à demander au moteur de calculer le point inverse d'une valeur donnée, plutôt que de fournir la fonction inverse)
- Comment prendre en compte cette notion d'inversion dans l’évaluation, sans se baser sur la situation comme c'est le cas aujourd'hui dans le mécanisme
inversion
?
Hum je suis pas très familier avec la littérature sur les langages à calculs inversibles ou partiellement inversibles (j'ai vu passer un papier sur ça récemment https://dl.acm.org/doi/pdf/10.1145/3409000) mais dans ce cas précis il me semble que vous gagneriez à avoir une syntaxe spéciale pour déclarer deux variables reliées par des fonctions inverses non ?
a <-> b :
- a : b * 2
- b : a * 50%
Après il faudrait gérer ça de manière spéciale dans votre moteur d'exécution, ça revient à avoir une arrête à double sens dans votre graphe de dépendance, et donc de la traiter de manière spéciale puisque pour calculer a
on a pas besoin de b
, ni vice-versa.
La notion de "situation" n'existe pas au niveau du langage, c'est seulement dans les simulateurs que l'on définit une "situation" comme un ensemble de remplacements. Comme Denis, il me semble préférable de parler « d'inversion » de la formule, même si c'est une implémentation différente du mécanisme d'inversion par dichotomie que nous utilisons par ailleurs.
À terme ces formules d'inversions pourraient certainement être inférées (cf. POC betagouv/mon-entreprise#568), mais donner les formules dans les deux sens est une bonne solution en attendant. Pour éviter des erreurs d'incohérence (une formule mise à jour mais pas son inverse par exemple), je pense aussi que les deux formules doivent être définies au même endroit dans le code (et non dans deux règles différentes).
chiffre d'affaires:
description: Montant total des recettes brutes
formule: revenu net + cotisations
inversible avec:
revenu net: chiffre d'affaires - cotisations
cotisations: chiffre d'affaires - revenu net
mais dans ce cas précis il me semble que vous gagneriez à avoir une syntaxe spéciale pour déclarer deux variables reliées par des fonctions inverses non
Effectivement, ça semble être une bonne solution.
La notion de "situation" n'existe pas au niveau du langage, c'est seulement dans les simulateurs que l'on définit une "situation" comme un ensemble de remplacements.
Elle existe aujourd'hui, que ce soit au niveau des règles (aller chercher la valeur dans la situation) ou même de certains mécanisme (notamment inversion qui vérifie qu'une règle a une valeur dans la situation pour choisir l'inversion à effectuer).
La question est de savoir si c'est une bonne chose, et effectivement, la réponse est sans doute "non". Cela semble plus solide de réutiliser le mécanisme de remplacement. Dans ce cas, il va falloir réfléchir à comment implémenter l'inversion sans utiliser la situation (ce qui revient à notre problème ici).
Sur la solution
Je suis d'accord avec le principe, à cette nuance sémantique près, je préciserai l'inversion sur la règle de revenu net
plutôt que chiffre d'affaires
. Mais cela revient strictement au même.
chiffre d'affaires:
description: Montant total des recettes brutes
revenu net:
formule: chiffre d'affaires - cotisations
inversions:
chiffre d'affaires: revenu net + cotisations
Je n'ai pas réfléchit d'avantage à cette solution, mais il me semble que cela implique de garantir que si une formule est utilisée dans l'evaluation, alors l'autre ne doit pas l'être. Je n'ai par contre aucune réponse à l'heure actuelle sur les implications sur la génération de l'AST, et sur la prise en compte statique d'une telle contrainte.
Discussion à continuer donc... (j'en profite pour mettre à jour l'issue)
Je suis retombé sur le problème en considérant les choses sous un autre angle: l'algo de détection de cycles est incomplet tant qu'il ne prend pas en compte les objectifs des simulateurs.
C'est le cas de manière claire avec le cycle rem totale
, nette après impôt
et nette
qui sont justement le "groupe d'objectifs" du simulateur indépendant.
Je pensais à 2 autres solutions, toujours du point de vue de l'aglo de cycles:
- Prendre clairement en entrée de l'algo les configs et les objectifs, mais dans ce cas les configs
doivent être du Publicodes, partie intégrante de l'interpréteur et donc du langage (un peu comme
un fichier
package.json
qui liste les dépendances et justifie donc unimport dep
). On peut voir les objectifs comme des dépendances ou variables d'entrée qui sont déclarées dans un fichier adéquat dans la base de règles. - Marquer dans les règles les données attendues en entrée, comme un groupe d'entrées ou groupe de dépendance nommé. L'API pour les clients de la base de règles serait d'extraire les groupes d'entrées et s'assurer que l'UI reflète la contrainte qu'il faut nécessairement renseigner une (et une seulement) des variables de chaque groupe d'entrée avant de lancer l'évaluation.
La 2e solution ressemblerait à:
rem totale:
…
groupe: objectif indépendant 1
nette après impôt:
…
groupe: objectif indépendant 1
nette:
…
groupe: objectif indépendant 1
Ce que j'aime bien avec cette solution c'est qu'elle permet, en plus d'être facielemnt prise en compte dans l'algo de cycles (et autres), d'avoir une API du genre:
type GroupName = string
const getGroupedRules: ParsedRules => Record<GroupName, DottedName[]> // table inverse group -> rule
const canEvaluate = ([parsedRules: ParsedRules, initialSituation: DottedName[]]) => Boolean {
const groupedRules = getGroupedRules(parsedRules)
return groupedRules.every(([groupName, rules]) => intersection(rules, initialSituation).length == 1)
}
qui peut être utilisé par la librairie cliente (mon-entreprise) pour vérifier qu'elle implémente bien toutes les contraintes: elle doit donner une situation initiale correcte avant de pouvoir lancer un evaluate.
Edit:
La solution analytique semble clairement la meilleure à terme, mais ça ne m'est pas apparu évident en tombant sur le problème qu'elle pourrait résoudre tous les cas de figure. Il est peut-être démontrable que oui? En tout cas je n'ai pas parcouru tous les cas de figures existant pour me faire une idée.
Je n'ai pas réfléchit d'avantage à cette solution, mais il me semble que cela implique de garantir que si une formule est utilisée dans l'evaluation, alors l'autre ne doit pas l'être. Je n'ai par contre aucune réponse à l'heure actuelle sur les implications sur la génération de l'AST, et sur la prise en compte statique d'une telle contrainte.
Peut-être l'approche de moncanEvaluate
peut aider ici? On peut aider le code client à décider si la situation rend les choses évaluables ou non. Il y a peut-être une approche plus élégante ceci dit.
Sur l'inversion, voir aussi #5 J'ai pas recreusé le sujet, mais je reste pour l'instant sur l'idée que préciser l'inversion dans la situation « utilise le brut qui correspond à 2000€ de net » comme dans https://github.com/betagouv/publicodes/issues/5#issuecomment-737899238 est le plus naturel. Cela permettrait notamment de simplifier le code d'invalidation de “cache intelligent” (https://github.com/betagouv/mon-entreprise/pull/1231#issuecomment-732096305).
Marquer dans les règles les données attendues en entrée, comme un groupe d'entrées ou groupe de dépendance nommé. L'API pour les clients de la base de règles serait d'extraire les groupes d'entrées et s'assurer que l'UI reflète la contrainte qu'il faut nécessairement renseigner une (et une seulement) des variables de chaque groupe d'entrée avant de lancer l'évaluation.
C'est une approche très intéressante, qui semble régler le problème décrit ici. Il s'agit en quelque sorte d'une assertion comme quoi le cycle tagué n'aura pas lieu au runtime, puisque l'on s'assure qu'il y a au moins une des valeur qui est définie avant l'évaluation.
L'autre avantage est qu'elle s'appuie sur l'utilisation de publicodes telle qu'elle est actuellement : nous fonctionnons par groupe d'objectifs. D'ailleurs, pas mal de logique est ajoutée aux reducers
de mon-entreprise pour assurer qu'il n'y a pas deux objectifs remplis en même temps, et ce serait bénéfique de la transférer dans une nouvelle fonction de l'API de publicodes (comme tu le fais remarquer).
Un dernier gros bonus : j'ai l'impression que cette écriture permet de se débarrasser de l'inversion. Un groupe d'objectif sans cycle aboutissant obligatoirement à une inversion. Et pour trouver le "pivot" qui définit l'inversion, il suffit juste de remonter les dépendances de chaque règle du groupe jusqu'à la source. Par exemple, pour le groupe salarié, brut
est la seule règle qui ne dépend pas des autres, et qui est en dépendance des autres règles, c'est donc la règle à inverser.
Un point, ceci dit : certaines règles appartiennent à plusieurs groupes (par exemple chiffre d'affaires appartient aux cycles revenu dirigeant auto-entrepreneur, salaire brut dirigeant sasu, et independant). Comment cela se concrétise ?
A continuer de réfléchir et de formaliser, mais ça me semble être une bonne piste...