Рамда труба условно? - PullRequest
       7

Рамда труба условно?

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

У меня есть функция фильтра, которая имеет несколько переменных, определяющих логи фильтрации c. Если переменная определена, я хочу фильтровать, если нет, я не хочу фильтровать (т.е. выполнять функцию в конвейере). В более общем смысле, имея предикат, который я могу проверить по каждому аргументу в конвейере, чтобы определить, должен ли я вызывать его или просто перейти к следующей функции.

Я делаю это, чтобы предотвратить сложные логики ветвления c, но, будучи довольно новичком в функциональном программировании, я подумал, что это будет лучший способ рефакторинга.

Например:

resources = R.pipe(
   filterWithRadius(lat, lng, radius), // if any of these arguments are nil, act like R.identity
   filterWithOptions(filterOptions)(keyword), // if either filterOptions or keyword is nil, act like R.identity
   filterWithOptions(tagOptions)(tag) // same as above.
)(resources);

Я пытался использовать R.unless / R.when, но, похоже, он не работает с функциями более чем одного аргумента. R.pipeWith было бы здесь полезно, если бы он имел дело с аргументами функции.

В качестве примера реализации:

const filterWithRadius = R.curry((lat, long, radius, resources) =>
  R.pipe(
    filterByDistance(lat, long, radius), // simply filters down a geographic location, will fail if any of lat/long/radius are not defined
    R.map(addDistanceToObject(lat, long)), // adds distance to the lat and long to prop distanceFromCenter
    R.sortBy(R.prop("distanceFromCenter")) // sorts by distance
  )(resources)
);

resources - это массив этих объектов ресурсов. По сути, каждая из функций filterRadius и filterOptions - это чистые функции, ожидающие массив ресурсов и допустимые аргументы (не определены) и выводящие новый отфильтрованный список. Таким образом, цель здесь состоит в том, чтобы каким-то образом составить (или реорганизовать) такой код, чтобы, если все параметры были неопределенными, он выполнял функцию, иначе просто действовал бы как идентификатор.

Есть ли более чистый / лучший способ, чем этот?

resources = R.pipe(
   lat && lng && radius
     ? filterWithRadius(lat, lng, radius)
     : R.identity,
   keyword ? filterWithOptions(filterOptions)(keyword) : R.identity,
   tag ? filterWithOptions(tagOptions)(tag) : R.identity
)(resources);

Ответы [ 4 ]

1 голос
/ 12 января 2020

Я просто хочу указать на этот анти-паттерн:

// inline use of R.pipe
someVar = R.pipe(...)(someVar)

Это не только мутация someVar, которая противоречит основному принципу функционального программирования, но и неправильное использование R.pipe, которая предназначена для создания новой функции , такой как -

const someProcess = R.pipe(...)

const someNewVar = someProcess(someVar)

Я понимаю, что вы используете R.pipe для того, чтобы код читался лучше и вытекал из сверху вниз, но указанное вами c использование контрпродуктивно. Нет смысла создавать промежуточную функцию, если вы собираетесь сразу же ее утилизировать.

Подумайте о том, чтобы представить свои намерения более простым способом -

const output =
  $ ( input                                      // starting with input,
    , filterWithRadius (lat, lng, radius)        // filterWithRadius then,
    , filterWithOptions (filterOptions, keyword) // filterWithOptions then,
    , filterWithOptions (tagOptions, tag)        // filterWithOptions then,
    , // ...                                     // ...
    )

Так же, как плотник изготавливает шаблоны и шаблоны, указывающие c на его / ее проект, задача программиста - изобрести любую утилиту, которая облегчит его / ее работу. Все, что вам нужно, чтобы сделать это возможным, это умный $. Вот полный пример -

const $ = (input, ...operations) =>
  operations .reduce (R.applyTo, input)
  
const add1 = x =>
  x + 1
  
const square = x =>
  x * x
 
const result =
  $ ( 10     // input of 10
    , add1   // 10 + 1 = 11
    , add1   // 11 + 1 = 12
    , square // 12 * 12 = 144
    )
    
console .log (result) // 144
<script src="https://unpkg.com/ramda@0.26.1/dist/ramda.min.js"></script>

Ваша программа не ограничена тремя (3) операциями. Мы можем без беспокойства связывать тысячи -

$ (2, square, square, square, square, square)
// => 4294967296

Что касается того, чтобы заставить функции вести себя как R.identity, когда определенные аргументы равны нулю (undefined), я бы предложил использовать аргументы по умолчанию в качестве лучшей практики -

const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
  // ...

Теперь, если lat и lng не определены, вместо них будет предоставлено 0, которое является допустимым местоположением, известным как Prime Meridian . Однако поиск radius из 0 не должен давать результатов. Таким образом, мы можем легко завершить sh нашу функцию -

const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
  radius <= 0 // if radius is less than or equal to 0,
    ? input   // return the input, unmodified
    : ...     // otherwise perform the filter using lat, lng, and radius

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


Я вижу, что вы использовали встроенный R.pipe анти-шаблон в вашей функции filterWithRadius. Мы могли бы использовать $, чтобы помочь нам здесь снова -

const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
  radius <= 0
    ? input
    : $ ( input
        , filterByDistance (lat, lng, radius)
        , map (addDistanceToObject (lat, lng))
        , sortBy (prop ("distanceFromCenter"))
        )

Надеюсь, это откроет вам глаза на некоторые из возможностей, которые вам доступны.

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

Есть ли более чистый / лучший способ, чем этот?

R.pipe(
   lat && lng && radius
     ? filterWithRadius(lat, lng, radius)
     : R.identity,
   keyword ? filterWithOptions(filterOptions)(keyword) : R.identity,
   tag ? filterWithOptions(tagOptions)(tag) : R.identity
)

Похоже, что каждая функция может быть применена, если вызов их аргументов не равен нулю. Учитывая это, функции фильтра могут решать, применять ли фильтрацию или нет.

Этот шаблон называется call guard, где в основном первые инструкции тела функции используются для защиты функции приложение из любого непригодного значения.

const filterWithRadius = (lat, lng, radius) => {
  if (!lat || !lng || !radius) {
    return R.identity;
  }
  
  return R.filter((item) => 'doSomething');
}


const foo = R.pipe(
  filterWithRadius(5, 1, 60),
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js" integrity="sha256-43x9r7YRdZpZqTjDT5E0Vfrxn1ajIZLyYWtfAXsargA=" crossorigin="anonymous"></script>

Вы можете сделать более интенсивное использование рамды следующим образом:

const filterWithFoo = R.unless(
  (a, b, c) => R.isNil(a) || R.isNil(b) || R.isNil(c),
  R.filter(...)
);
0 голосов
/ 10 января 2020

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

Но, как указал Ори Дрори, вы можете написать декоратор функций, чтобы это произошло.

Вот одно из предложений:

// Dummy implementations
const filterWithRadius = (lat, lng, radius, resources) =>
  ({...resources, radiusFilter: `${lat}-${lng}-${radius}`})
const filterWithOptions = (opts, val, resources) => 
  ({...resources, [`optsFilter-${opts}`]: val})

// Test function (to be used in pipelines, but more general)
const ifNonNil = (fn) => (...args) => any(isNil, args) 
  ? identity 
  : (data) => fn (...[...args, data])
  // alternately, for variadic result : (...newArgs) => fn (...[...args, ...newArgs])

// Pipeline call
const getUpdatedResources = (
  {lat, lng, radius, filterOptions, keyword, tagOptions, tag}
) => pipe (
  ifNonNil (filterWithRadius) (lat, lng, radius),
  ifNonNil (filterWithOptions) (filterOptions, keyword),
  ifNonNil (filterWithOptions) (tagOptions, tag)
)

// Test data
const resources = {foo: 'bar'}

const query1 = {
   lat: 48.8584, lng: 2.2945, radius: 10, 
   filterOptions: 'baz', keyword: 'qux',
   tagOptions: 'grault', tag: 'corge'
}

const query2 = {
   lat: 48.8584, lng: 2.2945, radius: 10, 
   tagOptions: 'grault', tag: 'corge'
}

const query3 = {
   lat: 48.8584, lng: 2.2945, radius: 10, 
   filterOptions: 'baz', keyword: 'qux',
}

const query4 = {
   filterOptions: 'baz', keyword: 'qux',
   tagOptions: 'grault', tag: 'corge'
}

const query5 = {
   lat: 48.8584/*, lng: 2.2945*/, radius: 10, 
   filterOptions: 'baz', keyword: 'qux',
   tagOptions: 'grault', tag: 'corge'
}

const query6 = {}

// Demo
console .log (getUpdatedResources (query1) (resources))
console .log (getUpdatedResources (query2) (resources))
console .log (getUpdatedResources (query3) (resources))
console .log (getUpdatedResources (query4) (resources))
console .log (getUpdatedResources (query5) (resources))
console .log (getUpdatedResources (query6) (resources))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script>
<script> const {any, isNil, pipe, identity} = R </script>

Мы начнем с фиктивных реализаций ваших filter* функций, которые просто добавляют свойство во входной объект.

Важная функция здесь ifNotNil. Он принимает функцию n аргументов, возвращая функцию n - 1 аргументов, которая при вызове проверяет, является ли какой-либо из этих аргументов nil. Если они есть, он возвращает функцию идентификации; в противном случае он возвращает функцию с одним аргументом, которая, в свою очередь, вызывает исходную функцию с аргументами n - 1 и этот последний.

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

В примерах показаны различные комбинации параметров, включенных и исключенных.

Это делает предположение, что ваши функции не каррированы, что, скажем, filterWithRadius выглядит как (lat, lng, radius, resources) => ... Если они каррированы мы могли бы написать это вместо этого:

const ifNonNil = (fn) => (...args) => any(isNil, args) 
  ? identity 
  : reduce ((f, arg) => f(arg), fn, args)

используется с

const filterWithRadius = (lat) => (lng) => (radius) => (resources) => 
  ({...resources, radiusFilter: `${lat}-${lng}-${radius}`})

, но все еще вызывается в конвейере как

pipe (
  ifNonNil (filterWithRadius) (lat, lng, radius),
  // ...
)

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

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

Чтобы это решение работало, все ваши функции должны быть каррированы, с resources в качестве конечного параметра.

Это создание функции (passIfNil), которая принимает функцию (fn) и параметры (в правильном порядке для fn). Если какой-либо из этих параметров равен нулю, возвращается R.identity. Если нет, то возвращается оригинальный fn, но с примененным к нему args.

Пример (не тестировался):

const passIfNil = (fn, ...args) => R.ifElse(
  R.any(R.isNil),
  R.always(R.identity),
  R.always(fn(...args))
);

resources = R.pipe(
   passIfNil(filterWithRadius, lat, lng, radius), // if any of these arguments are nil, act like R.identity
   passIfNil(filterWithOptions, filterOptions, keyword), // if either filterOptions or keyword is nil, act like R.identity
   passIfNil(filterWithOptions, tagOptions, tag) // same as above.
)(resources);
...