【问题标题】:Arrays: conditionally mapping elements数组:有条件地映射元素
【发布时间】:2018-04-22 06:52:41
【问题描述】:

我使用 Redux,我经常会使用类似这样的表达式编写 reducer:

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

意图应该足够清楚:只修改数组中具有给定 id 的项,同时照原样复制其他项。另请注意,排序很重要,否则我只能使用filter() 函数。

这个 sn-p 在 eslint 上触发 no-confusing-arrow 错误,这对我来说很有意义。这也可以通过在箭头函数体周围添加圆括号来轻松解决,所以这里没什么大不了的:

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

我也想通过prettier解析这段代码,自动去掉箭头函数体周围的括号,回到sn-p的版本1。

这里明显的解决方案是用详细的方式编写箭头函数:

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

但老实说,我觉得它有点太笨拙了。

除了特定的上下文和使用的工具(eslint 和 prettier,可以进行不同的配置/关闭/其他),有没有更好的方法来编写这个?强>

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

Array.mapIf(callback, condition)

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

我自己显然可以编写这样的函数,但也许其他函数式语言中已经存在一些值得关注的通用文化/灵感。

【问题讨论】:

  • 我认为任何主要语言都没有这样的功能 - 要么是这个,要么我的 Google-fu 没有达到标准。大概是后者。 Clojure 是一种主要的函数式编程语言,该函数不存在。

标签: javascript arrays functional-programming


【解决方案1】:

没有这样的原生函数,因为你可以自己轻松实现:

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 分支。

使用成熟的函数式语言,您可能会使用functional lense 解决此问题。但是,我认为 mapWhen 足以满足您的情况并且更惯用。

【讨论】:

  • 同意命名!为什么要返回一个接受数组的函数,而不是直接将数组作为第三个(或第一个)参数函数传递,有什么特别的原因吗?
  • 我的意思很明显:将数组作为第三个(或第一个)参数传递给函数
  • @GavinoGrifoni 在 FP 中,您通常定义柯里化函数,其中每个 lambda 接受一个参数。不过,在 JS 中,人们习惯于使用多参数函数。所以我倾向于采取这种中间立场,只有最后一个参数对应于 curry 形式,因此函数是可组合的。
【解决方案2】:

在 ruby​​ 中你会返回类似的东西

users.dup.select {|u| u.id == selected_user_id }.each {|u| u.removed = false }

dup 在这种情况下很重要,因为在 Redux 中您希望返回一个新数组而不修改原始数组,因此您首先必须创建原始数组的副本。见https://stackoverflow.com/a/625746/1627766(注意讨论和这个很相似)

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

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 函数看起来很棒!
【解决方案3】:

如有必要,您可以使用Object.assign 添加属性。

return users.map(user =>
    Object.assign({}, user, user.id === selectedUserID && { removed: false })
);

【讨论】:

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

我将提供这个作为@ftor 实用答案的补充——这个 sn-p 展示了如何使用泛型函数来完成某种结果

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 的帮助——在这段摘录之后展开完整的代码 sn-p

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 函数的所有函数组合和最终语法,简直完美!
  • 我认为这应该在 npm 上的某个地方,因为我确信我不是唯一遇到这个问题的人。如果我们可以隐藏数据包中的幕后复杂性,只需公开 updateWhen 函数,那将非常有用!
  • 你有兴趣自己发布这样的包吗?否则,我很乐意自己做,当然要归功于所有功劳。
  • 加维诺,你的热情让我微笑。如果您确实创建了这样的包,请分享链接 - 值得注意的是:updateWhen 只是传感器的一个有用的应用程序。转换器可用于在您的程序中编写各种有用的数据转换。但是还有另一层抽象,它本身就是一种更好的数据类型。在这个程序中,我们的构建块是 []{},我们必须考虑使用 map 和 reduce 来转换它们——现在这些“转换器”——一个更好的构建块可以让我们摆脱对 map/reduce/transduce 的思考完全
  • 这就是像原生 MapSet 和库 moriImmutable.js 这样的东西发挥作用的地方 - 函数式程序员知道 []{} 只是不要削减它在某些情况下
猜你喜欢
  • 2020-10-06
  • 2020-07-31
  • 2022-11-17
  • 2019-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-03-04
  • 2010-12-04
相关资源
最近更新 更多