diff --git a/.gitignore b/.gitignore index 5a41b24..c40bb27 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ lib .merlin _opam/ .idea/ -.bsb.lock \ No newline at end of file +.bsb.lock +ppx/focus.native diff --git a/bsconfig.json b/bsconfig.json index 19a225f..651f010 100644 --- a/bsconfig.json +++ b/bsconfig.json @@ -7,19 +7,28 @@ "error": "+8" }, "sources": [ - {"dir": "src"}, - {"dir": "ppx", "type": "ppx", "ppx": ["Focus"]}, - {"dir": "tests", "type": "dev", "subdirs": true, "ppx": ["Focus"], "backend": "native"} + { "dir": "src" }, + { "dir": "ppx", "type": "ppx", "ppx": ["Focus"] }, + { + "dir": "tests", + "type": "dev", + "subdirs": true, + "ppx": ["Focus"], + "backend": "native" + } ], "ocaml-dependencies": ["compiler-libs", "unix", "bigarray", "str"], "public": ["Main"], - "entries": [{ - "backend": "native", - "main-module": "Tests" - }, { - "backend": "native", - "type": "ppx", - "main-module": "Focus" - }], + "entries": [ + { + "backend": "native", + "main-module": "Tests" + }, + { + "backend": "native", + "type": "ppx", + "main-module": "Focus" + } + ], "refmt": 3 } diff --git a/ppx/Focus.re b/ppx/Focus.re index ff89b3a..41d76b5 100644 --- a/ppx/Focus.re +++ b/ppx/Focus.re @@ -6,17 +6,23 @@ open Ast_helper; let fail = (loc, txt) => raise(Location.Error(Location.error(~loc, txt))); let lid = (~loc=default_loc^, s) => mkloc(Longident.parse(s), loc); -let evar = (~loc=?, ~attrs=?, s) => Exp.ident(~loc?, ~attrs?, lid(~loc?, s)); +let evar = (~loc=?, ~attrs=?, s) => + Exp.ident(~loc?, ~attrs?, lid(~loc?, s)); let updated_record = (record, field, value) => Exp.mk( Pexp_record( - [(mknoloc(Lident(field)), Exp.mk(Pexp_ident(mknoloc(Lident(value)))))], + [ + ( + mknoloc(Lident(field)), + Exp.mk(Pexp_ident(mknoloc(Lident(value)))), + ), + ], Some(Exp.mk(Pexp_ident(mknoloc(Lident(record))))), ), ); -let createLens = name => [%expr +let createFieldLens = name => [%expr { get: r => [%e Exp.field(evar("r"), mknoloc(Lident(name)))], set: (v, r) => [%e updated_record("r", name, "v")], @@ -26,34 +32,69 @@ let createLens = name => [%expr let focus_mapper = Parsetree.{ ...Ast_mapper.default_mapper, - expr: (mapper, expr) => - switch (expr.pexp_desc) { - | Pexp_field(pexp, {txt}) => - %expr - { - open Lens; - let%e rec recurseField = ( - fun - | {pexp_desc: Pexp_field({pexp_desc: Pexp_ident({txt: Lident(name)})}, {txt: Lident(snd_name)})} => { - print_string("base case: \n"); - print_string("name: " ++ name); - print_newline(); - print_string("snd_name: " ++ snd_name); - print_newline(); + expr: (mapper, expr) => [%expr + { + open Lens; + let%e rec recurseField = + fun + | { + pexp_desc: + Pexp_field( + {pexp_desc: Pexp_ident({txt: Lident(name)})}, + {txt: Lident(snd_name)}, + ) | + Pexp_apply( + {pexp_desc: Pexp_ident({txt: Lident("$.")})}, + [ + (_, {pexp_desc: Pexp_ident({txt: Lident(name)})}), + (_, {pexp_desc: Pexp_ident({txt: Lident(snd_name)})}), + ], + ), + } => [%expr + [%e createFieldLens(snd_name)] + ] + | { + pexp_desc: + Pexp_apply( + {pexp_desc: Pexp_ident({txt: Lident("$.")})}, + [ + (_, {pexp_desc: Pexp_ident({txt: Lident(name)})}), + (_, next), + ], + ), + } => { + %expr + [%e recurseField(next)] + } + | { + pexp_desc: + Pexp_field(next, {txt: Lident(name)}) | + Pexp_apply( + {pexp_desc: Pexp_ident({txt: Lident("$.")})}, + [ + (_, next), + (_, {pexp_desc: Pexp_ident({txt: Lident(name)})}), + ], + ), + } => [%expr + [%e recurseField(next)] |-- [%e createFieldLens(name)] + ] - %expr - [%e createLens(snd_name)]; - } - | {pexp_desc: Pexp_field(next, {txt: Lident(name)})} => [%expr - [%e recurseField(next)] |-- [%e createLens(name)] - ] - | x => Ast_mapper.default_mapper.expr(mapper, x) - ); + | { + pexp_desc: + Pexp_apply( + {pexp_desc: Pexp_ident({txt: Lident("$.")})}, + [(_, left), (_, right)], + ), + } => [%expr + [%e recurseField(left)] |-? [%e recurseField(right)] + ] - recurseField(expr); - } - | _ => fail(expr.pexp_loc, "invalid lens declaration") - }, + | x => Ast_mapper.default_mapper.expr(mapper, x); + + recurseField(expr); + } + ], }; let mapper = _argv => @@ -61,7 +102,10 @@ let mapper = _argv => ...Ast_mapper.default_mapper, expr: (mapper, expr) => switch (expr.pexp_desc) { - | Pexp_extension(({txt: "focus", loc}, PStr([{pstr_desc: Pstr_eval(expr, attributes)}]))) => + | Pexp_extension(( + {txt: "focus", loc}, + PStr([{pstr_desc: Pstr_eval(expr, attributes)}]), + )) => focus_mapper.expr(focus_mapper, expr) | _ => Ast_mapper.default_mapper.expr(mapper, expr) }, diff --git a/src/Lens.re b/src/Lens.re index bd1fb2e..ef1b35a 100644 --- a/src/Lens.re +++ b/src/Lens.re @@ -4,6 +4,11 @@ type t('a, 'b) = { }; let (|-) = (f, g, x) => g(f(x)); +let (|?) = (f, g, x) => + switch (f(x)) { + | Some(x) => g(x) + | None => None + }; let over = (l, f, a) => { let value = l.get(a); @@ -17,7 +22,32 @@ let modify = (l, f, a) => { l.set(new_value, a); }; -let compose = (l1, l2) => {get: l2.get |- l1.get, set: l1.set |- modify(l2)}; +let flatMap = (f, x) => + switch (x) { + | Some(x) => f(x) + | None => None + }; + +let optionalModify = (l, f, a) => { + let value = l.get(a); + switch (value) { + | Some(x) => + let new_value = f(x); + l.set(Some(new_value), a); + | None => a + }; +}; + +let compose = (l1, l2) => { + get: l2.get |- l1.get, + set: l1.set |- modify(l2), +}; + +let optional = (l1, l2) => { + get: l2.get |? l1.get, + set: l1.set |- optionalModify(l2), +}; let (--|) = compose; let (|--) = (l1, l2) => compose(l2, l1); +let (|-?) = (l1, l2) => optional(l2, l1); diff --git a/tests/Tests.re b/tests/Tests.re index 00b4823..940cd35 100644 --- a/tests/Tests.re +++ b/tests/Tests.re @@ -1,3 +1,16 @@ +/* helpers */ +let string_of_option = x => + switch (x) { + | None => "None" + | Some(x) => "Some(" ++ x ++ ")" + }; + +let opt_map = (f, x) => + switch (x) { + | Some(x) => Some(f(x)) + | None => None + }; + type department = { name: string, address, @@ -12,14 +25,19 @@ type person = { age: int, department, address, + friend: option(person), }; let streetL = [%focus person.department.address.street]; -/*let streetNumber = [%focus person.address?street_number];*/ +let streetNumber = [%focus person.address $. street_number]; let addressL = [%focus person.department.address]; +let friend = [%focus person $. friend]; + +let friendStreetNumber = [%focus person $. friend.address $. street_number]; + let addressStreetL = [%focus address.street]; let department = { @@ -30,7 +48,18 @@ let department = { }, }; -let person = { +let zach = { + name: "zach", + age: 42, + address: { + street: "some street", + street_number: Some(100), + }, + department, + friend: None, +} + +let bob = { name: "bob", age: 42, address: { @@ -38,18 +67,110 @@ let person = { street_number: None, }, department, + friend: Some(zach), +} + +let alice = { + name: "alice", + age: 24, + address: { + street: "some street", + street_number: Some(13), + }, + department, + friend: Some(bob), }; -print_string("street: " ++ streetL.get(person)); +let f = friend.get(bob); + +print_string("bob's street: " ++ streetL.get(bob)); print_newline(); -print_string("department street: " ++ addressL.get(person).street); +print_string("bob's department street: " ++ addressL.get(bob).street); print_newline(); -print_string("department street inline: " ++ [%focus person.department.address].get(person).street); +print_string( + "bob's street_number: " + ++ (streetNumber.get(bob) |> opt_map(string_of_int) |> string_of_option), +); +print_newline(); + +print_string( + "alice's friend street_number: " + ++ (friendStreetNumber.get(alice) |> opt_map(string_of_int) |> string_of_option), +); print_newline(); print_string( - "set street on adress record: " ++ addressStreetL.set("Infinite Loop 1", person.department.address).street, + "bob's friend street_number: " + ++ (friendStreetNumber.get(bob) |> opt_map(string_of_int) |> string_of_option), ); print_newline(); + +print_string( + "alice's street_number: " + ++ (streetNumber.get(alice) |> opt_map(string_of_int) |> string_of_option), +); +print_newline(); + +print_string( + "sets alice's street_number: " + ++ ( + streetNumber.set(None, alice) + |> streetNumber.get + |> opt_map(string_of_int) + |> string_of_option + ), +); +print_newline(); + +print_string( + "bob's department street inline: " + ++ [%focus person.department.address].get(bob).street, +); +print_newline(); + +print_string( + "set bob's street on adress record: " + ++ addressStreetL.set("Infinite Loop 1", bob.department.address).street, +); +print_newline(); + +/* + /* {get: a => a.department, set: (a, value) => {...a, department: value}} |-- {get: a => a.address, set: (a, value) => {...a, address: value}}] + */ + let l1 = a => a.department; + let l2 = a => a.address; + + l1 |-- l2; + compose(l2, l1); + l1 |- l2; + l2(l1(x)); + + l1 |-- l2; + compose(l2, l1); + l1 |- l2; + switch (l1(x)) { + | Some(x) => l2(x) + | None => None + }; + + let l1 = (value, a) => {...a, department: value}; + let l2 = (value, a) => {...a, street: value}; + + l1 |-- l2; + compose(l2, l1); + l2 |- modify(l1); + modify(l1, l2(x)); + + let modify = (l1, f, a) => { + let value = l1.get(a); + switch (value) { + | Some(x) => + let new_value = f(value); + l1.set(Some(new_value), a); + | None => l1.set(None, a) + }; + }; + + */