Estructuras de datos teóricos y su aplicación en JavaScript. P1 Parejas

“Los malos programadores piensan en el código. Los buenos programadores piensan en las estructuras de datos y sus relaciones ”, Linus Torvalds, creador de Linux.
Tomamos como un axioma que muy a menudo la solución a cualquier problema en la programación se reduce a elegir la estructura de datos correcta. Este axioma se puede probar, pero es mucho tiempo y el artículo trata un poco sobre otro. Hoy hablaremos sobre una de las estructuras teóricas más simples, sobre el par.

Un par es una estructura de datos que almacena dos elementos de datos en sí mismo, lo que implica que de alguna manera queremos combinarlos lógicamente en uno. Resuelve precisamente este problema, es decir Si hay algunos elementos de datos y bnecesitamos presentarlos en el formulario b, entonces podemos implementar este diseño en pares.

Si crees que nunca has encontrado o no vas a encontrar algo como esto, entonces no es así.

Aquí está el ejemplo más simple de una propiedad calculada de la documentación de VUE:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

Desde el punto de vista de la teoría, fullNameesta es una pareja. Y hay innumerables ejemplos de este tipo en la tecnología y el mundo real, que a menudo modelamos con código de programa.

Puede notar con razón que podemos combinar lógicamente cualquier número de elementos y tendrá razón. Esto nos lleva a un concepto estructural más complejo de datos compuestos , uno de los cuales es un par . Trataremos los datos compuestos un poco más tarde, ahora nos centraremos en el par.

La teoría nos dice que los pares tienen las siguientes propiedades:

  • , .. - , , . , ( - . , )
  • . . . , , , , , .

    :

    pair = { a, b }
    

    Si en el código de llamada trabajaremos con el par directamente en este estilo: al pair.acambiar la implementación del par, nos veremos obligados a reescribir el código de llamada siempre que aparezca la llamada al par. ¡Esto no es genial!

    Una pareja es una abstracción (incluso un nivel bajo), por lo que sería un error trabajar directamente con sus componentes. Al trabajar con los componentes de cualquier abstracción, debemos usar la interfaz de abstracción, de lo contrario, el código se convertirá en un caos (será más difícil de leer, más fácil cometer un error, pero lo más importante, más difícil de cambiar, porque un cambio tendrá que hacerse en diferentes partes del código).


Los pares se pueden implementar de maneras muy diferentes. En cualquier lenguaje de programación, hay más de una oportunidad para implementar esta estructura de datos.
Por ejemplo, puede implementar un par para tipos de datos arbitrarios:

//       
//      
const pair = (first, second) => (elementType) => {
  switch(elementType) {
    case 'first':
     return first;
   case 'second':
     return second;
  }
};
//    ,      ,
//    
const getFirstElement = (pair) => (pair('first'));
const getSecondElement = (pair) => (pair('second'));

Esta forma de trabajar se llama enviar mensajes. Curiosamente, pero fue esta forma de interacción entre entidades que una vez se llamó OOP antes de C / C ++.

¿Tenemos que usar el diseño switch ? Por supuesto que no necesariamente. Estos son los detalles de la implementación técnica, ¡y puede haber muchas implementaciones!

¡Lo más importante es hacer que la pareja sea mutable!

Por ejemplo, puede implementar la misma funcionalidad usando Map

//     
//  ,       
const pair = (first, second) => (
  new Map([
    ['first', first],
    ['second', second],
  ])
);
//    
const getFirst = (pair) => (pair.get('first'));
const getSecond = (pair) => (pair.get('second'));

Tenga en cuenta que la primera implementación se puede reemplazar fácilmente con la segunda. y el segundo al primero! El código de llamada no cambiará. Esta es una ventaja importante de las abstracciones. Podemos cambiar fácilmente la implementación de la abstracción en el programa, pero el código que trabaja con esta abstracción no sabrá acerca de estos cambios. No necesitaremos editar el código que funciona con estas abstracciones si queremos cambiar la implementación de las abstracciones mismas. Esto es muy importante porque ahorra dinero al cliente y aumenta las bonificaciones para desarrolladores.

Por cierto, digamos que no sabemos acerca de la existencia de Mapov en js, pero podemos trabajar con objetos.

//       
//   
const pair = (first, second) => (
  Object.freeze({
    'first': first,
    'second': second,
  })
);
//    ,       
const getFirst = (pair) => (pair.first);
const getSecond = (pair) => (pair.second);

Como puede adivinar fácilmente, ambas implementaciones anteriores también pueden reemplazarse por una tercera y nada cambiará de esto (en realidad, hay alguna diferencia. Los mapas no arrojan excepciones cuando intentan cambiar propiedades directamente, pero arrojan "objetos congelados" TypeError: Cannot assign to read only property. Sin embargo, en el contexto de este artículo importante ).

¿Por qué necesitamos conocimiento de las estructuras de datos teóricos?


Si observamos el trabajo de un programador a vista de pájaro, veremos que, en esencia, él está involucrado en eso. que crea herramientas que manipulan ciertos conjuntos de datos. Por lo tanto, constantemente tenemos que seleccionar algún tipo de estructura de almacenamiento para los datos y encontrar formas de trabajar con ellos o dar al conjunto de datos caótico una estructura específica. Comprender las estructuras de datos típicas nos permite tener un conjunto de soluciones preparadas para diferentes tareas típicas y simplemente elegir la forma más conveniente para una tarea específica.

Analicemos un ejemplo:

“Implemente un mecanismo para trabajar con fracciones. Las fracciones deben almacenarse en el formato habitual. aquellos. en forma de 1/2. También es necesario implementar operaciones básicas (suma, resta, multiplicación, división). La normalización debe ser proporcionada ".

¡Vamos a darle sentido a la condición del problema! Necesitamos implementar una abstracción para alguna entidad matemática, por lo tanto, es razonable referirse a la fuente original . Por supuesto, si los programadores experimentados leen este texto, deben haber imaginado inmediatamente la solución en código sin leer el artículo sobre fracciones en matemáticas, pero asumiremos. que imaginamos débilmente cómo funcionan las fracciones e ilustramos todo el curso del razonamiento.

El material de Wikipedia nos dice que las fracciones son ordinarias (1/4) y decimales (0.1). Obviamente, por la condición del problema, necesitamos trabajar con fracciones en el formato de presentación habitual.

También vemos que la fracción ordinaria es una unión lógica de dosnúmeros (numerador y denominador), esta es la señal más segura sobre la necesidad de utilizar un par como estructura de datos para esta tarea.

En el código, podríamos describir esta estructura de la siguiente manera:

//        ,
//  ,    
//          ,
//     
const makeRational = (num, denom) => (
  new Map([
    ['num', num],
    ['denom', denom],
  ])
);

//  ,    
//   ,      ,    
//    
const getNumer = (rat) => (rat.get('num'));
const getDenom = (rat) => (rat.get('denom'));

Luego, debemos analizar la situación con fracciones de dicho plan 8/4, 6/9. La condición de la tarea dice acerca de la necesidad de prever la normalización. La normalización de una fracción es la eliminación del divisor común más grande (MCD).

Implementamos una función para buscar dos números GCD:

//Gcd     
const getGcd = (a, b) => ((a % b) ? getGcd(b, a % b) : Math.abs(b));

Si el texto de la función no le resulta claro, le aconsejo que lea sobre la recursividad .

Para normalizar la fracción, necesitamos dividir su numerador y denominador por MCD. Escribimos la función de normalización:

const normalize = (n1, n2) => {
  const commonDivisor = getGcd(n1, n2);
  return [n1 / commonDivisor, n2 / commonDivisor];
};

Sería lógico poner la normalización en el constructor makeRational para normalizar siempre los datos al crear la fracción.

const makeRational = (num, denom) => {
  const [normalizeNum, normalizeDenom] = normalize(num, denom);
  return new Map([
    ['num', normalizeNum],
    ['denom', normalizeDenom],
  ]);
};

A continuación, trabajaremos en la interfaz de operaciones.

1) Suma

Para agregar dos fracciones ordinarias, debes llevarlas a un denominador común. Luego agregue los numeradores y deje el denominador sin cambios.

//..       ,
//       ,
//         
const add = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2) + getNumer(rat2) * getDenom(rat1),
    getDenom(rat1) * getDenom(rat2),
  ));

2) Resta

Para obtener la diferencia de fracciones, también deben reducirse a un denominador común, y luego restar los numeradores, el denominador no debe modificarse.

//    
//     
const sub = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2) - getNumer(rat2) * getDenom(rat1),
    getDenom(rat1) * getDenom(rat2),
  ));

3) Multiplicación

Para multiplicar dos fracciones ordinarias, debes multiplicar sus numeradores y denominadores.

const multi = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getNumer(rat2),
    getDenom(rat1) * getDenom(rat2),
  ));


4) División

Para dividir una fracción ordinaria en otra, debes multiplicar la primera fracción por la fracción inversa de la segunda. El inverso se llama una fracción cuyo numerador es igual al denominador del original, y el denominador es el numerador del original.

const div = (rat1, rat2) => (
  makeRational(
    getNumer(rat1) * getDenom(rat2),
    getDenom(rat1) * getNumer(rat2),
  ));

Como puede ver, en cada operación creamos una nueva entidad. Esta es una consecuencia de la regla de inmutabilidad. Tenga en cuenta que en matemáticas operaciones no cambian el número original de los números, y crear nuevas: 1 + 2 = 3.

Debería prestar atención. que podemos cambiar la implementación makeRationala cualquier otra, pero el código de llamada no lo sabrá y continuará funcionando. Esto es una consecuencia del hecho de que implementamos correctamente la abstracción y trabajamos con sus componentes a través de la interfaz, y no directamente.

También es importante que el usuario pueda obtener sus datos de la manera habitual, por lo que presentamos una función adicional.

const ratToString = (rat) => `${getNumer(rat)}/${getDenom(rat)}`;

Adjuntaré una lista de pruebas:

describe('normalize', () => {
  test('should work', () => {
    expect(normalize(21, 6)).toEqual([7, 2]);
    expect(normalize(2, 3)).toEqual([2, 3]);
  });
});


describe('rational', () => {
  test('getters', () => {
    const rat1 = makeRational(3, 9);
    expect(getNumer(rat1)).toBe(1);
    expect(getDenom(rat1)).toBe(3);

    const rat3 = makeRational(-4, 16);
    expect(getNumer(rat3)).toBe(-1);
    expect(getDenom(rat3)).toBe(4);
  });

  test('add&sub', () => {
    const rat1 = makeRational(3, 9);
    const rat2 = makeRational(10, 3);
    expect(add(rat1, rat2)).toEqual(makeRational(11, 3));
    expect(sub(rat1, rat2)).toEqual(makeRational(-3, 1));

    const rat4 = makeRational(12, 5);
    const rat3 = makeRational(-4, 16);
    expect(add(rat3, rat4)).toEqual(makeRational(43, 20));
    expect(sub(rat3, rat4)).toEqual(makeRational(-53, 20));

    const rat5 = makeRational(1, 15);
    const rat6 = makeRational(4, 25);
    expect(add(rat5, rat6)).toEqual(makeRational(17, 75));
    expect(sub(rat5, rat6)).toEqual(makeRational(-7, 75));
  });

  test('multi&div', () => {
    const rat1 = makeRational(1, 2);
    const rat2 = makeRational(2, 3);
    expect(multi(rat1, rat2)).toEqual(makeRational(2, 6));

    const rat3 = makeRational(1, 3);
    const rat4 = makeRational(1, 2);
    expect(div(rat3, rat4)).toEqual(makeRational(2, 3));
  });

  test('ratToString', () => {
    const rat1 = makeRational(3, 9);
    const rat3 = makeRational(-4, 16);
    expect(ratToString(rat1)).toBe('1/3');
    expect(ratToString(rat3)).toBe('-1/4');
  });
});

Conclusión


El texto resultó ser bastante voluminoso, pero espero que sea claro. Para resumir:

  1. Podemos usar la estructura de datos de steam siempre que necesitemos combinar lógicamente dos valores no relacionados
  2. La pareja debe ser inmune.
  3. La pareja debe proporcionar una interfaz para trabajar con sus componentes.

Pido disculpas de antemano si el artículo parece sobrecargado a alguien. Me concentro principalmente en desarrolladores poco experimentados, así que trato de escribir de tal manera que la lógica de mi razonamiento y la elección de esta o aquella solución sean extremadamente claras.

Si el artículo es de interés para la comunidad. entonces la dirección de las estructuras de datos típicas continuará. El siguiente tema son las listas relacionadas.

All Articles