【问题标题】:Ramda pipe conditionally?有条件的 Ramda 管道?
【发布时间】:2020-04-27 19:02:50
【问题描述】:

我有一个过滤器函数,它本质上具有多个确定过滤逻辑的变量。如果定义了变量,我想过滤——如果没有,我不想过滤(即在管道中执行函数)。更一般地说,有一个谓词,我可以检查管道的每个参数,以确定我应该调用它还是只传递给下一个函数。

我这样做是为了防止复杂的分支逻辑,但对于函数式编程来说还很陌生,我认为这是重构的最佳方式。

例如:

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 是这些资源对象的数组。从本质上讲,filterRadiusfilterOptions 这两个函数都是纯函数,需要一个资源数组和 有效 参数(非未定义)并输出一个新的过滤列表。所以这里的目标是以某种方式组合(或重构),如果参数都是未定义的,它将运行函数,否则只是充当身份。

还有比这更干净/更好的方法吗?

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

【问题讨论】:

  • 请添加函数的实现、输入的数据和预期的结果。
  • @OriDrori 我已经添加了我的部分实现以及预期的结果。如果还有什么不清楚的地方请告诉我。
  • 如何将资源旁边的所有参数传递给管道?
  • @OriDrori 管道包含在路由处理程序中,我在其中解构 req.query 以便 latreq.query.lat 如果它存在,例如。因此,根据req.query 中的属性,可能会或可能不会定义一些变量。所以我正在尝试重构复杂的分支逻辑(之前我们有多个嵌套的 if 语句来进行过滤)。
  • 我认为用不常见的组合子(在 Ramda 之外)对条件分支进行编码不是一个好主意,因为它会混淆代码。如果您需要表达式并相应地拆分管道,只需使用 if/else 语句或条件运算符。

标签: functional-programming ramda.js


【解决方案1】:

要使此解决方案起作用,您的所有函数都需要进行柯里化,resources 作为最终参数。

这是创建一个函数 (passIfNil),它接受一个函数 (fn) 和参数(fn 的正确顺序)。如果此参数中的任何一个为 nil,则返回 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);

【讨论】:

  • 我相信这也会起作用:const passIfNil = (fn, ...args) => R.unless(R.any(R.isNil), R.always(fn(...args)));
  • 这似乎不允许将resources 数据传递到管道,或任何装饰函数接收它。
  • 是的。如果任何参数为零,则结果应为R.identity。回滚。
【解决方案2】:

我认为您希望将这种行为的责任放在错误的地方。如果您希望管道函数对某些数据具有一种行为,而对其他数据具有不同的行为(或者在这种情况下,缺少数据),那么这些单独的函数应该处理它,而不是包装它们的管道函数。

但是,正如 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。如果有,则返回标识函数;否则,它会返回一个有一个参数的函数,该函数又会使用 n - 1 参数和最新的一个参数调用原始函数。

我们使用它来构建一个管道,该管道将从接受所需变量的函数返回(这里从潜在的查询对象天真地解构)。通过传递查询然后传递要转换的实际数据来调用该函数。

这些示例显示了包含和排除的各种参数组合。

这假设您的函数没有被柯里化,也就是说,filterWithRadius 看起来像 (lat, lng, radius, resources) =&gt; ... 如果它们被柯里化,我们可能会这样写:

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),
  // ...
)

您甚至可以在同一管道中混合和匹配 curried 和非 curried 版本,尽管我希望这会增加混乱。

【讨论】:

    【解决方案3】:

    我只想指出这个反模式:

    // 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,
        , // ...                                     // ...
        )
    

    就像木匠制作特定于他/她的项目的夹具和模板一样,程序员的工作就是发明任何可以让他/她的工作更轻松的实用程序。使这成为可能,您只需要一个智能的$。这是一个完整的例子-

    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
    &lt;script src="https://unpkg.com/ramda@0.26.1/dist/ramda.min.js"&gt;&lt;/script&gt;

    您的程序不限于三 (3) 次操作。我们可以无忧无虑地连接数千个 -

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

    至于在某些参数为 nil (undefined) 时使函数的行为类似于 R.identity,我建议使用默认参数作为最佳实践 -

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

    现在,如果 latlng 未定义,则将提供 0,这是一个称为本初子午线的有效位置。但是,搜索 radius0 应该不会返回任何结果。这样我们就可以轻松完成我们的功能了-

    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 更加健壮,而不会引入空检查复杂性。这是一个明显的胜利,因为该函数更具自我记录性,在更多情况下产生有效结果,并且不涉及编写更多代码来“修复”问题。


    我看到你在 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"))
            )
    

    我希望这能让您看到一些可用的可能性。

    【讨论】:

      【解决方案4】:

      还有比这更清洁/更好的方法吗?

      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),
      );
      &lt;script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js" integrity="sha256-43x9r7YRdZpZqTjDT5E0Vfrxn1ajIZLyYWtfAXsargA=" crossorigin="anonymous"&gt;&lt;/script&gt;

      您可以通过以下方式更深入地使用 ramda:

      const filterWithFoo = R.unless(
        (a, b, c) => R.isNil(a) || R.isNil(b) || R.isNil(c),
        R.filter(...)
      );
      

      【讨论】:

        猜你喜欢
        • 2019-10-04
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-06-05
        • 1970-01-01
        • 2020-02-22
        相关资源
        最近更新 更多