数组:有条件地映射元素

4

我使用Redux,经常需要编写像这样的带有表达式的reducers:

return users.map(user =>
  user.id !== selectedUserID ? user : {
    ... user,
    removed: false
  }
);

意图应该足够清晰:只修改具有给定id的数组项,同时将其他项保留原样复制。还要注意排序很重要,否则我可以使用filter()函数。
此代码片段会在eslint上触发no-confusing-arrow错误,这对我来说是有道理的。这也可以通过在箭头函数主体周围添加圆括号来轻松解决,所以这里没有大问题:
return users.map(user => (
  user.id !== selectedUserID ? user : {
    ... user,
    removed: false
  }
));

我也希望通过prettier解析此代码,该工具会自动删除箭头函数体周围的括号,使代码回到片段的第一版。
显而易见的解决方案是以冗长的方式编写箭头函数:
return users.map(user => {
  if(user.id !== selectedUserID) {
    return user; // unmodified user
  }
  return {
    ... user,
    removed: false
  };
});

但是我觉得它有点笨拙。

除了具体的上下文和使用的工具(eslint和prettier,可以进行不同的配置/关闭/任何操作),是否有更好的编写方式?

在我最疯狂的梦想中,存在一个签名类似于:

Array.mapIf(callback, condition)

这个函数会循环遍历数组中的所有元素,并仅对满足给定条件的元素调用callback函数,同时返回其他未修改的元素。

显然我可以自己编写这样的函数,但也许其他函数式语言中已经存在类似的函数,值得一看以获得一般文化/灵感。


3
我认为没有哪种主要的语言有这样的功能——要么是我的Google搜索能力不够好,要么就是这个功能不存在。很可能是后者。这个功能在Clojure中并不存在,Clojure是一种主要的函数式编程语言。 - Qwerp-Derp
4个回答

3

没有这样的原生功能,因为您可以轻松地自己实现它:

const mapWhen = (p, f) => xs => xs.map(x => p(x) ? f(x) : x);

const users = [
  {id: 1, removed: true},
  {id: 2, removed: true},
  {id: 3, removed: true}
];

console.log(
  mapWhen(
    ({id}) => id === 2,
    user => Object.assign({}, user, {removed: false})
  ) (users)
);

我选择使用 mapWhen 作为名称,而不是使用 mapIf,因为后者会暗示存在一个 else 分支。

在成熟的函数式语言中,您可能会通过函数式镜头来解决此问题。然而,我认为对于您的情况,mapWhen已足够,并且更符合惯例。


同意命名!你为什么要返回一个接受数组的函数,而不是直接将数组作为第三个(或第一个)参数传递给函数呢? - GavinoGrifoni
我显然是指:将数组作为第三个(或第一个)参数传递给函数。 - GavinoGrifoni
1
在FP中,通常定义柯里化函数,其中每个lambda只接受一个参数。但在JS中,人们习惯于使用多参数函数。因此,我倾向于采取这种折中方法,只有最后一个参数对应于柯里化形式,以使函数可组合。 - user6445533

2
在Ruby中,你会返回类似于以下内容:
users.dup.select {|u| u.id == selected_user_id }.each {|u| u.removed = false }

dup在这种情况下非常重要,因为在Redux中你需要返回一个新数组而不是修改原始数组,所以你首先必须创建原始数组的副本。参见https://dev59.com/kXRB5IYBdhLWcg3weHHx#625746(注意该讨论与此非常相似)。

在你的情况下,我会使用:

const users = [
  {id: 1, removed: true},
  {id: 2, removed: true},
  {id: 3, removed: true}
];

const selectedUserId = 2;

console.log(
  users.map(
    (user) => ({
      ...user,
      removed: (user.id !== selectedUserId ? false : user.removed)
    })
  )
);


我喜欢Ruby解决问题的方式,像这种情况下,select函数似乎非常棒! - GavinoGrifoni

0
你可以使用 Object.assign,它会在必要时添加属性。
return users.map(user =>
    Object.assign({}, user, user.id === selectedUserID && { removed: false })
);

我没有考虑过这个选项,可能是解决这个特定情况的不错方案,谢谢!不过,我还想看看是否有更通用的方法来解决这个问题。 - GavinoGrifoni

0
我将提供这篇文章作为对@ftor实用答案的补充 - 这个代码片段展示了如何使用通用函数来实现这种结果。

const identity = x =>
  x

const comp = ( f, g ) =>
  x => f ( g ( x ) )
  
const compose = ( ...fs ) =>
  fs.reduce ( comp, identity )
  
const append = ( xs, x ) =>
  xs.concat ( [ x ] )

const transduce = ( ...ts ) => xs =>
  xs.reduce ( compose ( ...ts ) ( append ), [] )

const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, x ) : append ( acc, x )

const mapper = f =>
  k => ( acc, x ) => k ( acc, f ( x ) )

const data0 =
  [ { id: 1, remove: true }
  , { id: 2, remove: true }
  , { id: 3, remove: true }
  ]

const data1 =
  transduce ( when ( x => x.id === 2)
            , mapper ( x => ( { ...x, remove: false } ) )
            )
            (data0)
            
console.log (data1)
// [ { id: 1, remove: true }
// , { id: 2, remove: false }
// , { id: 3, remove: true }
// ]

然而,我认为我们可以通过更通用的行为来改进when,我们只需要一些虚构类型LeftRight的帮助-在此摘录后展开完整的代码片段

const Left = value =>
  ({ fold: ( f, _ ) =>
       f ( value )
  })

const Right = value =>
  ({ fold: ( _, f ) =>
       f ( value )
  })

// a more generic when
const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, Right ( x ) ) : k ( acc, Left ( x ) )

// mapping over Left/Right
mapper ( m =>
  m.fold ( identity                         // Left branch
         , x => ( { ...x, remove: false } ) // Right branch
         ) ) 

const Left = value =>
  ({ fold: ( f, _ ) =>
       f ( value )
  })
  
const Right = value =>
  ({ fold: ( _, f ) =>
       f ( value )
  })

const identity = x =>
  x

const comp = ( f, g ) =>
  x => f ( g ( x ) )
  
const compose = ( ...fs ) =>
  fs.reduce ( comp, identity )
  
const append = ( xs, x ) =>
  xs.concat ( [ x ] )

const transduce = ( ...ts ) => xs =>
  xs.reduce ( compose ( ...ts ) ( append ), [] )

const when = f =>
  k => ( acc, x ) => f ( x ) ? k ( acc, Right ( x ) ) : k ( acc, Left ( x ) )

const mapper = f =>
  k => ( acc, x ) => k ( acc, f ( x ) )

const data0 =
  [ { id: 1, remove: true }
  , { id: 2, remove: true }
  , { id: 3, remove: true }
  ]

const data1 =
  transduce ( when ( x => x.id === 2 )
            , mapper ( m =>
               m.fold ( identity
                      , x => ( { ...x, remove: false } ) 
                      ) )
            )
            (data0)
            
console.log (data1)
// [ { id: 1, remove: true }
// , { id: 2, remove: false }
// , { id: 3, remove: true }
// ]

因此,您的最终函数可能看起来像这样

const updateWhen = ( f, records, xs ) =>
  transduce ( when ( f )
            , mapper ( m =>
                m.fold ( identity
                       , x => ( { ...x, ...records } )
                       ) )
            ) ( xs )

你可以在你的 reducer 中这样调用它

const BakeryReducer = ( donuts = initState , action ) =>
  {
    switch ( action.type ) {
      case ApplyGlaze:
        return updateWhen ( donut => donut.id === action.donutId )
                          , { glaze: action.glaze }
                          , donuts
                          )
      // ...
      default:
        return donuts
    }
  } 

哇,这是一个纯函数式方法解决问题的绝佳例子!喜欢所有的函数组合和updateWhen函数的最终语法简直完美! - GavinoGrifoni
我认为这应该在npm的某个地方,因为我相信我不是唯一遇到这个问题的人。如果我们可以将幕后复杂性隐藏在一个包中,只暴露updateWhen函数,那将非常有用! - GavinoGrifoni
你有兴趣自己发布这个软件包吗?否则我很乐意将其发布,当然会提供应得的所有荣誉。 - GavinoGrifoni
Gavino,你的热情让我感到开心。如果你真的创建了这样一个包,请分享链接——值得注意的是:updateWhen只是转换器的一个有用应用。转换器可以用来编写程序中的各种各样有用的数据转换。但是还有另一层抽象,它本身就是更好的数据类型;在这个程序中,我们的构建块是[]{},我们必须考虑如何使用map和reduce进行转换,现在又加入了这些“转换器”——一个更好的构建块将使我们摆脱对map/reduce/transduce的思考。 - Mulan
这就是本地MapSet,以及库moriImmutable.js发挥作用的地方 - 函数式编程人员知道,在某些情况下,[]{}并不足够。 - Mulan
但是!-变换器不仅对抽象数据结构有用...因此,通用的变换器库仍然很有用。如果没有这样的库,我会感到惊讶。如果您有兴趣了解变换器的强大功能,我有一些包含变换器示例的其他答案 - 我对这个主题的理解归功于我的榜样 - Mulan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接