Lentes ou máis ben que os getters e os axustes combinables

Esta vez descubriremos que son lentes (lentes en inglés), como miran a JavaScript e espero que ao final de todo isto poidamos crear Unha implementación case adecuada.

Pero primeiro imos volver un pouco e imos preguntarnos a nós mesmos.

Que son getter e setter?

son funcións que se debe facer para un propósito, extraer ou asignar un valor. Pero por suposto que non é o único que poden facer. Na maioría dos casos (que vin) úsanse para observar os cambios nunha variable e causar algún efecto ou poñer validacións que impiden calquera comportamento non desexado.

en JavaScript pode ser explícito.

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'

ou pode ser implícito.

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 o que ten que ter algo malo que algunhas persoas senten a necesidade de usar alternativas como leses?

Comecemos co segundo exemplo. Podo dicir a algunhas persoas que non lles gustan as cousas máxicas, só tendo unha función que se executaba sen o seu coñecemento é suficiente para evitalos.

O primeiro exemplo é máis interesante. Vexámolo de novo.

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

executar get_thing é o resultado stuff, ata agora ben. Pero aquí vén o problema, cando o usa de novo e do mesmo xeito que obtén other stuff. Ten que seguir a última chamada a set_thing para saber o que recibirá. Non ten a capacidade de predicar o resultado de get_thing, non pode ser 100% seguro sen mirar (ou coñecer) outras partes do código.

¿Hai unha alternativa mellor?

Non diría mellor. Intentemos crear esta lente, entón podes decidir se che gusta ou non.

Que necesitamos? A lente é un concepto que está no paradigma da programación funcional, entón o primeiro que faremos creará funcións auxiliares. Estes serán a nosa primeira versión de Getter e Setter.

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

Agora o “constructor”

IV id = “

notará que Lens non fai absolutamente nada, isto é de propósito. Dende que pode entender que a maior parte do traballo está en getter e setter. O resultado será tan eficiente como as súas implementacións de getter e setter.

Agora, para facer un lens facer algo útil crearemos tres funcións.

view: extrae un valor.

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

over: transformar un valor usando unha devolución de chamada.

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

set: substituír 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);}

é hora de crear algunhas probas.

Digamos que temos un obxecto chamado alice .

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

Comecemos con algo sinxelo, imos inspeccionar un valor. Teriamos que facer isto.

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

Vexo que non están impresionados e iso está ben. Acabo de escribir moitas cousas só para ver un nome. Pero este é o problema, todas as funcións illadas. Sempre temos a opción de combinar e crear novos. Comecemos con Lens(prop, assoc), imos usalo con moita frecuencia.

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

e agora …

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

pode incluso ir máis aló e crear unha función que só acepta o obxecto que contén os datos.

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`

suficiente. Volvamos ás nosas probas. Imos con over, transformaremos o texto en maiú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 acasoget_name(alice) // => "Alice Jones"

é a quenda de set.

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

todo Moi agradable, pero name é só unha propiedade, que pasa cos obxectos ou arranxos anidados? Ben, é aí onde a nosa implementación convértese en algo incómodo. Agora, teriamos que facer algo parecido.

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"

escoito a eles. Non te preocupes, non me deixaría escribir cousas así. É por mor das cousas que algúns van e din “EUA Ramda e xa” (e son correctos), pero o que o fai Ramda tan especial?

o toque especial

van a A documentación de Ramda e buscar “lente” verá que teñen unha función chamada que basicamente fai o mesmo que Lprop. E se van ao código fonte verá isto.

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

mirar iso.Agora, os comentarios do código e a documentación suxiren que traballas cunha única propiedade. Volvamos á nosa busca na túa documentación. Agora imos prestar atención a esa curiosa función chamada lensPath. Parece que fai exactamente o que queremos. Unha vez máis vemos o código fonte e que vemos?

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

O segredo está noutras funcións que o fan Non ten ningunha ligazón específica con lenses. Non é xenial?

Que hai nesa función path? Comprobamos. Vou amosar-lle unha versión un pouco diferente, pero o comportamento é o mesmo.

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;}

i ‘ facer o mesmo con assocPath. Neste caso, en Ramda use algunhas funcións internas, pero en esencia isto é o que ocorre.

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; }}

co noso novo coñecemento Podemos crear Lpath e mellorar Lprop.

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

Agora podemos facer outras cousas, como manipular a propiedade pets de alice.

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'

Todo funciona moi ben, pero hai un pequeno detalle, o noso constructor Lens non produce “instancias combinables “. Imaxina que temos leses en varios lugares e queremos combinalos do seguinte xeito.

que non funcionaría porque compose Agarde a unha lista de funcións e o que temos agora son obxectos. Pero podemos cambiar isto (dun xeito moi curioso) con algúns trucos de programación funcional.

Comecemos co constructor. En lugar de devolver un obxecto, devolveremos unha función, unha que recibe “por” unha devolución de chamada, un obxecto e que devolve unha función (que é unha cousa que ten un método map que Siga estas regras)

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

e a de fn => obj => que? Isto axudaranos co problema que temos con compose. Despois de proporcionarlle getter e setter devolve unha función que é compatible con compose.

e functor.map? Isto é para asegurarse de que podemos usar unha lente como unha unidade (como Lprop('pets')) e tamén como parte dunha cadea usando compose.

No caso de que pregunte a diferenza que hai con que Ramda fai, utilizan a súa propia implementación da función map.

agora modifican view e over. Comezando con view.

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

que funciona constant pode parecer pouco complexo pero ten o seu propósito. As cousas poden ser enredadas moito cando usas compose, que a estrutura está garantida de que o valor que queiramos permanecer intacto.

e over? É case o mesmo, agás que nese caso usamos a 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;}

E agora debemos ter unha implementación case adecuada. Isto é o que temos sen contar as dependencias (path e 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;}

¿Crerías se digo que funciona? Non debe. Fagamos algunhas probas. Imos volver con alice e imos engadir outro obxecto, calie.

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: };

e porque tivemos todo o planificado antes, xa temos algunha lente dispoñible.

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

Supoñamos que queremos manipular a propiedade dragon de cada un, todo o que temos que facer é 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"

Así que só manipulamos un obxecto anidado en varios niveis que combinan a lente. Resolvemos un problema que combina funcións. Se non o pensas xenial, non sei que máis contarlles.

Estas cousas son difíciles de vender porque requiren un estilo particular para poder aproveitarlos ao máximo. E para aqueles que usan JavaScript, probablemente hai unha biblioteca que resolve o mesmo problema, pero de forma máis cómoda ou polo menos encaixa no seu estilo.

De todos os xeitos, se aínda están interesados en como farían funciona Estas lentes nunha revisión contexto máis complexo este repositorio, é un exemplo de “real World App” (algo así como un clon do Medium.com) utiliza Hyperapp para xestionar a interface. O autor quería usar a lente para manexar o estado da aplicación.

Deixa unha resposta

O teu enderezo electrónico non se publicará Os campos obrigatorios están marcados con *