From b7de15c90070dcbbf393fb5a8961967fdddc19ab Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Fri, 14 Jul 2017 17:20:38 +0200 Subject: [PATCH 01/18] =?UTF-8?q?:gear:=20Explorer=20un=20monde=20simplifi?= =?UTF-8?q?=C3=A9=20pour=20le=20moteur?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + test/tree.test.js | 93 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 test/tree.test.js diff --git a/package.json b/package.json index 03388dcd6..d4240ea87 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "core-js": "^2.4.1", "css-loader": "^0.28.1", "eslint": "^4.4.1", + "daggy": "^1.1.0", "eslint-plugin-react": "^7.0.1", "express": "^4.15.3", "file-loader": "^0.11.1", diff --git a/test/tree.test.js b/test/tree.test.js new file mode 100644 index 000000000..78f231d26 --- /dev/null +++ b/test/tree.test.js @@ -0,0 +1,93 @@ +import R from 'ramda' +import {expect} from 'chai' +import daggy from 'daggy' + +describe('simplified tree walks', function() { + + // Notre domaine peut se simplifier à une liste d'équations à trous: + // a: 45 + // b: a + c + // d: a + 4 + // e: b + d + // Disons que je veux connaitre "e", alors il va me manquer "c" + // Si je connais "c", alors je peux calculer "e" + // Et mon ambition est aussi de pouvoir visualiser le calcul en HTML + // Donc j'ai une structure plate que je transforme en arbre (ce n'est pas + // le focus de la présente exploration), je veux pouvoir demander des choses + // diverses à cet arbre: l'évaluer, repérer les trous, le transformer en HTML + + // Plus tard je vais avoir des trucs plus sophistiqués, par exemple: + // b: a + (bleu: b, vert: c) + // qui est équivalent à: + // b: b-bleu + b-vert + // b-bleu: a + b + // b-vert: a + c + // Le but du jeu est de pouvoir le représenter de façon compacte, mais + // d'avoir un arbre simple à manipuler + + let evaluate = tree => tree.evaluate() + let missing = tree => tree.missing() + + const Tree = daggy.taggedSum('Tree', + { + Number: ['number'], + Sum: ['children'], + Variable: ['name'] + }) + + Tree.prototype.evaluate = function (f) { + return this.cata({ + Number: (number) => parseInt(number), + Sum: (children) => R.reduce(R.add,0,R.map(evaluate,children)), + }) + } + + Tree.prototype.missing = function (f) { + return this.cata({ + Number: (number) => [], + Variable: (name) => [name], + Sum: (children) => R.reduce(R.concat,[],R.map(missing,children)), + }) + } + + it('should provide a protocol for evaluation', function() { + let tree = Tree.Number("45"), + result = tree.evaluate() + expect(result).to.equal(45) + }); + + it('should evaluate expressions', function() { + let tree = Tree.Sum([Tree.Number("45"),Tree.Number("25")]), + result = tree.evaluate() + expect(result).to.equal(70) + }); + + it('should evaluate nested expressions', function() { + let tree = Tree.Sum([ + Tree.Sum([Tree.Number("35"),Tree.Number("10")]), + Tree.Number("25")]), + result = tree.evaluate() + expect(result).to.equal(70) + }); + + it('should provide a protocol for missing variables', function() { + let tree = Tree.Variable("a"), + result = tree.missing() + expect(result).to.deep.equal(["a"]) + }); + + it('should locate missing variables in expressions', function() { + let tree = Tree.Sum([Tree.Number("45"),Tree.Variable("a")]), + result = tree.missing() + expect(result).to.deep.equal(["a"]) + }); + + it('should locate missing variables in nested expressions', function() { + let tree = Tree.Sum([ + Tree.Sum([Tree.Number("35"),Tree.Variable("a")]), + Tree.Number("25")]), + result = tree.missing() + expect(result).to.deep.equal(["a"]) + }); + +}); From 5a184ad5115e21708efc574f3610ceff863b2e07 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Fri, 14 Jul 2017 23:11:53 +0200 Subject: [PATCH 02/18] Forme fonctionnelle --- test/tree.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index 78f231d26..84694bd3c 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -35,14 +35,14 @@ describe('simplified tree walks', function() { Variable: ['name'] }) - Tree.prototype.evaluate = function (f) { + Tree.prototype.evaluate = function () { return this.cata({ Number: (number) => parseInt(number), Sum: (children) => R.reduce(R.add,0,R.map(evaluate,children)), }) } - Tree.prototype.missing = function (f) { + Tree.prototype.missing = function () { return this.cata({ Number: (number) => [], Variable: (name) => [name], @@ -52,13 +52,13 @@ describe('simplified tree walks', function() { it('should provide a protocol for evaluation', function() { let tree = Tree.Number("45"), - result = tree.evaluate() + result = evaluate(tree) expect(result).to.equal(45) }); it('should evaluate expressions', function() { let tree = Tree.Sum([Tree.Number("45"),Tree.Number("25")]), - result = tree.evaluate() + result = evaluate(tree) expect(result).to.equal(70) }); @@ -66,19 +66,19 @@ describe('simplified tree walks', function() { let tree = Tree.Sum([ Tree.Sum([Tree.Number("35"),Tree.Number("10")]), Tree.Number("25")]), - result = tree.evaluate() + result = evaluate(tree) expect(result).to.equal(70) }); it('should provide a protocol for missing variables', function() { let tree = Tree.Variable("a"), - result = tree.missing() + result = missing(tree) expect(result).to.deep.equal(["a"]) }); it('should locate missing variables in expressions', function() { let tree = Tree.Sum([Tree.Number("45"),Tree.Variable("a")]), - result = tree.missing() + result = missing(tree) expect(result).to.deep.equal(["a"]) }); @@ -86,7 +86,7 @@ describe('simplified tree walks', function() { let tree = Tree.Sum([ Tree.Sum([Tree.Number("35"),Tree.Variable("a")]), Tree.Number("25")]), - result = tree.missing() + result = missing(tree) expect(result).to.deep.equal(["a"]) }); From 2da3d37ae09015f1fda12c7623c9063a60f18896 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 18 Jul 2017 09:20:58 +0200 Subject: [PATCH 03/18] :gear: Utiliser les F-algebra --- package.json | 1 + test/tree.test.js | 50 +++++++++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index d4240ea87..9892b11e9 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "daggy": "^1.1.0", "eslint-plugin-react": "^7.0.1", "express": "^4.15.3", + "fantasy-frees": "^0.1.0", "file-loader": "^0.11.1", "html-loader": "^0.5.1", "img-loader": "^2.0.0", diff --git a/test/tree.test.js b/test/tree.test.js index 84694bd3c..15e5db486 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -25,51 +25,59 @@ describe('simplified tree walks', function() { // Le but du jeu est de pouvoir le représenter de façon compacte, mais // d'avoir un arbre simple à manipuler - let evaluate = tree => tree.evaluate() - let missing = tree => tree.missing() + const Fx = daggy.tagged('Fx',['x']) + const unFix = R.prop('x') - const Tree = daggy.taggedSum('Tree', - { - Number: ['number'], - Sum: ['children'], - Variable: ['name'] + const Expr = daggy.taggedSum('Expr',{ + Num: ['x'], + Add: ['x', 'y'], + Var: ['name'] }) + const {Num, Add, Var} = Expr; - Tree.prototype.evaluate = function () { + // fold :: Functor f => (f a -> a) -> Fix f -> a + const fold = R.curry((alg, x) => R.compose(alg, R.map(fold(alg)), unFix)(x)) + + // Cette fonction fournit la traversée + Expr.prototype.map = function(f) { return this.cata({ - Number: (number) => parseInt(number), - Sum: (children) => R.reduce(R.add,0,R.map(evaluate,children)), + Num: (x) => this, // fixed + Add: (x, y) => Add(f(x), f(y)) }) } - Tree.prototype.missing = function () { - return this.cata({ - Number: (number) => [], - Variable: (name) => [name], - Sum: (children) => R.reduce(R.concat,[],R.map(missing,children)), + // Celle-ci l'évaluation + const evaluator = (a) => { + return a.cata({ + Num: (x) => x, + Add: (x, y) => x + y }) } + let evaluate = expr => fold(evaluator, expr) + + let num = x => Fx(Num(x)) + let add = (x, y) => Fx(Add(x,y)) + it('should provide a protocol for evaluation', function() { - let tree = Tree.Number("45"), + let tree = num(45), result = evaluate(tree) expect(result).to.equal(45) }); it('should evaluate expressions', function() { - let tree = Tree.Sum([Tree.Number("45"),Tree.Number("25")]), + let tree = add(num(45),num(25)), result = evaluate(tree) expect(result).to.equal(70) }); it('should evaluate nested expressions', function() { - let tree = Tree.Sum([ - Tree.Sum([Tree.Number("35"),Tree.Number("10")]), - Tree.Number("25")]), + let tree = add(num(45),add(num(15),num(10))), result = evaluate(tree) expect(result).to.equal(70) }); +/* it('should provide a protocol for missing variables', function() { let tree = Tree.Variable("a"), result = missing(tree) @@ -89,5 +97,5 @@ describe('simplified tree walks', function() { result = missing(tree) expect(result).to.deep.equal(["a"]) }); - +*/ }); From 0aa90327d387af705d39e2bf4fde081966a6d666 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 18 Jul 2017 09:26:56 +0200 Subject: [PATCH 04/18] =?UTF-8?q?:gear:=20Interpr=C3=A8te=20avec=20des=20v?= =?UTF-8?q?ariables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index 15e5db486..552ba6a10 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -42,22 +42,25 @@ describe('simplified tree walks', function() { Expr.prototype.map = function(f) { return this.cata({ Num: (x) => this, // fixed - Add: (x, y) => Add(f(x), f(y)) + Add: (x, y) => Add(f(x), f(y)), + Var: (name) => this }) } // Celle-ci l'évaluation - const evaluator = (a) => { + const evaluator = state => a => { return a.cata({ Num: (x) => x, - Add: (x, y) => x + y + Add: (x, y) => x + y, + Var: (name) => state[name] }) } - let evaluate = expr => fold(evaluator, expr) + let evaluate = (expr, state={}) => fold(evaluator(state), expr) let num = x => Fx(Num(x)) let add = (x, y) => Fx(Add(x,y)) + let ref = (name) => Fx(Var(name)) it('should provide a protocol for evaluation', function() { let tree = num(45), @@ -77,6 +80,12 @@ describe('simplified tree walks', function() { expect(result).to.equal(70) }); + it('should evaluate expressions involving variables', function() { + let tree = add(num(45),ref("a")), + result = evaluate(tree,{a:25}) + expect(result).to.equal(70) + }); + /* it('should provide a protocol for missing variables', function() { let tree = Tree.Variable("a"), From 8b279615fa352363f9f818f9c65861a59ffcb4d9 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 18 Jul 2017 11:38:55 +0200 Subject: [PATCH 05/18] =?UTF-8?q?:gear:=20Utilise=20Maybe=20pour=20l'optio?= =?UTF-8?q?nnalit=C3=A9=20des=20valeurs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- test/tree.test.js | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 9892b11e9..21bfdb2ce 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ "daggy": "^1.1.0", "eslint-plugin-react": "^7.0.1", "express": "^4.15.3", - "fantasy-frees": "^0.1.0", "file-loader": "^0.11.1", "html-loader": "^0.5.1", "img-loader": "^2.0.0", @@ -67,6 +66,7 @@ "mocha-webpack": "^0.7.0", "nearley-loader": "0.0.2", "postcss-loader": "^2.0.5", + "ramda-fantasy": "^0.8.0", "react-hot-loader": "^3.0.0-beta.6", "redux-devtools": "^3.4.0", "redux-devtools-dock-monitor": "^1.1.2", diff --git a/test/tree.test.js b/test/tree.test.js index 552ba6a10..074196465 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -1,6 +1,7 @@ import R from 'ramda' import {expect} from 'chai' import daggy from 'daggy' +import {Maybe as M} from 'ramda-fantasy' describe('simplified tree walks', function() { @@ -51,14 +52,16 @@ describe('simplified tree walks', function() { const evaluator = state => a => { return a.cata({ Num: (x) => x, - Add: (x, y) => x + y, - Var: (name) => state[name] + Add: (x, y) => R.lift(R.add)(x,y), + Var: (name) => M.toMaybe(state[name]) // Doesn't typecheck }) } - let evaluate = (expr, state={}) => fold(evaluator(state), expr) + let evaluate = (expr, state={}) => + fold(evaluator(state), expr) + .getOrElse(null) // for convenience - let num = x => Fx(Num(x)) + let num = x => Fx(Num(M.Just(x))) let add = (x, y) => Fx(Add(x,y)) let ref = (name) => Fx(Var(name)) @@ -86,6 +89,12 @@ describe('simplified tree walks', function() { expect(result).to.equal(70) }); + it('should evaluate expressions involving missing variables', function() { + let tree = add(num(45),ref("b")), + result = evaluate(tree,{a:25}) + expect(result).to.equal(null) + }); + /* it('should provide a protocol for missing variables', function() { let tree = Tree.Variable("a"), From 5a6aa2a0911751d9e7049ba026859f64c2767a53 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 18 Jul 2017 11:44:05 +0200 Subject: [PATCH 06/18] :gear: Fonction de collecte des variables manquantes --- test/tree.test.js | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index 074196465..02546a9b0 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -57,10 +57,22 @@ describe('simplified tree walks', function() { }) } + // Celle-ci la collecte des variables manquantes + const collector = state => a => { + return a.cata({ + Num: (x) => [], + Add: (x, y) => R.concat(x,y), + Var: (name) => state[name] ? [] : [name] + }) + } + let evaluate = (expr, state={}) => fold(evaluator(state), expr) .getOrElse(null) // for convenience + let missing = (expr, state={}) => + fold(collector(state), expr) + let num = x => Fx(Num(M.Just(x))) let add = (x, y) => Fx(Add(x,y)) let ref = (name) => Fx(Var(name)) @@ -95,25 +107,28 @@ describe('simplified tree walks', function() { expect(result).to.equal(null) }); -/* it('should provide a protocol for missing variables', function() { - let tree = Tree.Variable("a"), + let tree = ref("a"), result = missing(tree) expect(result).to.deep.equal(["a"]) }); it('should locate missing variables in expressions', function() { - let tree = Tree.Sum([Tree.Number("45"),Tree.Variable("a")]), + let tree = add(num(45),ref("a")), result = missing(tree) expect(result).to.deep.equal(["a"]) }); it('should locate missing variables in nested expressions', function() { - let tree = Tree.Sum([ - Tree.Sum([Tree.Number("35"),Tree.Variable("a")]), - Tree.Number("25")]), + let tree = add(add(num(35),ref("a")),num(25)), result = missing(tree) expect(result).to.deep.equal(["a"]) }); -*/ + + it('should locate missing variables in nested expressions', function() { + let tree = add(add(num(35),ref("a")),num(25)), + result = missing(tree,{a:25}) + expect(result).to.deep.equal([]) + }); + }); From 63dcb5030b8e9b33c03e708a9377c42bd7fdd50b Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 1 Aug 2017 10:42:11 +0200 Subject: [PATCH 07/18] :gear: Ajoute quelques notes --- test/tree.test.js | 10 ++++++++++ test/trees.md | 0 2 files changed, 10 insertions(+) create mode 100644 test/trees.md diff --git a/test/tree.test.js b/test/tree.test.js index 02546a9b0..f2cae84e0 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -26,6 +26,13 @@ describe('simplified tree walks', function() { // Le but du jeu est de pouvoir le représenter de façon compacte, mais // d'avoir un arbre simple à manipuler + // Pour intégrer dans le simulateur, il faut remplir les exigences + // suivantes: + // - décorer l'arbre avec une valeur à chaque noeud + // - réaliser le calcul de façon efficiente (1 fois par variable) + // - savoir "court-circuiter" le calcul de variables manquantes dans les conditionnelles + // - avoir un moyen de gérer les composantes et filtrage + const Fx = daggy.tagged('Fx',['x']) const unFix = R.prop('x') @@ -33,6 +40,9 @@ describe('simplified tree walks', function() { Num: ['x'], Add: ['x', 'y'], Var: ['name'] +// NotIf: ['condition','formule'], +// AnyOf: ['conditions'], +// AllOf: ['conditions'], }) const {Num, Add, Var} = Expr; diff --git a/test/trees.md b/test/trees.md new file mode 100644 index 000000000..e69de29bb From 8d92b7bf3ec4b8e9b174d2d0b38071d1ede9434c Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Tue, 1 Aug 2017 15:35:43 +0200 Subject: [PATCH 08/18] :gear: Ajoute quelques notes --- test/tree.test.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/tree.test.js b/test/tree.test.js index f2cae84e0..10a6b4917 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -36,6 +36,26 @@ describe('simplified tree walks', function() { const Fx = daggy.tagged('Fx',['x']) const unFix = R.prop('x') + // Chaque élément de notre liste est une définition: + + const Def = daggy.taggedSum('Def', { + Formula: ['expr'], + Conditional: ['cond','expr'], // Applicable si + Blocked: ['cond','expr'], // Non applicable si + }) + const {Formula, Conditional, Blocked} = Def + + // Ce qu'on décrit est un framework de programmation déclarative: on stipule des + // définitions (salaire net = brut - cotisations) mais on les donne sans ordre + // impératif, on laisse au moteur le soin de calculer les dépendances + + // Par contre, à l'exécution, il faut bien calculer des "effets de bord" + // pour rester performant: chaque évaluation d'une définition doit mettre + // à jour le 'dictionnaire' des valeurs connues, puis le mettre à disposition + // de la suite du calcul + + // La partie droite d'une définition est une expression: + const Expr = daggy.taggedSum('Expr',{ Num: ['x'], Add: ['x', 'y'], @@ -44,7 +64,7 @@ describe('simplified tree walks', function() { // AnyOf: ['conditions'], // AllOf: ['conditions'], }) - const {Num, Add, Var} = Expr; + const {Num, Add, Var} = Expr // fold :: Functor f => (f a -> a) -> Fix f -> a const fold = R.curry((alg, x) => R.compose(alg, R.map(fold(alg)), unFix)(x)) From 1d70be0b5dfbcb3bb44c15730671874faccd17e8 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Thu, 3 Aug 2017 14:03:04 +0200 Subject: [PATCH 09/18] =?UTF-8?q?:gear:=20Evaluer=20ET=20annoter,=20mais?= =?UTF-8?q?=20r=C3=A9cursion=20explicite?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/tree.test.js b/test/tree.test.js index 10a6b4917..76070e2d0 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -107,6 +107,45 @@ describe('simplified tree walks', function() { let add = (x, y) => Fx(Add(x,y)) let ref = (name) => Fx(Var(name)) + const ExprAnn = daggy.taggedSum('ExprAnn',{ + NumAnn: ['v', 'x'], + AddAnn: ['v', 'x', 'y'], + }) + const {NumAnn, AddAnn} = ExprAnn + + const annotate = a => { + return a.cata({ + NumAnn: (v,x) => NumAnn(x,x), + AddAnn: (v,x,y) => { + let ax = annotate(x), + ay = annotate(y), + vv = ax.val()+ay.val() + return AddAnn(vv,ax,ay) + } + }) + } + + ExprAnn.prototype.val = function() { + return this.cata({ + NumAnn: (v,x) => v, + AddAnn: (v,x,y) => v + }) + } + + it('should annotate nodes', function() { + let tree = NumAnn(null,45), + result = annotate(tree) + expect(result.val()).to.equal(45) + }); + + it('should annotate trees', function() { + let tree = AddAnn(null,NumAnn(null,25),NumAnn(null,45)), + result = annotate(tree) + expect(result.x.val()).to.equal(25) + expect(result.y.val()).to.equal(45) + expect(result.val()).to.equal(70) + }); + it('should provide a protocol for evaluation', function() { let tree = num(45), result = evaluate(tree) From 8500b61660741e6f74e20eaa0d9fbfacf37ba8d5 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sun, 6 Aug 2017 18:02:21 +0200 Subject: [PATCH 10/18] =?UTF-8?q?:gear:=20Evaluer=20ET=20annoter=20ET=20s?= =?UTF-8?q?=C3=A9parer=20la=20r=C3=A9cursion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 64 ++++++++++++++++++----------------------------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index 76070e2d0..c9525cd76 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -100,6 +100,25 @@ describe('simplified tree walks', function() { fold(evaluator(state), expr) .getOrElse(null) // for convenience + const AnnF = daggy.tagged('AnnF',['fr','a']) + + let ann = ({fst, snd}) => Fx(AnnF(fst,snd)) + + let nodeValue = annf => { + let {fr, a} = unFix(annf) + return a + } + + // fork is Haskell's "&&&" operator: (f &&& g) x = Pair(f(x),g(x)) + let fork = (f, g) => x => ({fst:f(x), snd:g(x)}) + + let synthesize = f => { + let algebra = f => R.compose(ann, fork(R.identity, R.compose(f, R.map(nodeValue)))) + return fold(algebra(f)) + } + + let annotate = (state, tree) => synthesize(evaluator(state))(tree) + let missing = (expr, state={}) => fold(collector(state), expr) @@ -107,45 +126,6 @@ describe('simplified tree walks', function() { let add = (x, y) => Fx(Add(x,y)) let ref = (name) => Fx(Var(name)) - const ExprAnn = daggy.taggedSum('ExprAnn',{ - NumAnn: ['v', 'x'], - AddAnn: ['v', 'x', 'y'], - }) - const {NumAnn, AddAnn} = ExprAnn - - const annotate = a => { - return a.cata({ - NumAnn: (v,x) => NumAnn(x,x), - AddAnn: (v,x,y) => { - let ax = annotate(x), - ay = annotate(y), - vv = ax.val()+ay.val() - return AddAnn(vv,ax,ay) - } - }) - } - - ExprAnn.prototype.val = function() { - return this.cata({ - NumAnn: (v,x) => v, - AddAnn: (v,x,y) => v - }) - } - - it('should annotate nodes', function() { - let tree = NumAnn(null,45), - result = annotate(tree) - expect(result.val()).to.equal(45) - }); - - it('should annotate trees', function() { - let tree = AddAnn(null,NumAnn(null,25),NumAnn(null,45)), - result = annotate(tree) - expect(result.x.val()).to.equal(25) - expect(result.y.val()).to.equal(45) - expect(result.val()).to.equal(70) - }); - it('should provide a protocol for evaluation', function() { let tree = num(45), result = evaluate(tree) @@ -164,6 +144,12 @@ describe('simplified tree walks', function() { expect(result).to.equal(70) }); + it('should annotate tree with evaluation results', function() { + let tree = add(num(45),add(num(15),num(10))), + result = nodeValue(annotate({},tree)).getOrElse(null) + expect(result).to.equal(70) + }); + it('should evaluate expressions involving variables', function() { let tree = add(num(45),ref("a")), result = evaluate(tree,{a:25}) From 0089674d93b7b40e6905955933bee6f73286cc98 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sun, 6 Aug 2017 18:08:58 +0200 Subject: [PATCH 11/18] =?UTF-8?q?:gear:=20Pour=20l'instant=20seul=20Assign?= =?UTF-8?q?=20a=20un=20statut=20=C3=A0=20part?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index c9525cd76..a8b5df583 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -39,11 +39,9 @@ describe('simplified tree walks', function() { // Chaque élément de notre liste est une définition: const Def = daggy.taggedSum('Def', { - Formula: ['expr'], - Conditional: ['cond','expr'], // Applicable si - Blocked: ['cond','expr'], // Non applicable si + Assign: ['name', 'expr'] }) - const {Formula, Conditional, Blocked} = Def + const {Assign} = Def // Ce qu'on décrit est un framework de programmation déclarative: on stipule des // définitions (salaire net = brut - cotisations) mais on les donne sans ordre @@ -60,9 +58,10 @@ describe('simplified tree walks', function() { Num: ['x'], Add: ['x', 'y'], Var: ['name'] -// NotIf: ['condition','formule'], -// AnyOf: ['conditions'], -// AllOf: ['conditions'], +// NotIf: ['condition','formule'], +// OnlyIf: ['condition','formule'], +// AnyOf: ['conditions'], +// AllOf: ['conditions'], }) const {Num, Add, Var} = Expr From 7d75d67d44025f8b5400a313c94dc098fb4b5f5a Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sun, 6 Aug 2017 22:36:04 +0200 Subject: [PATCH 12/18] :gear: Etoffe la documentation --- test/tree.test.js | 162 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 113 insertions(+), 49 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index a8b5df583..225d52a95 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -28,29 +28,26 @@ describe('simplified tree walks', function() { // Pour intégrer dans le simulateur, il faut remplir les exigences // suivantes: - // - décorer l'arbre avec une valeur à chaque noeud + // X décorer l'arbre avec une valeur à chaque noeud // - réaliser le calcul de façon efficiente (1 fois par variable) // - savoir "court-circuiter" le calcul de variables manquantes dans les conditionnelles // - avoir un moyen de gérer les composantes et filtrage - const Fx = daggy.tagged('Fx',['x']) - const unFix = R.prop('x') + // Ce qu'on décrit est un framework de programmation déclarative: on stipule des + // définitions (salaire net = brut - cotisations) mais on les donne sans ordre + // impératif, on laisse au moteur le soin de calculer les dépendances - // Chaque élément de notre liste est une définition: + // Chaque élément de notre base de règles est une définition: const Def = daggy.taggedSum('Def', { Assign: ['name', 'expr'] }) const {Assign} = Def - // Ce qu'on décrit est un framework de programmation déclarative: on stipule des - // définitions (salaire net = brut - cotisations) mais on les donne sans ordre - // impératif, on laisse au moteur le soin de calculer les dépendances - // Par contre, à l'exécution, il faut bien calculer des "effets de bord" // pour rester performant: chaque évaluation d'une définition doit mettre // à jour le 'dictionnaire' des valeurs connues, puis le mettre à disposition - // de la suite du calcul + // de la suite du calcul - on verra comment au Chapitre 3 // La partie droite d'une définition est une expression: @@ -65,8 +62,63 @@ describe('simplified tree walks', function() { }) const {Num, Add, Var} = Expr + // Chapitre 1... + + // Le type Expr est la traduction en JS du type suivant en Haskell, + // "naivement récursif": + // data Expr = Num Int | Var String | Add Expr Expr + + // Il se trouve qu'on peut gagner beaucoup en introduisant une petite + // complexité: on va exprimer la récursion avec un niveau d'indirection, + // la première étape étant de rendre le type polymorphique sur ce qui + // est récursif: + + // data ExprF r = Num Int | Var String | Add r r + + // Par exemple, une addition de deux additions c'est de type ExprF (ExprF r), + // et si je veux décrire des imbrications plus poussées d'additions dans + // des additions il me faudra un ExprF (ExprF (ExprF r)) et ainsi de + // suite: on a "déroulé" la récursion dans le type d'origine. + + // On peut alors retrouver le type d'origine en introduisant un + // "constructeur de point fixe de type", appelé Fx, et en introduisant + // ce qu'on appelle un "functor type" (c'est le suffixe F) + + // data Expr = Fx ExprF + + // Le point fixe de f est une solution à l'équation x = f x - on + // peut l'appliquer à des fonctions récursives, voir par exemple: + // https://www.vex.net/~trebla/haskell/fix.xhtml + + // En JS ça ne marche pas parce que JS est strict et non lazy... + + // Quand au point fixe d'un type, c'est le point fixe de son + // constructeur: une solution à l'équation T = Fx T + + // En JS c'est juste une fonction qui emballe et une qui déballe: + + const Fx = daggy.tagged('Fx',['x']) + const unFix = R.prop('x') + + // Les helpers suivants rendent moins pénible la construction de valeurs + // notamment pour les tests + + let num = x => Fx(Num(M.Just(x))) + let add = (x, y) => Fx(Add(x,y)) + let ref = (name) => Fx(Var(name)) + + // Une application de la théorie des catégories permet de dériver + // la fonction "fold" suivante, qui généralise aux structures récursives + // la notion de "reduction" (comme pour les listes), on l'appelle aussi + // un catamorphisme + // fold :: Functor f => (f a -> a) -> Fix f -> a - const fold = R.curry((alg, x) => R.compose(alg, R.map(fold(alg)), unFix)(x)) + const fold = R.curry((algebra, x) => R.compose(algebra, R.map(fold(algebra)), unFix)(x)) + + // Cf. https://www.schoolofhaskell.com/user/bartosz/understanding-algebras + + // Dans ce contexte, un "algebre" est une fonction qui nous dit comment calculer + // la réduction pour un noeud à partir des valeurs calculées pour les noeuds fils // Cette fonction fournit la traversée Expr.prototype.map = function(f) { @@ -86,44 +138,11 @@ describe('simplified tree walks', function() { }) } - // Celle-ci la collecte des variables manquantes - const collector = state => a => { - return a.cata({ - Num: (x) => [], - Add: (x, y) => R.concat(x,y), - Var: (name) => state[name] ? [] : [name] - }) - } - let evaluate = (expr, state={}) => fold(evaluator(state), expr) .getOrElse(null) // for convenience - const AnnF = daggy.tagged('AnnF',['fr','a']) - - let ann = ({fst, snd}) => Fx(AnnF(fst,snd)) - - let nodeValue = annf => { - let {fr, a} = unFix(annf) - return a - } - - // fork is Haskell's "&&&" operator: (f &&& g) x = Pair(f(x),g(x)) - let fork = (f, g) => x => ({fst:f(x), snd:g(x)}) - - let synthesize = f => { - let algebra = f => R.compose(ann, fork(R.identity, R.compose(f, R.map(nodeValue)))) - return fold(algebra(f)) - } - - let annotate = (state, tree) => synthesize(evaluator(state))(tree) - - let missing = (expr, state={}) => - fold(collector(state), expr) - - let num = x => Fx(Num(M.Just(x))) - let add = (x, y) => Fx(Add(x,y)) - let ref = (name) => Fx(Var(name)) + // Voici donc l'évaluation d'un arbre... it('should provide a protocol for evaluation', function() { let tree = num(45), @@ -143,11 +162,22 @@ describe('simplified tree walks', function() { expect(result).to.equal(70) }); - it('should annotate tree with evaluation results', function() { - let tree = add(num(45),add(num(15),num(10))), - result = nodeValue(annotate({},tree)).getOrElse(null) - expect(result).to.equal(70) - }); + // Problème: on évalue l'arbre tout entier d'un seul coup; mais + // peut-on aussi "décorer" l'arbre pendant sa traversée avec les + // valeurs intermédiaires ? On verra que oui, au Chapitre 2; en + // attendant on voudrait aussi savoir quelles sont les variables + // manquantes... + + const collector = state => a => { + return a.cata({ + Num: (x) => [], + Add: (x, y) => R.concat(x,y), + Var: (name) => state[name] ? [] : [name] + }) + } + + let missing = (expr, state={}) => + fold(collector(state), expr) it('should evaluate expressions involving variables', function() { let tree = add(num(45),ref("a")), @@ -185,4 +215,38 @@ describe('simplified tree walks', function() { expect(result).to.deep.equal([]) }); + // Chapitre 2... + + // Pour annoter l'arbre avec les valeurs intermédiaires on utilise un + // type "Cofree Comonad": ce sont des paires (fst,snd) dont la première + // valeur est un noeud de l'arbre et la seconde l'annotation; on a un + // constructeur ann et une fonction de lecture + + // Cf https://github.com/willtim/recursion-schemes/ + + const AnnF = daggy.tagged('AnnF',['fr','a']) + let ann = ({fst, snd}) => Fx(AnnF(fst,snd)) + let nodeValue = annf => { + let {fr, a} = unFix(annf) + return a + } + + // fork est l'opérateur "&&&" de Haskell: (f &&& g) x = Pair(f(x),g(x)) + let fork = (f, g) => x => ({fst:f(x), snd:g(x)}) + + // synthesize combine l'application d'un algèbre fourni f et de l'annotation + let synthesize = f => { + let algebra = f => R.compose(ann, fork(R.identity, R.compose(f, R.map(nodeValue)))) + return fold(algebra(f)) + } + + let annotate = (state, tree) => synthesize(evaluator(state))(tree) + + it('should annotate tree with evaluation results', function() { + let tree = add(num(45),add(num(15),num(10))), + result = nodeValue(annotate({},tree)).getOrElse(null) + expect(result).to.equal(70) + }); + + // Chapitre 3 }); From 1ce11035f0d7c3ae50f7fea4b3938f8db1733e9b Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Thu, 10 Aug 2017 22:59:15 +0200 Subject: [PATCH 13/18] =?UTF-8?q?:gear:=20Teste=20et=20documente=20l'?= =?UTF-8?q?=C3=A9valuation=20de=20formules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 ++ test/tree.test.js | 113 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 21bfdb2ce..c0cd9be08 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,9 @@ "daggy": "^1.1.0", "eslint-plugin-react": "^7.0.1", "express": "^4.15.3", + "fantasy-combinators": "0.0.1", + "fantasy-land": "^3.3.0", + "fantasy-tuples": "^1.0.0", "file-loader": "^0.11.1", "html-loader": "^0.5.1", "img-loader": "^2.0.0", diff --git a/test/tree.test.js b/test/tree.test.js index 225d52a95..c96c14d70 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -103,7 +103,7 @@ describe('simplified tree walks', function() { // Les helpers suivants rendent moins pénible la construction de valeurs // notamment pour les tests - let num = x => Fx(Num(M.Just(x))) + let num = x => Fx(Num(x)) let add = (x, y) => Fx(Add(x,y)) let ref = (name) => Fx(Var(name)) @@ -132,7 +132,7 @@ describe('simplified tree walks', function() { // Celle-ci l'évaluation const evaluator = state => a => { return a.cata({ - Num: (x) => x, + Num: (x) => M.Just(x), Add: (x, y) => R.lift(R.add)(x,y), Var: (name) => M.toMaybe(state[name]) // Doesn't typecheck }) @@ -249,4 +249,113 @@ describe('simplified tree walks', function() { }); // Chapitre 3 + + // On sait evaluer des expressions, il faut aussi être capable de + // gérer les règles définissant les variables appelées dans ces + // expressions; voyons ce que ça donne avec un algèbre plus simple: + + let calculate = R.curry((rules, name) => { + let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, + expr = find(rules, name) + return fold(evaluator2(calculate(rules)), expr) + }) + + const evaluator2 = calculate => a => { + return a.cata({ + Num: (x) => x, + Add: (x, y) => x+y, + Var: (name) => calculate(name) + }) + } + + it('should resolve variable dependencies', function() { + let rule1 = Assign("a",add(ref("b"),ref("b"))), + rule2 = Assign("b",num(15)), + rules = [rule1,rule2], + result = calculate(rules,"a") + expect(result).to.equal(30) + }); + + // Utilisons un Writer (un idiome fonctionnel pour par exemple écrire des logs) + // pour examiner le calcul de plus près: + + const { of, chain, map, ap } = require('fantasy-land'); + const { identity } = require('fantasy-combinators'); + const { Tuple2 } = require('fantasy-tuples'); + + const Writer = M => { + + const Writer = daggy.tagged(Writer,['run']); + + Writer.of = function(x) { + return Writer(() => Tuple2(x, M.empty())); + }; + + Writer.prototype.chain = function(f) { + return Writer(() => { + const result = this.run(); + const t = f(result._1).run(); + return Tuple2(t._1, result._2.concat(t._2)); + }); + }; + + Writer.prototype.tell = function(y) { + return Writer(() => { + const result = this.run(); + return Tuple2(null, result._2.concat(y)); + }); + }; + + Writer.prototype.map = function(f) { + return Writer(() => { + const result = this.run(); + return Tuple2(f(result._1), result._2); + }); + }; + + Writer.prototype.ap = function(b) { + return this.chain((a) => b.map(a)); + }; + + return Writer; + + }; + + const Str = daggy.tagged('Str',['s']) + Str.prototype.empty = Str.empty = function() {return Str("")} + Str.prototype.concat = function(b) {return Str(this.s+b.s)} + Str.prototype.length = function() {return this.s.length} + + const StrWriter = Writer(Str) + StrWriter.prototype.toString = function() {return this.run()._2.s} + + let trace = R.curry((rules, name) => { + let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, + expr = find(rules, name) + return fold(tracer(trace(rules)), expr) + }) + + const tracer = recurse => a => { + return a.cata({ + Num: (x) => StrWriter(() => Tuple2(x,x+",")), + Add: (x, y) => x.chain(xx => y.chain(yy => StrWriter(() => Tuple2(xx+yy,"+,")))), + Var: (name) => recurse(name).chain(x => StrWriter(() => Tuple2(x,name+","))) + }) + } + + // On voit qu'on a calculé la valeur de b 2 fois! Ce n'est pas utile, + // puisque cette valeur ne changera pas au cours du calcul; et comme on + // répète le calcul autant de fois qu'il y a de références à une variable + // donnée, si l'arbre est un tant soit peu complexe les performances seront + // très mauvaises. + + it('should trace the shape of the computation', function() { + let rule1 = Assign("a",add(ref("b"),ref("b"))), + rule2 = Assign("b",num(15)), + rules = [rule1,rule2], + result = trace(rules,"a").run() + expect(result._2).to.equal("15,b,15,b,+,") + expect(result._1).to.equal(30) + }); + }); From d35148378f93f22bab54be6f3c6ddac16bab8238 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Fri, 11 Aug 2017 15:41:27 +0200 Subject: [PATCH 14/18] :gear: Met en place les catamorphismes monadiques --- test/tree.test.js | 66 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index c96c14d70..a3f3cf06e 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -98,7 +98,8 @@ describe('simplified tree walks', function() { // En JS c'est juste une fonction qui emballe et une qui déballe: const Fx = daggy.tagged('Fx',['x']) - const unFix = R.prop('x') + Fx.prototype.project = function() { return this.x } + const unFix = fx => fx.project() // Les helpers suivants rendent moins pénible la construction de valeurs // notamment pour les tests @@ -327,7 +328,7 @@ describe('simplified tree walks', function() { Str.prototype.length = function() {return this.s.length} const StrWriter = Writer(Str) - StrWriter.prototype.toString = function() {return this.run()._2.s} + const log = (x, s) => StrWriter(() => Tuple2(x,Str(s))) let trace = R.curry((rules, name) => { let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, @@ -337,9 +338,9 @@ describe('simplified tree walks', function() { const tracer = recurse => a => { return a.cata({ - Num: (x) => StrWriter(() => Tuple2(x,x+",")), - Add: (x, y) => x.chain(xx => y.chain(yy => StrWriter(() => Tuple2(xx+yy,"+,")))), - Var: (name) => recurse(name).chain(x => StrWriter(() => Tuple2(x,name+","))) + Num: (x) => log(x, x+","), + Add: (x, y) => x.chain(xx => y.chain(yy => log(xx+yy,"+,"))), + Var: (name) => recurse(name).chain(x => log(x,name+",")) }) } @@ -354,8 +355,61 @@ describe('simplified tree walks', function() { rule2 = Assign("b",num(15)), rules = [rule1,rule2], result = trace(rules,"a").run() - expect(result._2).to.equal("15,b,15,b,+,") + expect(result._2.s).to.equal("15,b,15,b,+,") expect(result._1).to.equal(30) }); + // Pour corriger ce problème on va avoir besoin de formuler une version + // "monadique" du catamorphisme, c'est-à-dire qu'on va pouvoir l'associer + // à un contexte (ou monade) dans lequel tout le calcul va se dérouler, + // et qui va pouvoir accumuler des informations au fur et à mesure, par + // exemple un cache des variables déjà calculées. + + // On a déjà vu un exemple de monade, c'était Writer: voyons comment on + // reformule le catamorphisme pour qu'il se déroule dans la monade Writer. + // D'abord on ajoute de la plomberie: + + const cataM = (of, algM) => m => + m.project() + .traverse(of, x => x.cataM(of, algM)) + .chain(algM) + + const traverse = function(of, f) { + return this.cata({ + Num: (x) => of(this), + Add: (x, y) => f(x).chain(xx => f(y).chain(yy => of(Add(xx,yy)))), + Var: (name) => of(this) + }) + } + Expr.prototype.traverse = traverse + Fx.prototype.cataM = function(of, alg) { return cataM(of, alg)(this) } + + // Maintenant que c'est fait on voit qu'on a simplifié l'expression du + // catamorphisme: on n'a plus à expliciter l'enchaînement (sauf pour la + // récursion de plus haut niveau dans les variables) + + let trace2 = R.curry((rules, name) => { + let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, + expr = find(rules, name) + return cataM(StrWriter.of, tracer2(trace2(rules)))(expr) + }) + + const tracer2 = recurse => a => { + return a.cata({ + Num: (x) => log(x,x+","), + Add: (x, y) => log(x+y,"+,"), + Var: (name) => recurse(name).chain(x => log(x,name+",")) + }) + } + + it('should trace the shape of the computation, too', function() { + let rule1 = Assign("a",add(ref("b"),ref("c"))), + rule2 = Assign("b",num(15)), + rule3 = Assign("c",num(10)), + rules = [rule1,rule2,rule3], + result = trace2(rules,"a").run() + expect(result._1).to.equal(25) + expect(result._2.s).to.equal("15,b,10,c,+,") + }); + }); From 06d260d5f8e4fc597f0a5a8be90a5eb0b358e386 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sat, 12 Aug 2017 09:24:18 +0200 Subject: [PATCH 15/18] =?UTF-8?q?:gear:=20Ajoute=20quelques=20r=C3=A9f?= =?UTF-8?q?=C3=A9rences?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/tree.test.js b/test/tree.test.js index a3f3cf06e..261a0643c 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -224,6 +224,7 @@ describe('simplified tree walks', function() { // constructeur ann et une fonction de lecture // Cf https://github.com/willtim/recursion-schemes/ + // or http://www.timphilipwilliams.com/slides/HaskellAtBarclays.pdf const AnnF = daggy.tagged('AnnF',['fr','a']) let ann = ({fst, snd}) => Fx(AnnF(fst,snd)) @@ -278,7 +279,9 @@ describe('simplified tree walks', function() { }); // Utilisons un Writer (un idiome fonctionnel pour par exemple écrire des logs) - // pour examiner le calcul de plus près: + // pour examiner le calcul de plus près. L'implémentation est celle de + // https://github.com/fantasyland/fantasy-writers/ + // mais qui n'est plus à jour avec les versions récentes de daggy const { of, chain, map, ap } = require('fantasy-land'); const { identity } = require('fantasy-combinators'); @@ -367,6 +370,8 @@ describe('simplified tree walks', function() { // On a déjà vu un exemple de monade, c'était Writer: voyons comment on // reformule le catamorphisme pour qu'il se déroule dans la monade Writer. + // L'implémentation de cataM est inspirée de + // https://github.com/DrBoolean/excursion/ // D'abord on ajoute de la plomberie: const cataM = (of, algM) => m => From 21dd767feef5e530f8fbd8c553d36fe12df18ce3 Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Sun, 13 Aug 2017 16:22:11 +0200 Subject: [PATCH 16/18] :gear: Bascule sur les monades de Akh --- package.json | 1 + test/tree.test.js | 72 +++++++++-------------------------------------- 2 files changed, 15 insertions(+), 58 deletions(-) diff --git a/package.json b/package.json index c0cd9be08..30988b416 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "yaml-loader": "^0.5.0" }, "devDependencies": { + "akh": "^3.1.2", "autoprefixer": "^7.1.1", "babel-cli": "^6.24.1", "babel-core": "^6.24.1", diff --git a/test/tree.test.js b/test/tree.test.js index 261a0643c..65b2f120e 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -2,6 +2,7 @@ import R from 'ramda' import {expect} from 'chai' import daggy from 'daggy' import {Maybe as M} from 'ramda-fantasy' +import {Writer} from 'akh' describe('simplified tree walks', function() { @@ -279,59 +280,14 @@ describe('simplified tree walks', function() { }); // Utilisons un Writer (un idiome fonctionnel pour par exemple écrire des logs) - // pour examiner le calcul de plus près. L'implémentation est celle de - // https://github.com/fantasyland/fantasy-writers/ - // mais qui n'est plus à jour avec les versions récentes de daggy + // pour examiner le calcul de plus près. - const { of, chain, map, ap } = require('fantasy-land'); - const { identity } = require('fantasy-combinators'); - const { Tuple2 } = require('fantasy-tuples'); + const Str = daggy.tagged("Str",['s']) + Str.zero = Str("") + Str.prototype.zero = Str.zero + Str.prototype.concat = function(b) { return Str(this.s+b.s)} - const Writer = M => { - - const Writer = daggy.tagged(Writer,['run']); - - Writer.of = function(x) { - return Writer(() => Tuple2(x, M.empty())); - }; - - Writer.prototype.chain = function(f) { - return Writer(() => { - const result = this.run(); - const t = f(result._1).run(); - return Tuple2(t._1, result._2.concat(t._2)); - }); - }; - - Writer.prototype.tell = function(y) { - return Writer(() => { - const result = this.run(); - return Tuple2(null, result._2.concat(y)); - }); - }; - - Writer.prototype.map = function(f) { - return Writer(() => { - const result = this.run(); - return Tuple2(f(result._1), result._2); - }); - }; - - Writer.prototype.ap = function(b) { - return this.chain((a) => b.map(a)); - }; - - return Writer; - - }; - - const Str = daggy.tagged('Str',['s']) - Str.prototype.empty = Str.empty = function() {return Str("")} - Str.prototype.concat = function(b) {return Str(this.s+b.s)} - Str.prototype.length = function() {return this.s.length} - - const StrWriter = Writer(Str) - const log = (x, s) => StrWriter(() => Tuple2(x,Str(s))) + const log = (x, s) => Writer.tell(Str(s)).map(_ => x) let trace = R.curry((rules, name) => { let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, @@ -357,9 +313,9 @@ describe('simplified tree walks', function() { let rule1 = Assign("a",add(ref("b"),ref("b"))), rule2 = Assign("b",num(15)), rules = [rule1,rule2], - result = trace(rules,"a").run() - expect(result._2.s).to.equal("15,b,15,b,+,") - expect(result._1).to.equal(30) + result = trace(rules,"a").run(Str.zero) + expect(result.value).to.equal(30) + expect(result.output.s).to.equal("15,b,15,b,+,") }); // Pour corriger ce problème on va avoir besoin de formuler une version @@ -396,7 +352,7 @@ describe('simplified tree walks', function() { let trace2 = R.curry((rules, name) => { let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, expr = find(rules, name) - return cataM(StrWriter.of, tracer2(trace2(rules)))(expr) + return cataM(Writer.of, tracer2(trace2(rules)))(expr) }) const tracer2 = recurse => a => { @@ -412,9 +368,9 @@ describe('simplified tree walks', function() { rule2 = Assign("b",num(15)), rule3 = Assign("c",num(10)), rules = [rule1,rule2,rule3], - result = trace2(rules,"a").run() - expect(result._1).to.equal(25) - expect(result._2.s).to.equal("15,b,10,c,+,") + result = trace2(rules,"a").run(Str.zero) + expect(result.value).to.equal(25) + expect(result.output.s).to.equal("15,b,10,c,+,") }); }); From a379d2e2d557038248343f00c23c7bf9fe3a1fbc Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Mon, 14 Aug 2017 16:54:04 +0200 Subject: [PATCH 17/18] :gear: Memoise le calcul via StateT --- test/tree.test.js | 57 ++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 5 deletions(-) diff --git a/test/tree.test.js b/test/tree.test.js index 65b2f120e..1d9f95c56 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -2,7 +2,7 @@ import R from 'ramda' import {expect} from 'chai' import daggy from 'daggy' import {Maybe as M} from 'ramda-fantasy' -import {Writer} from 'akh' +import {StateT, Writer} from 'akh' describe('simplified tree walks', function() { @@ -30,7 +30,7 @@ describe('simplified tree walks', function() { // Pour intégrer dans le simulateur, il faut remplir les exigences // suivantes: // X décorer l'arbre avec une valeur à chaque noeud - // - réaliser le calcul de façon efficiente (1 fois par variable) + // X réaliser le calcul de façon efficiente (1 fois par variable) // - savoir "court-circuiter" le calcul de variables manquantes dans les conditionnelles // - avoir un moyen de gérer les composantes et filtrage @@ -287,8 +287,6 @@ describe('simplified tree walks', function() { Str.prototype.zero = Str.zero Str.prototype.concat = function(b) { return Str(this.s+b.s)} - const log = (x, s) => Writer.tell(Str(s)).map(_ => x) - let trace = R.curry((rules, name) => { let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, expr = find(rules, name) @@ -296,6 +294,7 @@ describe('simplified tree walks', function() { }) const tracer = recurse => a => { + let log = (x, s) => Writer.tell(Str(s)).map(_ => x) return a.cata({ Num: (x) => log(x, x+","), Add: (x, y) => x.chain(xx => y.chain(yy => log(xx+yy,"+,"))), @@ -356,6 +355,7 @@ describe('simplified tree walks', function() { }) const tracer2 = recurse => a => { + let log = (x, s) => Writer.tell(Str(s)).map(_ => x) return a.cata({ Num: (x) => log(x,x+","), Add: (x, y) => log(x+y,"+,"), @@ -363,7 +363,7 @@ describe('simplified tree walks', function() { }) } - it('should trace the shape of the computation, too', function() { + it('should trace the shape of the computation, showing two passes through b', function() { let rule1 = Assign("a",add(ref("b"),ref("c"))), rule2 = Assign("b",num(15)), rule3 = Assign("c",num(10)), @@ -373,4 +373,51 @@ describe('simplified tree walks', function() { expect(result.output.s).to.equal("15,b,10,c,+,") }); + // On a la possibilité "d'encapsuler" une monade dans une autre: + // on va se doter d'un State, une monade qui permet de stocker un + // état et de le modifier en le propageant dans tout le calcul, et + // conserver Writer à l'intérieur (on utilise la variante StateT, + // le T veut dire "transformation de monade") + + const S = StateT(Writer) + const log = (x, s) => S.lift(S.inner.tell(Str(s)).map(_ => x)) + + let trace3 = R.curry((rules, name) => { + let find = (rules, name) => R.find(x => R.prop("name",x) == name,rules).expr, + expr = find(rules, name) + return cataM(S.of, tracer3(trace3(rules)))(expr) + }) + + const memoize = f => name => { + let cache = result => + result + .chain(x => result.modify(state => R.assoc(name,run(result),state)) + .chain(z => S.of(x))) + + return S.get.chain(state => { + let cached = state[name] + return cached ? + S.of(cached.value.value) : cache(f(name)) + }) + } + + const tracer3 = recurse => a => { + return a.cata({ + Num: (x) => log(x,x+","), + Add: (x, y) => log(x+y,"+,"), + Var: memoize ((name) => recurse(name).chain(x => log(x,name+","))) + }) + } + + const run = (c, state) => Writer.run(StateT.run(c, state),Str.zero) + + it('should trace the shape of the computation, showing one pass through b', function() { + let rule1 = Assign("a",add(ref("b"),ref("b"))), + rule2 = Assign("b",num(15)), + rules = [rule1,rule2], + result = run(trace3(rules,"a"),{}) + expect(result.value.value).to.equal(30) + expect(result.output.s).to.equal("15,b,+,") + }); + }); From 46c994702b53926c99fced30e0c1cccb6f53c15e Mon Sep 17 00:00:00 2001 From: Laurent Bossavit Date: Wed, 16 Aug 2017 13:40:41 +0200 Subject: [PATCH 18/18] =?UTF-8?q?:gear:=20Ajoute=20une=20r=C3=A9f=C3=A9ren?= =?UTF-8?q?ce?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/tree.test.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/tree.test.js b/test/tree.test.js index 1d9f95c56..788ffa917 100644 --- a/test/tree.test.js +++ b/test/tree.test.js @@ -379,6 +379,11 @@ describe('simplified tree walks', function() { // conserver Writer à l'intérieur (on utilise la variante StateT, // le T veut dire "transformation de monade") + // On peut aller plus loin et mémoiser le catamorphisme: + // https://idontgetoutmuch.wordpress.com/2011/05/15/monadic-caching-folds/ + // ça ne semble pas nécessaire ici puisque tout se passe au niveau de + // la récursion sur "Var" + const S = StateT(Writer) const log = (x, s) => S.lift(S.inner.tell(Str(s)).map(_ => x))