Lenses o millor dit getters i setters combinables

Aquesta vegada anem a descobrir què són lenses (lents en anglès), com es veuen en javascript i espero que a la fi de tot això puguem crear una implementació gairebé adequada.

Però primer anem a retrocedir una mica i anem a preguntar-nos.

què són getter i setter?

són funcions que han de complir un propòsit, extreure o assignar un valor. Però clar això no és l’únic que poden fer. En la majoria dels casos (que jo he vist) s’usen per observar els canvis a una variable i causar algun efecte o per col·locar validacions que impedeixin algun comportament no desitjat.

En javascript poden ser explícits.

function Some() { let thing = 'stuff'; return { get_thing() { // puedes hacer lo que sea aquí return thing; }, set_thing(value) { // igual aquí thing = value; } }}let obj = Some();obj.get_thing(); // => 'stuff'obj.set_thing('other stuff');obj.get_thing(); // => 'other stuff'

O poden ser implícits.

let some = {};Object.defineProperty(some, 'thing', { get() { return 'thing'; }, set(value) { console.log("no pasarás"); }});some.thing // => 'thing'some.thing = 'what?';//// no pasarás//some.thing // => 'thing'

Però què té això de dolent que algunes persones senten la necessitat d’usar alternatives com lenses?

Comencem amb el segon exemple. Puc dir-los a algunes persones no els agraden les coses màgiques, el sol fet de tenir una funció que s’ha estat executant sense el seu coneixement és suficient per evitar-los.

El primer exemple és més interessant. Anem a veure-ho una altra vegada.

obj.get_thing(); // => 'stuff'obj.set_thing('other stuff');obj.get_thing(); // => 'other stuff'

S’executa get_thing el resultat és stuff, fins ara tot bé. Però aquí ve el problema, quan el fas servir una altra vegada i de la mateixa manera obtens other stuff. Has de rastrejar l’última crida a set_thing per saber el que obtindràs. No tens la capacitat de predicir el resultat de get_thing, no pots estar 100% segur sense mirar (o saber) altres parts de el codi.

Hi ha una alternativa millor?

No diria millor. Intentem crear aquests lenses, després poden decidir si els agrada o no.

Què necessitem? Lenses són un concepte que es troba en el paradigma de la programació funcional, llavors el primer que farem serà crear unes funcions auxiliars. Aquestes seran la nostra primera versió de getter i setter. A

// Getterfunction prop(key) { return obj => obj;}// Setterfunction assoc(key) { return (val, obj) => Object.assign({}, obj, {: val});}

Ara el “constructor.” A

function Lens(getter, setter) { return { getter, setter };}// Eso es todo.

Notaran que Lens no fa absolutament res, això és a propòsit. Des de ja poden donar-se compte que la major part de la feina està en getter i setter. El resultat serà tan eficient com ho siguin els seus implementacions de getter i setter.

Ara, per fer que un lens faci alguna cosa útil crearem tres funcions.

view: Extreu un valor. a

function view(lens, obj) { return lens.getter(obj);}

over: transforma un valor usant un callback.

function over(lens, fn, obj) { return lens.setter( fn(lens.getter(obj)), obj );}

set: reemplaça un valor a

function always(val) { return () => val;}function set(lens, val, obj) { // no es genial? Ya estamos reusando funciones return over(lens, always(val), obj);}

És moment de crear unes proves.

Diguem que tenim un objecte anomenat alice.

const alice = { name: 'Alice Jones', address: , pets: { dog: 'joker', cat: 'batman' }};

Comencem amb una cosa simple, anem a inspeccionar un valor. Hauríem de fer això.

const result = view( Lens(prop('name'), assoc('name')), alice);result // => "Alice Jones"

Veig que no estan impressionats i això està bé. Acabo d’escriure un munt de coses només per veure un nom. Però aquest és l’assumpte, tot això són funcions aïllades. Sempre tenim l’opció de combinar-les i crear noves. Comencem amb Lens(prop, assoc), anem usar-lo amb molta freqüència.

function Lprop(key) { return Lens(prop(key), assoc(key));}

i ara … a

const result = view(Lprop('name'), alice);result // => "Alice Jones"

Poden fins i tot anar més enllà i crear una funció que només accepti l’objecte que conté les dades.

const get_name = obj => view(Lprop('name'), obj);// o con aplicación parcialconst get_name = view.bind(null, Lprop('name'));// o usando una dependencia.// view = curry(view);const get_name = view(Lprop('name'));// y lo mismo aplica para `set` y `over`

Suficient. Tornem a les nostres proves. Anem amb over, anem a transformar el text a majúscules. A

const upper = str => str.toUpperCase();const uppercase_alice = over(Lprop('name'), upper, alice);// vieron lo que hice?get_name(uppercase_alice) // => "ALICE JONES"// por si acasoget_name(alice) // => "Alice Jones"

És el torn de set. a

const alice_smith = set(Lprop('name'), 'Alice smith', alice);get_name(alice_smith) // => "Alice smith"// por si acasoget_name(alice) // => "Alice Jones"

Tot molt bonic però name és només una propietat, què passa amb els objectes niats o organitzar? Bé, és aquí on la nostra implementació es torna una mica incòmoda. Just ara hauríem de fer alguna cosa així.

let dog = Lens( obj => prop('dog')(prop('pets')(obj)), obj => assoc('dog')(assoc('pets')(obj)));view(dog, alice); // => "joker"// o traemos una dependencia, `compose`dog = Lens( compose(prop("dog"), prop("pets")), compose(assoc("dog"), assoc("pets")));view(dog, alice); // => "joker"

Els escolto. No es preocupin, no els deixaria escriure coses així. És per coses com aquesta que alguns van i diuen “utilitza Ramda i ja” (i tenen raó) Però què fa Ramda que el fa tan especial?

El toc especial

Si van a la documentació de Ramda i busquen “lens” veuran que tenen una funció anomenada lensProp que bàsicament fa el mateix que Lprop. I si van a el codi font veuran això.

function lensProp(k) { return lens(prop(k), assoc(k));}

Miren això.Ara bé, els comentaris en el codi i la documentació suggereixen que treballa amb una sola propietat. Tornem a la nostra recerca en la seva documentació. Ara parem atenció a aquesta curiosa funció anomenada lensPath. Sembla que fa exactament el que volem. Un cop més veiem el codi font i què veiem? A

function lensPath(p) { return lens(path(p), assocPath(p));}// Bienvenidos al paradigma funcional

El secret està en altres funcions que no tenen cap vincle específic amb lenses. No és genial?

Què hi ha en aquesta funció path? Anem a revisar. Vaig a mostrar-los una versió lleugerament diferent, però el comportament és el mateix.

function path(keys, obj) { if (arguments.length === 1) { // esto es para imitar la dependencia `curry` // esto es lo que pasa // retornan una función que recuerda `keys` // y espera el argumento `obj` return path.bind(this, keys); } var result = obj; var idx = 0; while (idx < keys.length) { // no nos agrada null if (result == null) { return; } // así obtenemos los objetos anidados result = result]; idx += 1; } return result;}

Faré el mateix amb assocPath. En aquest cas en Ramda fan servir algunes funcions internes però en essència això és el que passa. A

function assocPath(path, value, obj) { // otra vez esto // por eso tienen la función `curry` if (arguments.length === 1) { return assocPath.bind(this, path); } else if (arguments.length === 2) { return assocPath.bind(this, path, value); } // revisamos si está vacío if (path.length === 0) { return value; } var index = path; // Cuidado: recursividad adelante if (path.length > 1) { var is_empty = typeof obj !== 'object' || obj === null || !obj.hasOwnProperty(index); // si el objeto actual está "vacío" // tenemos que crear otro // de lo contrario usamos el valor en `index` var next = is_empty ? typeof path === 'number' ? : {} : obj; // empecemos otra vez // pero ahora con un `path` reducido // y `next` es el nuevo `obj` value = assocPath(Array.prototype.slice.call(path, 1), value, next); } // el caso base // o copiamos un arreglo o un objeto if (typeof index === 'number' && Array.isArray(obj)) { // 'copiamos' el arreglo var arr = .concat(obj); arr = value; return arr; } else { // una copia como las de antes var result = {}; for (var p in obj) { result = obj; } result = value; return result; }}

Amb el nostre nou coneixement podem crear Lpath i millorar Lprop. a

function Lpath(keys) { return Lens(path(keys), assocPath(keys));}function Lprop(key) { return Lens(path(), assocPath());}

Ara podem fer altres coses, com manipular la propietat pets de alice. a

const dog_lens = Lpath();view(dog_lens, alice); // => 'joker'let new_alice = over(dog_lens, upper, alice);view(dog_lens, new_alice); // => 'JOKER'new_alice = set(dog_lens, 'Joker', alice);view(dog_lens, new_alice); // => 'Joker'

Tot funciona de meravella però hi ha un petit detall, el nostre constructor Lens no produeix “instàncies” combinables. Imagineu que tenim lenses en diversos llocs i volem combinar-los de la següent manera.

compose(pet_lens, imaginary_lens, dragon_lens);

Això no funcionaria perquè compose s’espera una llista de funcions i el que tenim ara són objectes. Però podem canviar això (d’una manera molt curiosa) amb alguns trucs propis de la programació funcional.

Comencem amb el constructor. En lloc de tornar un objecte anem retornar una funció, un que rebi “per parts” 1 callback, un objecte i que retorni un Functor (això és una cosa que té un mètode map que segueix aquestes regles) a

function Lens(getter, setter) { return fn => obj => { const apply = focus => setter(focus, obj); const functor = fn(getter(obj)); return functor.map(apply); };}

I això de fn => obj => què ? Això ens va ajudar amb el problema que tenim amb compose. Després que li proporciones getter i setter et retorna una funció que és compatible amb compose .

I functor.map? Això és per assegurar-nos que puguem utilitzar un lens com una unitat (com Lprop('pets')) i també com a part d’una cadena usant compose.

En el cas que es preguntin quina diferència hi ha amb el que fa Ramda, ells usen la seva pròpia implementació de la funció map.

Ara modifiquem view i over. Començant amb view. A

function view(lens, obj) { const constant = value => ({ value, map: () => constant(value) }); return lens(constant)(obj).value;}

Aquesta funció constant pot ser que sembli innecariamente complexa però té el seu propòsit. Les coses es poden embolicar molt quan fas servir compose, aquesta estructura s’assegura que el valor que volem es mantingui intacte.

I over? És gairebé igual, excepte que en aquest cas sí utilitzem la funció setter. A

function over(lens, fn, obj) { const identity = value => ({ value, map: setter => identity(setter(value)) }); const apply = val => identity(fn(val)); return lens(apply)(obj).value;}

I ara hauríem de tenir una implementació gairebé adequada. Això és el que tenim sense comptar les dependències (path and assocPath).

function Lens(getter, setter) { return fn => obj => { const apply = focus => setter(focus, obj); const functor = fn(getter(obj)); return functor.map(apply); };}function view(lens, obj) { const constant = value => ({ value, map: () => constant(value) }); return lens(constant)(obj).value;}function over(lens, fn, obj) { const identity = value => ({ value, map: setter => identity(setter(value)) }); const apply = val => identity(fn(val)); return lens(apply)(obj).value;}function set(lens, val, obj) { return over(lens, always(val), obj);}function Lprop(key) { return Lens(path(), assocPath());}function Lpath(keys) { return Lens(path(keys), assocPath(keys));}function always(val) { return () => val;}

Em creurien si els dic que funciona? No haurien. Fem unes proves. Tornem amb alice i anem afegir un altre objecte, calie. A

const alice = { name: "Alice Jones", address: , pets: { dog: "joker", cat: "batman", imaginary: { dragon: "harley" } }};const calie = { name: "calie Jones", address: , pets: { dog: "riddler", cat: "ivy", imaginary: { dragon: "hush" } }, friend: };

I perquè teniem tot planejat des d’abans, ja tenim uns lenses disponibles.

// uno genéricoconst head_lens = Lprop(0);// otros específicosconst bff_lens = compose(Lprop('friend'), head_lens); const imaginary_lens = Lpath();

Suposem que volem manipular la propietat dragon de cadascuna, tot el que hem de fer és combinar.

const dragon_lens = compose(imaginary_lens, Lprop('dragon'));// sólo porque síconst bff_dragon_lens = compose(bff_lens, dragon_lens); // democonst upper = str => str.toUpperCase();// viewview(dragon_lens, calie); // => "hush"view(bff_dragon_lens, calie); // => "harley"// overlet new_calie = over(dragon_lens, upper, calie);view(dragon_lens, new_calie); // => "HUSH"new_calie = over(bff_dragon_lens, upper, calie);view(bff_dragon_lens, new_calie); // => "HARLEY"// setnew_calie = set(dragon_lens, 'fluffykins', calie);view(dragon_lens, new_calie); // => "fluffykins"new_calie = set(bff_dragon_lens, 'pumpkin', calie);view(bff_dragon_lens, new_calie); // => "pumpkin"

Així que acabem de manipular un objecte niat en diversos nivells combinant lenses. Vam resoldre un problema combinant funcions. Si no els sembla genial no sé què més dir-los.

Aquestes coses són difícils de vendre perquè requereixen d’un estil particular per poder aprofitar-los a l’màxim. I per als que fan servir javascript, probablement hi ha una llibreria que resol el mateix problema però d’una manera més convenient o almenys que s’ajusti al seu estil.

En fi, si encara estan interessat en com funcionarien aquests lenses en un context més complex revisin aquest repositori, és un exemple de “real world app” (alguna cosa així com un clon d’medium.com) fa servir hyperapp per gestionar la interfície. L’autor va voler fer servir lenses per gestionar l’estat de l’aplicació.

Deixa un comentari

L'adreça electrònica no es publicarà. Els camps necessaris estan marcats amb *