Estruturas teóricas de dados e sua aplicação em JavaScript. P1 Casais

“Os programadores ruins pensam em código. Bons programadores pensam sobre estruturas de dados e seus relacionamentos ”, Linus Torvalds, criador do Linux.
Tomamos como axioma que muitas vezes a solução para qualquer problema de programação se resume a escolher a estrutura de dados correta. Esse axioma pode ser provado, mas faz muito tempo e o artigo é um pouco sobre outro. Hoje falaremos sobre uma das estruturas teóricas mais simples, sobre o par.

Um par é uma estrutura de dados que armazena quaisquer dois elementos de dados em si, o que implica que queremos combiná-los de alguma maneira logicamente em um. Resolve precisamente esse problema, ou seja, se houver alguns elementos de dados e bprecisarmos apresentá-los no formulário b, podemos implementar esse design em pares.

Se você acha que nunca encontrou ou não encontrará algo assim, não é assim.

Aqui está o exemplo mais simples de uma propriedade calculada da documentação do VUE:

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

Do ponto de vista da teoria, fullNameisso é um casal. E existem inúmeros exemplos na tecnologia e no mundo real, que frequentemente modelamos com o código do programa.

Você pode perceber com razão que podemos combinar logicamente qualquer número de elementos e você estará certo. Isso nos leva a um conceito estrutural mais complexo de dados compostos , um dos quais é um casal . Lidaremos com os dados compostos um pouco mais tarde, agora vamos nos concentrar no par.

A teoria nos diz que os pares têm as seguintes propriedades:

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

    :

    pair = { a, b }
    

    Se no código de chamada trabalharemos com o par diretamente nesse estilo: pair.amudando a implementação do par, seremos forçados a reescrever o código de chamada sempre que a chamada para o par aparecer. Isso não é ótimo!

    Um casal é uma abstração (mesmo um nível baixo); portanto, seria errado trabalhar diretamente com seus componentes. Ao trabalhar com os componentes de qualquer abstração, precisamos usar a interface de abstração, caso contrário, o código se tornará um caos (será mais difícil de ler, mais fácil cometer um erro, mas o mais importante é mais difícil de mudar, porque uma alteração terá que ser feita em diferentes partes do código).


Os pares podem ser implementados de maneiras muito diferentes. Em qualquer linguagem de programação, há mais de uma oportunidade para implementar essa estrutura de dados.
Por exemplo, você pode implementar um par para tipos de dados arbitrários:

//       
//      
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'));

Essa maneira de trabalhar é chamada de envio de mensagens. Por estranho que pareça, mas era essa maneira de interação entre entidades que antes era chamada de OOP antes do C / C ++.

Temos que usar o design switch ? Claro, não necessariamente. Estes são os detalhes da implementação técnica, e pode haver muitas implementações!

O mais importante é tornar o casal mutável!

Por exemplo, você pode implementar a mesma funcionalidade usando o Mapa

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

Observe que a primeira implementação pode ser facilmente substituída pela segunda. e o segundo ao primeiro! O código de chamada não será alterado. Essa é uma vantagem importante das abstrações. Podemos alterar facilmente a implementação de uma abstração em um programa, mas o código que trabalha com essa abstração não saberá sobre essas alterações. Não precisaremos editar o código que funciona com essas abstrações se quisermos alterar a implementação das abstrações. Isso é muito importante porque economiza dinheiro do cliente e aumenta os bônus do desenvolvedor.

A propósito, digamos que não sabemos sobre a existência de Mapov em js, mas podemos trabalhar com objetos.

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

Como você pode adivinhar com facilidade, as duas implementações anteriores também podem ser substituídas por uma terceira e nada mudará disso (na verdade, há alguma diferença. Os mapas não lançam exceções ao tentar alterar propriedades diretamente, mas lançam "objetos congelados" TypeError: Cannot assign to read only property. No entanto, no contexto deste artigo importante. ).

Por que precisamos de conhecimento de estruturas de dados teóricos?


Se observarmos o trabalho de um programador do ponto de vista de pássaros, veremos que, em essência, ele está envolvido nisso. que cria ferramentas que manipulam determinados conjuntos de dados. Portanto, precisamos constantemente selecionar algum tipo de estrutura de armazenamento para os dados e criar maneiras de trabalhar com eles ou fornecer ao conjunto de dados caóticos uma estrutura específica. A compreensão de estruturas de dados típicas nos permite possuir um conjunto de soluções prontas para diferentes tarefas típicas e simplesmente escolher a maneira mais conveniente para uma tarefa específica.

Vamos

analisar um exemplo: “Implemente um mecanismo para trabalhar com frações. As frações devem ser armazenadas no formato usual. Essa. sob a forma de 1/2. Também é necessário implementar operações básicas (adição, subtração, multiplicação, divisão). A normalização precisa ser fornecida. ”

Vamos entender a condição do problema! Precisamos implementar uma abstração para alguma entidade matemática, portanto, é razoável fazer referência à fonte original . Obviamente, se programadores experientes leem este texto, devem ter imaginado imediatamente a solução em código sem ler o artigo sobre frações em matemática, mas assumiremos. que imaginamos fracamente como as frações funcionam e ilustramos todo o curso do raciocínio.

O material da Wikipedia nos diz que as frações são comuns (1/4) e decimais (0,1). Obviamente, pela condição do problema, precisamos trabalhar com frações no formato de apresentação usual.

Também vemos que a fração ordinária é uma união lógica de doisnúmeros (numerador e denominador), este é o sinal mais seguro sobre a necessidade de usar um par como estrutura de dados para esta tarefa.

No código, poderíamos descrever essa estrutura da seguinte maneira:

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

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

Em seguida, devemos analisar a situação com frações de tal plano 8/4, 6/9. A condição da tarefa diz sobre a necessidade de fornecer normalização. A normalização de uma fração é a remoção do maior divisor comum (MDC) dele.

Implementamos uma função para procurar dois números da GCD:

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

Se o texto da função não estiver claro, recomendamos que você leia sobre recursão .

Para normalizar a fração, precisamos dividir seu numerador e denominador por GCD. Escrevemos a função de normalização:

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

Seria lógico colocar a normalização no construtor makeRational para normalizar sempre os dados ao criar a fração.

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

Em seguida, trabalharemos na interface de operações.

1) Adição

Para adicionar duas frações comuns, você deve trazê-las para um denominador comum. Em seguida, adicione os numeradores e deixe o denominador inalterado.

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

2) Subtração

Para obter a diferença de frações, elas também precisam ser reduzidas a um denominador comum e, subtraído os numeradores, o denominador deve permanecer inalterado.

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

3) Multiplicação

Para multiplicar duas frações comuns, você precisa multiplicar seus numeradores e denominadores.

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


4) Divisão

Para dividir uma fração comum em outra, é necessário multiplicar a primeira fração pela fração inversa da segunda. O inverso é uma fração cujo numerador é igual ao denominador do original e o denominador é o numerador do original.

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

Como você pode ver, em cada operação, criamos uma nova entidade. Isso é uma consequência da regra de imutabilidade. Note-se que em matemática operações não alterar o número original de números, e criar novo: 1 + 2 = 3.

Deve prestar atenção. que podemos alterar a implementação makeRationalpara outra, mas o código de chamada não saberá sobre isso e continuará funcionando. Isso é uma conseqüência do fato de termos implementado corretamente a abstração e trabalhamos com seus componentes por meio da interface, e não diretamente.

Também é importante que o usuário possa obter seus dados da maneira usual, por isso introduzimos uma função adicional.

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

Anexarei uma lista de testes:

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

Conclusão


O texto acabou sendo bastante volumoso, mas espero que esteja claro. Para resumir:

  1. Podemos usar a estrutura de dados do vapor sempre que precisamos combinar logicamente dois valores não relacionados
  2. O casal deve estar imune
  3. O casal deve fornecer uma interface para trabalhar com seus componentes

Peço desculpas antecipadamente se o artigo parecer sobrecarregado para alguém. Eu me concentro principalmente em desenvolvedores não muito experientes, então tento escrever de tal maneira que a lógica do meu raciocínio e a escolha de uma ou outra solução sejam extremamente claras.

Se o artigo for de interesse da comunidade. então a direção das estruturas de dados típicas continuará. O próximo tópico é uma lista relacionada.

All Articles