Esta vez vamos a descubrir quÃĐ son lenses (lentes en inglÃĐs), cÃģmo se ven en javascript y espero que al final de todo esto podamos crear una implementaciÃģn casi adecuada.
Pero primero vamos a retroceder un poco y vamos a preguntarnos.
ÂŋQuÃĐ son getter y setter?
Son funciones que deben cumplir un propÃģsito, extraer o asignar un valor. Pero claro eso no es lo Único que pueden hacer. En la mayorÃa de los casos (que yo he visto) se usan para observar los cambios a una variable y causar algÚn efecto o para colocar validaciones que impidan algÚn comportamiento no deseado.
En javascript pueden ser explÃcitos.
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 pueden ser implÃcitos.
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'
ÂŋPero quÃĐ tiene eso de malo que algunas personas sienten la necesidad de usar alternativas como lenses?
Comencemos con el segundo ejemplo. Puedo decirles a algunas personas no les gustan las cosas mÃĄgicas, el sÃģlo hecho de tener una funciÃģn que se ha estado ejecutando sin su conocimiento es suficiente para evitarlos.
El primer ejemplo es mÃĄs interesante. Vamos a verlo otra vez.
obj.get_thing(); // => 'stuff'
obj.set_thing('other stuff');
obj.get_thing(); // => 'other stuff'
Se ejecuta get_thing el resultado es stuff, hasta ahora todo bien. Pero aquà viene el problema, cuando lo usas otra vez y de la misma manera obtienes other stuff. Tienes que rastrear la Última llamada a set_thing para saber lo que obtendrÃĄs. No tienes la capacidad de predicir el resultado de get_thing, no puedes estar 100% seguro sin mirar (o saber) otras partes del cÃģdigo.
ÂŋHay una alternativa mejor?
No dirÃa mejor. Intentemos crear estos lenses, despuÃĐs pueden decidir si les gusta o no.
ÂŋQuÃĐ necesitamos? Lenses son un concepto que se encuentra en el paradigma de la programaciÃģn funcional, entonces lo primero que haremos serÃĄ crear unas funciones auxiliares. Estas serÃĄn nuestra primera versiÃģn de getter y setter.
// Getter
function prop(key) {
return obj => obj[key];
}
// Setter
function assoc(key) {
return (val, obj) => Object.assign({}, obj, {[key]: val});
}
Ahora el "constructor."
function Lens(getter, setter) {
return { getter, setter };
}
// Eso es todo.
NotarÃĄn que Lens no hace absolutamente nada, esto es a propÃģsito. Desde ya pueden darse cuenta que la mayor parte del trabajo estÃĄ en getter y setter. El resultado serÃĄ tan eficiente como lo sean sus implementaciones de getter y setter.
Ahora, para hacer que un lens haga algo Útil crearemos tres funciones.
view: Extrae un valor.
function view(lens, obj) {
return lens.getter(obj);
}
over: transforma un valor usando un callback.
function over(lens, fn, obj) {
return lens.setter(
fn(lens.getter(obj)),
obj
);
}
set: reemplaza un valor
function always(val) {
return () => val;
}
function set(lens, val, obj) {
// no es genial? Ya estamos reusando funciones
return over(lens, always(val), obj);
}
Es momento de crear unas pruebas.
Digamos que tenemos un objeto llamado alice.
const alice = {
name: 'Alice Jones',
address: ['22 Walnut St', 'San Francisco', 'CA'],
pets: { dog: 'joker', cat: 'batman' }
};
Empecemos con algo simple, vamos a inspeccionar un valor. TendrÃamos que hacer esto.
const result = view(
Lens(prop('name'), assoc('name')),
alice
);
result // => "Alice Jones"
Veo que no estÃĄn impresionados y eso estÃĄ bien. Acabo de escribir un montÃģn de cosas sÃģlo para ver un nombre. Pero este es el asunto, todo eso son funciones aisladas. Siempre tenemos la opciÃģn de combinarlas y crear nuevas. Empecemos con Lens(prop, assoc), vamos usarlo con mucha frecuencia.
function Lprop(key) {
return Lens(prop(key), assoc(key));
}
Y ahora...
const result = view(Lprop('name'), alice);
result // => "Alice Jones"
Pueden incluso ir mÃĄs allÃĄ y crear una funciÃģn que sÃģlo acepte el objeto que contiene los datos.
const get_name = obj => view(Lprop('name'), obj);
// o con aplicaciÃģn parcial
const 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`
Suficiente. Volvamos a nuestras pruebas. Vamos con over, vamos a transformar el texto a mayÚsculas.
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 acaso
get_name(alice) // => "Alice Jones"
Es el turno de set.
const alice_smith = set(Lprop('name'), 'Alice smith', alice);
get_name(alice_smith) // => "Alice smith"
// por si acaso
get_name(alice) // => "Alice Jones"
Todo muy bonito pero name es sÃģlo una propiedad, ÂŋQuÃĐ pasa con los objetos anidados o los arreglos? Bueno, es ahà donde nuestra implementaciÃģn se vuelve algo incÃģmoda. Justo ahora tendrÃamos que hacer algo asÃ.
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"
Los escucho. No se preocupen, no los dejarÃa escribir cosas asÃ. Es por cosas como esta que algunos van y dicen "usa Ramda y ya" (y tienen razÃģn) ÂŋPero quÃĐ hace ramda que lo hace tan especial?
El toque especial
Si van a la documentaciÃģn de ramda y buscan "lens" verÃĄn que tienen una funciÃģn llamada lensProp que basicamente hace lo mismo que Lprop. Y si van al cÃģdigo fuente verÃĄn esto.
function lensProp(k) {
return lens(prop(k), assoc(k));
}
Miren eso. Ahora bien, los comentarios en el cÃģdigo y la documentaciÃģn sugieren que trabaja con una sola propiedad. Volvamos a nuestra bÚsqueda en su documentaciÃģn. Ahora prestemos atenciÃģn a esa curiosa funciÃģn llamada lensPath. Parece que hace exactamente lo que queremos. Una vez mÃĄs vemos el cÃģdigo fuente y ÂŋquÃĐ vemos?
function lensPath(p) {
return lens(path(p), assocPath(p));
}
// Bienvenidos al paradigma funcional
El secreto estÃĄ en otras funciones que no tienen ningÚn vinculo especÃfico con lenses. ÂŋNo es genial?
ÂŋQuÃĐ hay en esa funciÃģn path? Vamos a revisar. Voy a mostrarles una versiÃģn ligeramente diferente, pero el comportamiento es el mismo.
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[keys[idx]];
idx += 1;
}
return result;
}
HarÃĐ lo mismo con assocPath. En este caso en ramda usan algunas funciones internas pero en esencia esto es lo que pasa.
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[0];
// 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[1] === 'number'
? []
: {}
: obj[index];
// 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[index] = value;
return arr;
} else {
// una copia como las de antes
var result = {};
for (var p in obj) {
result[p] = obj[p];
}
result[index] = value;
return result;
}
}
Con nuestro nuevo conocimiento podemos crear Lpath y mejorar Lprop.
function Lpath(keys) {
return Lens(path(keys), assocPath(keys));
}
function Lprop(key) {
return Lens(path([key]), assocPath([key]));
}
Ahora podemos hacer otras cosas, como manipular la propiedad pets de alice.
const dog_lens = Lpath(['pets', 'dog']);
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'
Todo funciona de maravilla pero hay un pequeÃąo detalle, nuestro constructor Lens no produce "instancias" combinables. Imaginen que tenemos lenses en varios lugares y queremos combinarlos de la siguiente manera.
compose(pet_lens, imaginary_lens, dragon_lens);
Eso no funcionarÃa porque compose espera una lista de funciones y lo que tenemos ahora son objetos. Pero podemos cambiar eso (de una forma muy curiosa) con algunos trucos propios de la programaciÃģn funcional.
Empecemos con el constructor. En lugar de devolver un objeto vamos retornar una funciÃģn, una que reciba "por partes" un callback, un objeto y que devuelva un Functor (eso es una cosa que tiene un mÃĐtodo map que sigue estas reglas)
function Lens(getter, setter) {
return fn => obj => {
const apply = focus => setter(focus, obj);
const functor = fn(getter(obj));
return functor.map(apply);
};
}
ÂŋY eso de fn => obj =>quÃĐ? Eso nos va ayudar con el problema que tenemos con compose. DespuÃĐs de que le proporcionas getter y setter te devuelve una funciÃģn que es compatible con compose.
ÂŋY functor.map? Eso es para asegurarnos que podamos usar un lens como una unidad (como Lprop('pets')) y tambiÃĐn como parte de una cadena usando compose.
En caso de que se pregunten quÃĐ diferencia hay con lo que hace ramda, ellos usan su propia implementaciÃģn de la funciÃģn map.
Ahora modificamos view y over. Empezando con view.
function view(lens, obj) {
const constant = value => ({ value, map: () => constant(value) });
return lens(constant)(obj).value;
}
Esa funciÃģn constant puede que parezca innecariamente compleja pero tiene su propÃģsito. Las cosas se pueden enredar mucho cuando usas compose, esa estructura se asegura que el valor que queremos se mantenga intacto.
ÂŋY over? Es casi igual, excepto que en ese caso sà utilizamos la funciÃģn setter.
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;
}
Y ahora deberÃamos tener una implementaciÃģn casi adecuada. Esto es lo que tenemos sin contar las dependencias (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([key]), assocPath([key]));
}
function Lpath(keys) {
return Lens(path(keys), assocPath(keys));
}
function always(val) {
return () => val;
}
ÂŋMe creerÃan si les digo que funciona? No deberÃan. Hagamos unas pruebas. Volvamos con alice y vamos aÃąadirle otro objeto, calie.
const alice = {
name: "Alice Jones",
address: ["22 Walnut St", "San Francisco", "CA"],
pets: { dog: "joker", cat: "batman", imaginary: { dragon: "harley" } }
};
const calie = {
name: "calie Jones",
address: ["22 Walnut St", "San Francisco", "CA"],
pets: { dog: "riddler", cat: "ivy", imaginary: { dragon: "hush" } },
friend: [alice]
};
Y porque teniamos todo planeado desde antes, ya tenemos unos lenses disponibles.
// uno genÃĐrico
const head_lens = Lprop(0);
// otros especÃficos
const bff_lens = compose(Lprop('friend'), head_lens);
const imaginary_lens = Lpath(['pets', 'imaginary']);
Supongamos que queremos manipular la propiedad dragon de cada una, todo lo que tenemos que hacer es combinar.
const dragon_lens = compose(imaginary_lens, Lprop('dragon'));
// sÃģlo porque sÃ
const bff_dragon_lens = compose(bff_lens, dragon_lens);
// demo
const upper = str => str.toUpperCase();
// view
view(dragon_lens, calie); // => "hush"
view(bff_dragon_lens, calie); // => "harley"
// over
let 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"
// set
new_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"
Asà que acabamos de manipular un objeto anidado en varios niveles combinando lenses. Resolvimos un problema combinando funciones. Si no les parece genial no sÃĐ quÃĐ mÃĄs decirles.
Estas cosas son difÃciles de vender porque requieren de un estilo particular para poder aprovecharlos al mÃĄximo. Y para los que usan javascript, probablemente existe una librerÃa que resuelve el mismo problema pero de una manera mÃĄs conveniente o por lo menos que se ajuste a su estilo.
En fin, si aÚn estÃĄn interesado en cÃģmo funcionarÃan estos lenses en un contexto mÃĄs complejo revisen este repositorio, es un ejemplo de "real world app" (algo asà como un clon de medium.com) usa hyperapp para manejar la interfaz. El autor quiso usar lenses para manejar el estado de la aplicaciÃģn.
Fuentes
Gracias por su tiempo. Si este artÃculo les pareciÃģ Útil y quieren apoyar mis esfuerzos para crear mÃĄs contenido pueden dejar una propina en ko-fi.com/vonheikemen.

Top comments (0)