Ramda 有条件地管道?
Ramda pipe conditionally?
我有一个过滤器函数,它本质上具有多个确定过滤逻辑的变量。如果定义了变量,我想过滤——如果没有,我不想过滤(即在管道中执行函数)。更一般地说,有一个谓词,我可以检查管道的每个参数以确定我是应该调用它还是只传递给下一个函数。
我这样做是为了防止复杂的分支逻辑,但作为函数式编程的新手,我认为这将是重构的最佳方式。
例如:
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
都是纯函数,需要一组资源和 valid 参数(不是未定义的)并输出一个新的,过滤的列表。所以这里的目标是以某种方式组合(或重构),如果参数都是未定义的,它将 运行 函数,否则只是作为身份。
还有比这更cleaner/better的方法吗?
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);
要使此解决方案起作用,您的所有函数都需要柯里化,并将 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);
我认为你想把这种行为的责任放在错误的地方。如果您希望您的管道函数对某些数据有一种行为,而对其他数据有不同的行为(或者在这种情况下,缺少数据),那么应该由那些单独的函数来处理它,而不是包装它们的管道函数。
但是,正如 Ori Drori 指出的那样,您可以编写一个函数装饰器来实现这一点。
这是一个建议:
// 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
。如果有,则returns恒等函数;否则它 returns 一个参数的函数,它依次调用带有 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),
// ...
)
您甚至可以在同一管道中混合和匹配咖喱化和非咖喱化版本,尽管我希望这会增加混乱。
我只想指出这个反模式:
// inline use of R.pipe
someVar = R.pipe(...)(someVar)
这不仅是 someVar
的变体,它违背了函数式编程的基本原则,而且也是 R.pipe
的误用,旨在创建一个新的 函数,如-
const someProcess = R.pipe(...)
const someNewVar = someProcess(someVar)
我知道您使用 R.pipe
是为了让代码更易读并从上到下流畅,但您的具体使用会适得其反。如果您要立即处理它,则没有理由创建中间函数。
考虑以更直接的方式表达您的意图 -
const output =
$ ( input // starting with input,
, filterWithRadius (lat, lng, radius) // filterWithRadius then,
, filterWithOptions (filterOptions, keyword) // filterWithOptions then,
, filterWithOptions (tagOptions, tag) // filterWithOptions then,
, // ... // ...
)
就像木匠制作特定于 his/her 项目的夹具和模板一样,程序员的工作是发明任何实用程序,使 his/her 的工作更轻松。要做到这一点,您需要的只是一个智能 $
。这是一个完整的例子 -
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
至于当某些参数为 nil (undefined
) 时使函数表现得像 R.identity
,我建议最好使用默认参数 -
const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
// ...
现在,如果 lat
和 lng
未定义,将提供 0
,这是一个有效的位置,称为 Prime Meridian.然而,0
的 radius
搜索应该 return 没有结果。所以我们可以轻松完成我们的功能 -
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
在不引入空检查复杂性的情况下更加健壮。这是一个明显的胜利,因为该函数更加自我记录,在更多情况下产生有效结果,并且不涉及为 "fix" 问题编写 更多 代码。
我看到您也在 filterWithRadius
函数中使用了内联 R.pipe
反模式。我们可以使用 $
在这里再次帮助我们 -
const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
radius <= 0
? input
: $ ( input
, filterByDistance (lat, lng, radius)
, map (addDistanceToObject (lat, lng))
, sortBy (prop ("distanceFromCenter"))
)
我希望这会让您大开眼界,看到一些可用的可能性。
Is there a cleaner/better way than this?
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>
您可以通过以下方式更深入地使用 ramda:
const filterWithFoo = R.unless(
(a, b, c) => R.isNil(a) || R.isNil(b) || R.isNil(c),
R.filter(...)
);
我有一个过滤器函数,它本质上具有多个确定过滤逻辑的变量。如果定义了变量,我想过滤——如果没有,我不想过滤(即在管道中执行函数)。更一般地说,有一个谓词,我可以检查管道的每个参数以确定我是应该调用它还是只传递给下一个函数。
我这样做是为了防止复杂的分支逻辑,但作为函数式编程的新手,我认为这将是重构的最佳方式。
例如:
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
都是纯函数,需要一组资源和 valid 参数(不是未定义的)并输出一个新的,过滤的列表。所以这里的目标是以某种方式组合(或重构),如果参数都是未定义的,它将 运行 函数,否则只是作为身份。
还有比这更cleaner/better的方法吗?
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);
要使此解决方案起作用,您的所有函数都需要柯里化,并将 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);
我认为你想把这种行为的责任放在错误的地方。如果您希望您的管道函数对某些数据有一种行为,而对其他数据有不同的行为(或者在这种情况下,缺少数据),那么应该由那些单独的函数来处理它,而不是包装它们的管道函数。
但是,正如 Ori Drori 指出的那样,您可以编写一个函数装饰器来实现这一点。
这是一个建议:
// 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
。如果有,则returns恒等函数;否则它 returns 一个参数的函数,它依次调用带有 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),
// ...
)
您甚至可以在同一管道中混合和匹配咖喱化和非咖喱化版本,尽管我希望这会增加混乱。
我只想指出这个反模式:
// inline use of R.pipe
someVar = R.pipe(...)(someVar)
这不仅是 someVar
的变体,它违背了函数式编程的基本原则,而且也是 R.pipe
的误用,旨在创建一个新的 函数,如-
const someProcess = R.pipe(...)
const someNewVar = someProcess(someVar)
我知道您使用 R.pipe
是为了让代码更易读并从上到下流畅,但您的具体使用会适得其反。如果您要立即处理它,则没有理由创建中间函数。
考虑以更直接的方式表达您的意图 -
const output =
$ ( input // starting with input,
, filterWithRadius (lat, lng, radius) // filterWithRadius then,
, filterWithOptions (filterOptions, keyword) // filterWithOptions then,
, filterWithOptions (tagOptions, tag) // filterWithOptions then,
, // ... // ...
)
就像木匠制作特定于 his/her 项目的夹具和模板一样,程序员的工作是发明任何实用程序,使 his/her 的工作更轻松。要做到这一点,您需要的只是一个智能 $
。这是一个完整的例子 -
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
至于当某些参数为 nil (undefined
) 时使函数表现得像 R.identity
,我建议最好使用默认参数 -
const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
// ...
现在,如果 lat
和 lng
未定义,将提供 0
,这是一个有效的位置,称为 Prime Meridian.然而,0
的 radius
搜索应该 return 没有结果。所以我们可以轻松完成我们的功能 -
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
在不引入空检查复杂性的情况下更加健壮。这是一个明显的胜利,因为该函数更加自我记录,在更多情况下产生有效结果,并且不涉及为 "fix" 问题编写 更多 代码。
我看到您也在 filterWithRadius
函数中使用了内联 R.pipe
反模式。我们可以使用 $
在这里再次帮助我们 -
const filterWithRadius = (lat = 0, lng = 0, radius = 0, input) =>
radius <= 0
? input
: $ ( input
, filterByDistance (lat, lng, radius)
, map (addDistanceToObject (lat, lng))
, sortBy (prop ("distanceFromCenter"))
)
我希望这会让您大开眼界,看到一些可用的可能性。
Is there a cleaner/better way than this?
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>
您可以通过以下方式更深入地使用 ramda:
const filterWithFoo = R.unless(
(a, b, c) => R.isNil(a) || R.isNil(b) || R.isNil(c),
R.filter(...)
);