草庐IT

javascript - lambda : Fold an object

coder 2024-07-30 原文

我正在构建 PWA 并使用 Ramda 进行逻辑构建。我正在尝试构建一个函数,该函数给出了 Google Places Detail response返回一个自定义地址对象。

让我通过向您展示我的测试来在代码中描述它:

assert({
  given: 'a google places api response from Google Places',
  should: 'extract the address',
  actual: getAddressValues({
    address_components: [
      {
        long_name: '5',
        short_name: '5',
        types: ['floor'],
      },
      {
        long_name: '48',
        short_name: '48',
        types: ['street_number'],
      },
      {
        long_name: 'Pirrama Road',
        short_name: 'Pirrama Rd',
        types: ['route'],
      },
      {
        long_name: 'Pyrmont',
        short_name: 'Pyrmont',
        types: ['locality', 'political'],
      },
      {
        long_name: 'Council of the City of Sydney',
        short_name: 'Sydney',
        types: ['administrative_area_level_2', 'political'],
      },
      {
        long_name: 'New South Wales',
        short_name: 'NSW',
        types: ['administrative_area_level_1', 'political'],
      },
      {
        long_name: 'Australia',
        short_name: 'AU',
        types: ['country', 'political'],
      },
      {
        long_name: '2009',
        short_name: '2009',
        types: ['postal_code'],
      },
    ],
    geometry: {
      location: {
        lat: -33.866651,
        lng: 151.195827,
      },
      viewport: {
        northeast: {
          lat: -33.8653881697085,
          lng: 151.1969739802915,
        },
        southwest: {
          lat: -33.86808613029149,
          lng: 151.1942760197085,
        },
      },
    },
  }),
  expected: {
    latitude: -33.866651,
    longitude: 151.195827,
    city: 'Pyrmont',
    zipCode: '2009',
    streetName: 'Pirrama Road',
    streetNumber: '48',
  },
});

正如你所看到的,我想要的地址对象更“扁平”(因为没有更好的术语)。我正在努力编写这个转换函数。我尝试使用 Ramda 的 evolve 来做这件事,但这会保留 key 。我需要先使用进化来转换对象,然后 reduce传播 key 的对象。
// Pseudo
({ address_components }) => ({ ...address_components })

我使用evolve成功提取了相关信息并使用 renameKeys 重命名 key 来自 Ramda adjunct,但我不知道之后如何压平该对象。你是怎样做的?或者是否有更简单的方法来实现所需的转换?

编辑:

我找到了一种方法来实现我的转变,但它非常冗长。我觉得有一种更简单的方法来提取地址数据。无论如何,这是我目前的解决方案:
export const getAddressValues = pipe(
  evolve({
    address_components: pipe(
      reduce(
        (acc, val) => ({
          ...acc,
          ...{
            [head(prop('types', val))]: prop('long_name', val),
          },
        }),
        {}
      ),
      pipe(
        pickAll([
          'route',
          'locality',
          'street_number',
          'country',
          'postal_code',
        ]),
        renameKeys({
          route: 'streetName',
          locality: 'city',
          street_number: 'streetNumber',
          postal_code: 'zipCode',
        }),
        map(ifElse(isNil, always(null), identity))
      )
    ),
    geometry: ({ location: { lat, lon } }) => ({
      latitude: lat,
      longitude: lon,
    }),
  }),
  ({ address_components, geometry }) => ({ ...address_components, ...geometry })
);

编辑:根据@codeepic 的回答,这是我最终使用的纯 JavaScript 解决方案(尽管 @user3297291 很优雅,我喜欢它):
const getLongNameByType = (arr, type) => 
  arr.find(o => o.types.includes(type)).long_name;

const getAddressValues = ({ address_components: comp, geometry: { location: { lat, lng } } }) => ({
  latitude: lat,
  longitude: lng,
  city: getLongNameByType(comp, 'locality'),
  zipCode: getLongNameByType(comp, 'postal_code'),
  streetName: getLongNameByType(comp, 'route'),
  streetNumber: getLongNameByType(comp, 'street_number'),
  country: getLongNameByType(comp, 'country'),
});

最佳答案

镜头可能是您最好的选择。 Ramda 有一个通用的 lens 函数,以及对象属性( lensProp )、数组索引( lensIndex )和更深路径( lensPath )的特定函数,但不包括在数组中查找匹配值的函数通过身份证。不过,我们自己制作并不难。

通过将两个函数传递给 lens 来制作镜头:获取对象并返回相应值的getter,以及获取新值和对象并返回对象的更新版本的setter。

我们写在这里lensMatch它查找或设置数组中给定属性名称与提供的值匹配的值。和 lensType简单地通过 'type'lensMatch取回一个函数,该函数将采用一组类型并返回一个镜头。

使用任何镜头,我们都有 view , set , 和 over 分别获取、设置和更新值的函数。

const lensMatch = (propName) => (key) => lens ( 
  find ( propEq (propName, key) ),
  (val, arr, idx = findIndex (propEq (propName, key), arr)) =>
     update(idx > -1 ? idx : length(arr), val, arr)
)
const lensTypes = lensMatch ('types')
const longName = (types) => 
  compose (lensProp ('address_components'), lensTypes (types), lensProp ('long_name'))
// can define `shortName` similarly if needed

const getAddressValues = applySpec ( {
  latitude:     view (lensPath (['geometry', 'location', 'lat']) ),
  longitude:    view (lensPath (['geometry', 'location', 'lng']) ),
  city:         view (longName (['locality', 'political']) ),
  zipCode:      view (longName (['postal_code']) ),
  streetName:   view (longName (['route']) ),
  streetNumber: view (longName (['street_number']) ),
})

const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}

console .log (
  getAddressValues (response)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script><script>
const {applySpec, compose, find, findIndex, lens, lensProp, lensPath, propEq, update, view} = R  </script>


我们可以使用更简单的 lensMatch 版本。对于这个问题,因为我们没有使用 setter:

const lensMatch = (propName) => (key) => 
  lens (find (propEq (propName, key) ), () => {} )

但我不会推荐它。全lensMatch是一个有用的效用函数。

我们可能希望通过多种方式更改此解决方案。我们可以移动 viewlongName并编写另一个小助手来包装 lensPath 的结果在 view简化调用看起来更像这样。

  longitude:    viewPath (['geometry', 'location', 'lng']),
  city:         longName (['locality', 'political']),

或者我们可以写一个包装到 applySpec ,也许 viewSpec它简单地将所有属性函数包装在 view 中.这些留给读者作为练习。

(这个介绍几乎没有从我的 earlier answer 修改。)

更新

我还尝试了一种完全独立的方法。我认为它的可读性较差,但它的性能可能更高。比较这些选项很有趣。

const makeKey = JSON.stringify

const matchType = (name) => (
  spec,
  desc = spec.reduce( (a, [t, n]) => ({...a, [makeKey (t)]: n}), {})
) => (xs) => xs.reduce(
  (a, { [name]: fld, types }, _, __, k = makeKey(types)) => ({
    ...a,
    ...(k in desc ? {[desc[k]]: fld} : {})
  }), 
  {}
)
const matchLongNames = matchType('long_name')

const getAddressValues2 = lift (merge) (
  pipe (
    prop ('address_components'), 
    matchLongNames ([
      [['locality', 'political'], 'city'],
      [['postal_code'], 'zipCode'],
      [['route'], 'streetName'],
      [['street_number'], 'streetNumber'],
    ])
  ),
  applySpec ({
    latitude: path(['geometry', 'location', 'lat']),
    longitude: path(['geometry', 'location', 'lng']),
  })
)

const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}

console .log (
  getAddressValues2 (response)
)
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.js"></script><script>
const {applySpec, lift, merge, path, pipe, prop} = R                          </script>


此版本将问题分为两部分:一是针对更简单的字段,latitudelongitude ,一个用于其他更难匹配的,然后简单地合并将每个应用到响应的结果。

较简单的字段不需要注释。这只是 applySpec 的简单应用和 path .另一个,封装为 matchType接受将输入的类型(以及要提取的字段的名称)与输出的属性名称匹配的规范。它建立了一个索引,desc ,基于类型(这里使用 JSON.stringify ,虽然有明显的替代品)。然后它减少一个对象数组,找到任何其 types 的对象。属性在索引中并将其值与适当的字段名称联系起来。

这是一个有趣的变体。我仍然更喜欢我的原始版本,但是对于大型阵列,这可能会对性能产生重大影响。

另一个更新

在阅读了 user633183 的回答后,我一直在思考我想如何使用这样的东西。使用 Maybe 有很多话要说在这里。但是我可能希望以两种不同的方式与结果进行交互。一个让我逐个字段操作,每个字段都包裹在自己的 Maybe 中.另一个是作为一个完整的对象,拥有它的所有领域;但出于所展示的原因,它必须包含在自己的 Maybe 中。

这是一个不同的版本,它生成第一个变体并包含一个将其转换为第二个变体的函数。

const maybeObj = pipe (
  toPairs,
  map(([k, v]) => v.isJust ? Just([k, v.value]) : Nothing()),
  sequence(Maybe),
  map(fromPairs)
)

const maybeSpec = (spec = {}) => (obj = {}) =>
  Object .entries (spec) .reduce (
    (a, [k, f] ) => ({...a, [k]: Maybe (is (Function, f) && f(obj))}), 
    {}
  )

const findByTypes = (types = []) => (xs = []) =>
  xs .find (x => equals (x.types, types) ) 

const getByTypes = (name) => (types) => pipe (
  findByTypes (types),
  prop (name)
)

const getAddressComponent = (types) => pipe (
  prop ('address_components'),
  getByTypes ('long_name') (types)
)
const response = {"address_components": [{"long_name": "5", "short_name": "5", "types": ["floor"]}, {"long_name": "48", "short_name": "48", "types": ["street_number"]}, {"long_name": "Pirrama Road", "short_name": "Pirrama Rd", "types": ["route"]}, {"long_name": "Pyrmont", "short_name": "Pyrmont", "types": ["locality", "political"]}, {"long_name": "Council of the City of Sydney", "short_name": "Sydney", "types": ["administrative_area_level_2", "political"]}, {"long_name": "New South Wales", "short_name": "NSW", "types": ["administrative_area_level_1", "political"]}, {"long_name": "Australia", "short_name": "AU", "types": ["country", "political"]}, {"long_name": "2009", "short_name": "2009", "types": ["postal_code"]}], "geometry": {"location": {"lat": -33.866651, "lng": 151.195827}, "viewport": {"northeast": {"lat": -33.8653881697085, "lng": 151.1969739802915}, "southwest": {"lat": -33.86808613029149, "lng": 151.1942760197085}}}}

getAddressComponent (['route']) (response)

const extractAddress = maybeSpec({
  latitude:     path (['geometry', 'location', 'lat']),
  longitude:    path (['geometry', 'location', 'lng']),
  city:         getAddressComponent (['locality', 'political']),
  zipCode:      getAddressComponent  (['postal_code']),
  streetName:   getAddressComponent  (['route']),
  streetNumber: getAddressComponent (['street_number']),  
})

const transformed = extractAddress (response)

// const log = pipe (toString, console.log)
const log1 = (obj) => console.log(map(toString, obj))
const log2 = pipe (toString, console.log)

// First variation
log1 (
  transformed
)

// Second variation
log2 (
  maybeObj (transformed)
)
<script src="https://bundle.run/ramda@0.26.1"></script>
<script src="https://bundle.run/ramda-fantasy@0.8.0"></script>
<script>
const {equals, fromPairs, is, map, path, pipe, prop, toPairs, sequence, toString} = ramda;
const {Maybe} = ramdaFantasy;
const {Just, Nothing} = Maybe;
</script>


函数maybeObj转换这样的结构:

{
  city: Just('Pyrmont'),
  latitude: Just(-33.866651)
}

变成这样:

Just({
  city: 'Pyrmont',
  latitude: -33.866651
})

但有一个 Nothing :

{
  city: Just('Pyrmont'),
  latitude: Nothing()
}

回到 Nothing :

Nothing()

它作用于对象很像 R.sequence 适用于数组和其他可折叠类型。 (对于 long, complicated reasons,Ramda 不会将对象视为可折叠的。)

其余的很像@user633183 的答案,但用我自己的习语写的。可能唯一值得注意的其他部分是 maybeSpec ,它的作用很像 R.applySpec 但将每个字段包装在 Just 中或 Nothing .

(请注意,我正在使用 Maybe from Ramda-Fantasy 。该项目已停止,我可能应该弄清楚使用那里的最新项目之一需要进行哪些更改。归咎于懒惰。唯一的我想,所需的更改是将调用 Maybe 替换为他们提供的任何函数 [或您自己的] 将 nil 值转换为 Nothing 并将其他所有值转换为 Just s。)

关于javascript - lambda : Fold an object,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56438949/

有关javascript - lambda : Fold an object的更多相关文章

  1. ruby-on-rails - 使用 javascript 更改数据方法不会更改 ajax 调用用户的什么方法? - 2

    我遇到了一个非常奇怪的问题,我很难解决。在我看来,我有一个与data-remote="true"和data-method="delete"的链接。当我单击该链接时,我可以看到对我的Rails服务器的DELETE请求。返回的JS代码会更改此链接的属性,其中包括href和data-method。再次单击此链接后,我的服务器收到了对新href的请求,但使用的是旧的data-method,即使我已将其从DELETE到POST(它仍然发送一个DELETE请求)。但是,如果我刷新页面,HTML与"new"HTML相同(随返回的JS发生变化),但它实际上发送了正确的请求类型。这就是这个问题令我困惑的

  2. ruby - 了解在 Ruby 中与 lambda 一起使用的 inject 行为 - 2

    我经常将预配置的lambda插入可枚举的方法中,例如“map”、“select”等。但是“注入(inject)”的行为似乎有所不同。例如与mult4=lambda{|item|item*4}然后(5..10).map&mult4给我[20,24,28,32,36,40]但是,如果我制作一个2参数lambda用于像这样的注入(inject),multL=lambda{|product,n|product*n}我想说(5..10).inject(2)&multL因为“inject”有一个可选的单个初始值参数,但这给了我......irb(main):027:0>(5..10).inject

  3. ruby - 当你有一个没有参数的 case 语句并且 when 子句是 lambda 时会发生什么? - 2

    这段代码没有像我预期的那样执行:casewhen->{false}then"why?"else"ThisiswhatIexpect"end#=>"why?"这也不是casewhen->(x){false}then"why?"else"ThisiswhatIexpect"end#=>"why?"第一个then子句在两种情况下都被执行,这意味着我提供给when子句的lambda没有被调用。我知道无论when子句的主题是什么,都应该调用大小写相等运算符===。我想知道当没有为case提供参数时,===的另一边会发生什么。我在想它可能是nil,但它不可能是:->{false}===nil#=>

  4. ruby - 如何将 lambda 传递给 Hash.each? - 2

    如何将lambda传递给hash.each,以便我可以重复使用一些代码?>h={a:'b'}>h.eachdo|key,value|end=>{:a=>"b"}>test=lambdado|key,value|puts"#{key}=#{value}"end>test.call('a','b')a=b>h.each&testArgumentError:wrongnumberofarguments(1for2)from(irb):1:in`blockinirb_binding'from(irb):5:in`each'from(irb):5from/Users/jstillwell/.rv

  5. ruby - 在 Mechanize 中使用 JavaScript 单击链接 - 2

    我有这个:AccountSummary我想单击该链接,但在使用link_to时出现错误。我试过:bot.click(page.link_with(:href=>/menu_home/))bot.click(page.link_with(:class=>'top_level_active'))bot.click(page.link_with(:href=>/AccountSummary/))我得到的错误是:NoMethodError:nil:NilClass的未定义方法“[]” 最佳答案 那是一个javascript链接。Mechan

  6. Ruby 将上下文分配给 lambda? - 2

    是否可以不为lambda分配上下文?例如:classRuledefget_rulereturnlambda{putsname}endendclassPersonattr_accessor:namedefinit_rule@name="ruby"Rule.new.get_rule.call()#shouldsay"ruby"butsaywhatobjectofclassRull,doesnothavevariablename#orself.instance_eval&Rule.new.get_ruleendend我的目标是->没有上下文的存储过程对象,并在特定位置调用之前分配上下文。可能

  7. javascript - jQuery 的 jquery-1.10.2.min.map 正在触发 404(未找到) - 2

    我看到有关未找到文件min.map的错误消息:GETjQuery'sjquery-1.10.2.min.mapistriggeringa404(NotFound)截图这是从哪里来的? 最佳答案 如果ChromeDevTools报告.map文件的404(可能是jquery-1.10.2.min.map、jquery.min.map或jquery-2.0.3.min.map,但任何事情都可能发生)首先要知道的是,这仅在使用DevTools时才会请求。您的用户不会遇到此404。现在您可以修复此问题或禁用sourcemap功能。修复:获取文

  8. ruby-on-rails - 我将 Rails3 与 tinymce 一起使用。如何呈现用户关闭浏览器javascript然后输入xss? - 2

    我有一个用Rails3编写的站点。我的帖子模型有一个名为“内容”的文本列。在帖子面板中,html表单使用tinymce将“content”列设置为textarea字段。在首页,因为使用了tinymce,post.html.erb的代码需要用这样的原始方法来实现。.好的,现在如果我关闭浏览器javascript,这个文本区域可以在没有tinymce的情况下输入,也许用户会输入任何xss,比如alert('xss');.我的前台会显示那个警告框。我尝试sanitize(@post.content)在posts_controller中,但sanitize方法将相互过滤tinymce样式。例如

  9. ruby - 调用 instance_eval(&lambda) 传递当前上下文时出现错误 'wrong number of arguments' - 2

    要清楚-此代码运行完美-codewithproc但如果我将Proc.new更改为lambda,则会出现错误ArgumentError:wrongnumberofarguments(1for0)这可能是因为instance_eval想要将self作为参数传递,而lambda将其视为一种方法并且不接受未知参数?有两个例子-第一个是工作:classRuledefget_ruleProc.new{putsname}endendclassPersonattr_accessor:namedefinit_rule@name="ruby"instance_eval(&Rule.new.get_rule

  10. ruby - 使用 Selenium WebDriver 启用/禁用 javascript - 2

    出于某种原因,我必须为Firefox禁用javascript(手动,我们按照提到的步骤执行http://support.mozilla.org/en-US/kb/javascript-settings-for-interactive-web-pages#w_enabling-and-disabling-javascript)。使用Ruby的SeleniumWebDriver如何实现这一点? 最佳答案 是的,这是可能的。而是另一种方式。您首先需要查看链接Selenium::WebDriver::Firefox::Profile#[]=

随机推荐