Есть ли более элегантный способ решить эту проблему с фильтром Ramda? - PullRequest
0 голосов
/ 08 января 2020
var a = { id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' };
var b = { id: 2, name: 'name2', type: 'type1', tag: 'tag2' };
var c = { id: 3, name: 'name3', type: 'type1', tag: 'tag1' };
var d = { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' };
var e = { id: 5, name: 'name4', type: 'type2', tag: 'tag4' };
var f = { id: 6, name: 'name5', type: 'type2', tag: 'tag3' };

var arr = [a, b, c, d, e, f];

var typeFind = R.curry((type, ex) => R.find(ele => ele.type === type, ex));
var tagEqProps = R.curry((o1, o2) => R.eqProps('tag', o1, o2));
var filtered = R.curry((arr, ex) => R.filter(ele => tagEqProps(ele, typeFind(ele.type, ex)), arr));

var excludes = R.filter(ele => ele.name === 'filter-name', arr);
filtered(arr, excludes);

Мне нужно отфильтровать:

  1. объекты, имя которых равно 'имя фильтра'
  2. объекты, свойство тега которых равно объектам 'имя фильтра' в каждый тип

в данном случае результат равен 1, 3, 4, 6.

Есть ли более элегантный способ решения этой проблемы?

Ответы [ 2 ]

0 голосов
/ 10 января 2020

Ниже приведен ряд версий, которые показывают, как можно сделать бессмысленную версию этой функции с помощью некоторых пользовательских комбинаторов. В конце, однако, я полагаю, что это совсем не стоит того, чтобы другая версия с промежуточной переменной - написанная с Рамдой или без нее - была значительно более читабельной.

Первый проход

Совершенно очевидно, что есть два различных шага к проблеме: во-первых, поиск тегов, которые соответствуют filter-name, и во-вторых, поиск элементов, которые имеют эти имена тегов. Давайте напишем функции для них:

const tagNames = (filterName) => (arr) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag')) (arr)

const getItems= (propNames) => (arr) => filter (anyPass (map (propEq ('tag'), propNames))) (arr)

const matchTags = (filterName) => (arr) => 
  getItems (tagNames (filterName) (arr)) (arr)

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

Это прямое использование соответствующих функций Рамды . Мы объединяем две функции в matchTags относительно простым способом. Прохождение arr в двух разных точках немного удивительно, но это не должно быть слишком удивительно, учитывая требования.

Перемещение в сторону без точек

Когда последний аргумент каррирования используется только как При вызове базовой функции и возвращаемого результата мы можем просто удалить параметр и этот вызов и получить эквивалентную функцию. Другими словами, (arg) => foo(arg) эквивалентно foo для любой унарной функции, foo. Это касается как tagNames, так и getItems с параметром arr. Таким образом, мы можем упростить приведенное выше, например:

const tagNames = (filterName) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag'))

const getItems = (propNames) => filter (anyPass (map (propEq ('tag'), propNames)))

И затем мы можем go сделать еще один шаг, удалив propNames из getItems:

const getItems = pipe (map (propEq ('tag')), anyPass, filter)

. Вы можете проверить что мы ничего не сломали в следующем фрагменте:

const tagNames = (filterName) => 
  pipe (filter (propEq ('name', filterName)), pluck ('tag'))

const getItems = pipe (map (propEq ('tag')), anyPass, filter)

const matchTags = (filterName) => (arr) => 
  getItems (tagNames (filterName) (arr)) (arr)

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

Добавление комбинаторов

Мы застряли для простого способа сделать matchTags или tagNames бессмысленными. Они используют свои аргументы несколько раз или в позициях, отличных от самого конца. Конечно, мы могли бы оставить это так, но мы могли бы заметить, что в них есть шаблоны, которые мы могли бы использовать повторно. Один из способов сделать это - использовать Combinators . Комбинаторы объединяют простые функции в более сложные, и многие важные функции Ramda действуют как комбинаторы, включая все функции Ramda, упомянутые в этом списке, и другие, такие как converge и useWith.

Давайте посмотрим на структуру из matchTags, называя части, которые мы видим:

const matchTags = (filterName) => (arr) => 
//                 `----x----'    `-y-' 
  getItems (tagNames (filterName) (arr)) (arr)
//`---f--'  `---g--' `-----x----' `-y-'  `-y-'

Мы можем написать очень простую функцию, которая фиксирует эту структуру и использует ее для построения matchTags из более простых частей. Существует сильная традиция использовать отдельные заглавные буквы для именования таких комбинаторов или использовать имена определенных птиц. Мы не хотим повторно использовать какие-либо из них или делать какое-либо реальное наводящее на размышления имя для функции, в которой мы не уверены, что это обычно полезно. А пока давайте просто вызовем эту функцию Z1. Мы можем использовать это следующим образом:

const Z1 = (f) => (g) => (x) => (y) =>
  f (g (x) (y)) (y)

const matchTags = Z1 (getItems) (tagNames)

Мы не убрали никакой сложности - мы просто пошутили вокруг. Мы позволили Z1 обработать сложность объединения функций и использовали его для упрощения matchTags. Это могло бы стоить это само по себе. Но если мы решим, что Z1 является достаточно важной функцией, и мы хотим использовать ее повторно и, возможно, дать ей более описательное имя, то это определенно стоит.

Мы могли бы сделать то же самое с небольшим переписыванием из tagNames:

const tagNames = (filterName) => (arr) =>
//                `----x---'     `-y-'
  pluck('tag') ((pipe (propEq ('name'), filter)) (filterName) (arr))
//`----f-----'  `--------------g--------------'   `----x---'  `-y-'

Теперь мы можем добавить Z2, например так:

const Z2 = (f) => (g) => (x) => (y) =>
  f (g (x) (y))

const tagNames = Z2 (pluck('tag')) (pipe (propEq ('name'), filter))

Z2 выглядит более знакомым. Есть хороший шанс, что это существующий комбинатор с именем. Если мы решим, мы можем попытаться найти, какое имя может быть связано с этим. Это оставлено в качестве упражнения для читателя.

Комбинируя все это и вставляя вспомогательные функции, мы можем переписать так:

const matchTags = Z1
  (pipe (map (propEq ('tag')), anyPass, filter))
  (Z2 (pluck('tag')) (pipe (propEq ('name'), filter)))

Это все еще работает, как показано в следующем фрагменте:

const Z1 = (f) => (g) => (x) => (y) =>
  f (g (x) (y)) (y)

const Z2 = (f) => (g) => (x) => (y) =>
  f (g (x) (y))

const matchTags = Z1
  (pipe (map (propEq ('tag')), anyPass, filter))
  (Z2 (pluck('tag')) (pipe (propEq ('name'), filter)))

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]

console .log (matchTags ('filter-name') (arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {pipe, filter, propEq, pluck, anyPass, map} = R        </script>

Это стоит того?

Мы создали два новых потенциально многоразовых комбинатора, и мы реорганизовали наш основной код, чтобы использовать их и теперь это бессмысленно. Итак, вопрос в том, стоит ли это?

Мой ответ? Нет! Это ужасный код!

Несмотря на то, что мы могли бы найти другие применения для этих комбинаторов, если мы не будем широко их использовать, мы не вспомним, что делают Z1 и Z2. Возможно, мы найдем больше описательных имен, и, возможно, это поможет. Но есть большая вероятность, что это всегда останется загадкой.

Если бы требования были настолько загадочными, что любой код был бы сложным, то это могло бы сработать. Но мы можем добиться большего.

Упрощенная реализация

Запись двух шагов, необходимых явно, с локальной переменной между ними делает код намного проще для чтения:

const matchTags = (filterName, arr) => {
  const tags = pluck ('tag', filter (propEq ('name', filterName), arr))
  return filter (propSatisfies (includes(__, tags), 'tag'), arr)
}

const arr = [{ id: 1, name: 'filter-name', type: 'type1', tag: 'tag1' }, { id: 2, name: 'name2', type: 'type1', tag: 'tag2' }, { id: 3, name: 'name3', type: 'type1', tag: 'tag1' }, { id: 4, name: 'filter-name', type: 'type2', tag: 'tag3' }, { id: 5, name: 'name4', type: 'type2', tag: 'tag4' }, { id: 6, name: 'name5', type: 'type2', tag: 'tag3' }]


console .log (matchTags ('filter-name', arr))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script>const {__, pluck, filter, propEq, propSatisfies, includes} = R  </script>

Обратите внимание на переменную tags. Это хранит, в нашем случае, ['tag1', 'tag3']. Использование этого делает код более похожим на наш начальный этап, где функциональность была разделена по этапам процесса.

Для меня это чистая и понятная реализация. Это не единственный, конечно. И я бы никогда не привел Рамду только для того, чтобы создать подобную функцию, поскольку простая JS версия столь же чиста:

const matchTags = (filterName, arr) => {
  const tags = arr
    .filter (({name}) => name == filterName)
    .map (({tag}) => tag)
  return arr .filter (({tag}) => tags .includes (tag))
}

Между ними я не уверен, предпочитаю ли я версию Ramda или ванильный JS один, но, на мой взгляд, любой мог бы это сделать.

Уроки

Многие новые пользователи Ramda, похоже, думают, что бессмысленное получение само по себе является важной целью.

Я бы предложил вместо этого просто инструмент в нашем наборе инструментов. Мы должны использовать его только до тех пор, пока он облегчает чтение и запись нашего кода. Когда это запутывает вещи, мы должны избегать этого полностью.

Это урок, который я должен выучить снова и снова. Приведенная выше цепочка реализаций показала, как я пытался решить эту проблему. Несмотря на то, что я проповедую эту идею «бессмысленно не для всего», я все равно слишком часто возвращаюсь к ней.

0 голосов
/ 08 января 2020

Используйте R.filter с R.propEq , чтобы получить все элементы с именем, равным filter-name, и сорвать теги. Затем используйте R.innerJoin со списком тегов, которые вы взяли, чтобы получить все элементы с соответствующими тегами:

const { pipe, filter, propEq, innerJoin, pluck } = R;

const getFiltered = pipe(
  filter(propEq('name', 'filter-name')),
  pluck('tag')
);

const getByTags = arr => innerJoin(
  (record, tag) => record.tag === tag,
  arr,
  getFiltered(arr)
);

const arr = [{"id":1,"name":"filter-name","type":"type1","tag":"tag1"},{"id":3,"name":"name3","type":"type1","tag":"tag1"},{"id":4,"name":"filter-name","type":"type2","tag":"tag3"},{"id":6,"name":"name5","type":"type2","tag":"tag3"}];

const result = getByTags(arr);

console.log(result);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
...