Рамда: Как минимизировать вычислительные ресурсы с помощью каррирования, фильтрации и сопоставления?

Я создаю приложение React с помощью Ramda. Я все еще новичок в функциональном программировании (~ два месяца назад).

У меня есть такой список контактов:

const contacts = [
  {
    id: 1,
    firstName: 'Sven',
    lastName: 'Hillstedt',
    city: 'Aachen',
    company: '',
    position: 'Student',
    group: 'friends',
    tendency: 'maintain'
  },
  {
    id: 2,
    firstName: 'David',
  // ...
];

Учитывая строку, мне нужно отфильтровать этот (очень длинный, 10.000-100.000) список. Но мне нужно учитывать только ключи firstName, lastName, city, company и position. Есть массив, содержащий это:

const FIRST_NAME = 'firstName';
const LAST_NAME = 'lastName';
const CITY = 'city';
const COMPANY = 'company';
const POSITION = 'position';

export const stringFields = [FIRST_NAME, LAST_NAME, CITY, COMPANY, POSITION];

Теперь, используя Ramda, я написал следующие функции, которые принимают string и список контактов, сопоставляют клавиши контактов, выбирая соответствующие и уменьшая их, а затем возвращают отфильтрованные контакты:

import { any, filter, includes, map, pick, pipe, toLower, values } from 'ramda';

const contactIncludesValue = value =>
  pipe(
    pick(stringFields),
    map(toLower),
    values,
    any(includes(value))
  );

const filterContactsByValue = value => filter(contactIncludesValue(value));

Как видите, этот код запутан (даже если подумать, что он намного красивее, чем делать это по необходимости). Я карри value => много раз, что кажется неоптимальным. Я также сомневаюсь, что этот код выполняет итерацию по контактам только один раз и эффективен ли он.

Как бы вы отфильтровали и сопоставили (выберите только соответствующие ключи + lowerCase) большой список контактов, не повторяя его дважды или более? Есть ли способ избежать каррирования и написать этот очиститель?


person J. Hesters    schedule 11.03.2019    source источник
comment
Каждая оптимизация производительности должна начинаться с теста производительности и / или профилирования. Пожалуйста, предоставьте результаты тех.   -  person zerkms    schedule 12.03.2019
comment
@zerkms В зависимости от обстоятельств, в браузере на моем MacBook Pro это едва заметно, но на телефоне с Android вы можете увидеть круг, если перебирать список несколько раз.   -  person J. Hesters    schedule 12.03.2019
comment
Если у вас нет базовых показателей - вы не можете доказать, что улучшенное решение действительно что-то улучшило. Если вам нравится тратить время на случайные вещи и надеяться, что это что-то волшебным образом улучшит - хорошо, получайте удовольствие :-)   -  person zerkms    schedule 12.03.2019
comment
Например, в вашем случае велика вероятность, что проблема на самом деле не в многократном повторении, а в выделении тонны временных массивов и линейном поиске по ним. Но послушайте, пожалуйста, увеличьте количество петель (потому что вы догадались, верно?)   -  person zerkms    schedule 12.03.2019


Ответы (3)


Здесь есть несколько вопросов, на которые нужно ответить.

  • Даже если комментарии были немного язвительными, @zerkms прав. Нет смысла пробовать оптимизацию производительности, если вы не знаете, что код на самом деле имеет низкую производительность, особенно если это затрудняет написание или поддержку кода.

  • Вы не карри value => несколько раз. Он карризован только впереди, а частичное применение вашего значения происходит один раз при фильтрации списка.

  • Вы перебираете свои контакты только один раз. Но внутри каждого есть вызов any над вашим списком полей. Он выполняет возврат раньше, если находит совпадение, поэтому подсчитать количество вызовов нетривиально, но, вероятно, O(m * n), где m - количество полей, а n - количество контактов.

Эта версия вашего кода немного более сжатая. Вы можете найти или не найти его более читаемым:

const contactIncludesValue = value =>
  pipe(
    props(stringFields),
    map(toLower),
    any(includes(value))
  );

const filterContactsByValue = pipe(contactIncludesValue, filter);

Обратите внимание, что props удобнее, чем pick(...) -> values, и промежуточный map(toLower) работает после него так же хорошо.

person Scott Sauyet    schedule 12.03.2019
comment
Возможно, map(toLower) можно было бы удалить, если бы any(includes(value)) вместо этого прочитал any(eqBy(toLower, value)). Это будет означать две вещи: 1) вам не нужно заранее записывать все в нижнем регистре (очень небольшое улучшение) и 2) value не обязательно должен быть в нижнем регистре. - person customcommander; 12.03.2019

Как бы вы отфильтровали и сопоставили (выберите только соответствующие клавиши + нижний регистр) большой список контактов, не повторяя его дважды или больше? Есть ли способ избежать каррирования и написать этот очиститель?

Если вам нужно отфильтровать И преобразовать данные за один раз, я не понимаю, как это можно сделать, используя только filter.

Например, это не будет сохранять a и преобразовывать его:

const list = [
  {a: 'foo'},
  {b: 'bar'}
];

console.log(

  filter(pipe(map(toUpper), has('a')), list)

);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {filter, pipe, map, toUpper, has} = R;</script>

Для этого вам нужно использовать reduce или преобразователь.

Вот решение с использованием преобразователя. В этом примере:

  1. Работайте только с объектами, у которых свойство a равно 1.
  2. На свойстве b добавьте 10
  3. Затем выберите b

const list = [
  {a: 1, b: 2},
  {a: 2, b: 20},
  {a: 1, b: 3},
  {a: 2, b: 30},
  {a: 1, b: 4},
  {a: 2, b: 40},
];

console.log(

into([],
  compose(
    filter(propEq('a', 1)),
    map(over(lensProp('b'), add(10))),
    map(pick(['b']))
  ),
  list)
  
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {into, compose, filter, propEq, map, pick, over, lensProp, add} = R;</script>

Преимущество использования преобразователей заключается в том, что они отделяют логику получения результата (массива) от логики преобразования данных.

into([]) сообщает Ramda, что вы создаете массив, и поэтому все, что выходит из вашей compose цепочки, необходимо добавить к нему.

into('') сообщает Рамде, что вы производите строку. Ваша цепочка compose должна возвращать только строку. into позаботится о том, чтобы объединить его в окончательный результат:

const list = [
  {a: 1, b: 2},
  {a: 2, b: 20},
  {a: 1, b: 3},
  {a: 2, b: 30},
  {a: 1, b: 4},
  {a: 2, b: 40},
];

console.log(

into('',
  compose(
    filter(propEq('a', 1)),
    map(over(lensProp('b'), add(10))),
    map(prop('b'))
  ),
  list)
  
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {into, compose, filter, propEq, map, over, lensProp, add, prop} = R;</script>

person customcommander    schedule 12.03.2019

R.innerJoin, несомненно, представляет собой наиболее краткий способ его написания, но я не уверен в его временной сложности.

const filter = value => R.innerJoin(
  // you may lowercase, etc... here
  (record, prop) => R.propEq(prop, value, record),
  R.__,
  ['firstName', 'lastName', 'city', 'company', 'position'],
);

const onlySven = filter('Sven');
const onlyGiuseppe = filter('Giuseppe');

const data = [
  {
    id: 1,
    firstName: 'Sven',
    lastName: 'Hillstedt',
    city: 'Aachen',
    company: '',
    position: 'Student',
    group: 'friends',
    tendency: 'maintain'
  },
  // ...
];

console.log('Giuseppe', onlyGiuseppe(data));
console.log('Sven', onlySven(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>

person Hitmands    schedule 21.03.2019