拉姆达:折叠一个物体

时间:2019-06-04 07:07:30

标签: javascript functional-programming google-places-api ramda.js

我正在构建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来做到这一点,但这保留了密钥。我需要先使用Evolution来变换对象,然后reduce将对象扩展密钥。

// Pseudo
({ address_components }) => ({ ...address_components })

我成功地使用了evolve提取了相关信息,并使用了renameKeys重命名了Ramda附件中的键,但是后来我不知道如何弄平该对象。你是怎样做的?还是有一种更简单的方法来实现所需的转换?

编辑:

我找到了实现转换的方法,但是转换非常冗长。我觉得有一种更简单的方法来提取地址数据。无论如何,这是我当前的解决方案:

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'),
});

4 个答案:

答案 0 :(得分:3)

为此可能最好的选择是透镜。 Ramda具有通用的lens函数,以及针对对象属性(lensProp),数组索引(lensIndex)和更深层路径(lensPath)的特定函数。 ,但其中不包括通过id在数组中查找匹配值的方法。不过,自己打造并不难。

镜头是通过将两个函数传递给lens来制成的:一个获取对象并获取相应值的吸气剂,以及获取新值和对象并返回对象的更新版本的塞特眼镜。

在这里,我们编写lensMatch来在给定属性名称与提供的值匹配的数组中查找或设置值。并且lensType只需将'type'传递给lensMatch即可返回一个函数,该函数将采用类型数组并返回一个镜头。

使用任何镜头,我们都有viewsetover函数,它们分别获取,设置和更新值。

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是有用的实用程序功能。

我们可能有几种方法可以更改此解决方案。我们可以将view移到longName内,并编写另一个辅助程序,将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,另一个用于较难匹配的字段,它们很难匹配,然后简单地合并应用每个字段的结果那些回应。

更简单的字段无需评论。这只是applySpecpath的简单应用。另一个封装为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相似,但是将每个字段都包装在JustNothing中。

(请注意,我正在使用Maybe from Ramda-Fantasy。该项目已经停产,我可能应该已经知道使用那里的最新项目之一需要进行哪些更改。我想,唯一需要做的更改就是用它们提供的任何功能(或您自己提供的功能)替换对Maybe的调用,以将nil值转换为Nothing并将每个其他值转换为{{ 1}} s。)

答案 1 :(得分:2)

也许没有太大的改善,但是我有一些建议:

  • 您可以使用php artisan config:cache 代替(有点难读)内联缩减功能。
  • 通过拆分地址和位置逻辑,并使用组合的帮助器将两者结合起来,可以更轻松地了解发生的情况(使用indexByjuxt
  • 您可以使用mergeAll代替applySpec + pickAll

renameKeys
const { pipe, indexBy, prop, head, compose, path, map, applySpec, juxt, mergeAll } = R;

const reformatAddress = pipe(
  prop("address_components"),
  indexBy(
    compose(head, prop("types"))
  ),
  applySpec({
    streetName: prop("route"),
    city: prop("locality"),
    streetNumber: prop("street_number"),
    zipCode: prop("postal_code"),
  }),
  map(prop("long_name"))
);

const reformatLocation = pipe(
  path(["geometry", "location"]),
  applySpec({
    latitude: prop("lat"),
    longitude: prop("lng")
  })
);

// Could also be: converge(mergeRight, [ f1, f2 ])
const formatInput = pipe(
  juxt([ reformatAddress, reformatLocation]),
  mergeAll
);

console.log(formatInput(getInput()));


function getInput() { return {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}}}}; }

答案 2 :(得分:2)

这是在普通JS中实现的方法:很少的代码行,整个魔术发生在findObjByType函数中:

const findObjByType = (obj, type) => 
  obj.address_components.find(o => o.types.includes(type));

const getAddressValues = obj => ({
  latitude: obj.geometry.location.lat,
  longitude: obj.geometry.location.lng,
  city: findObjByType(obj, 'locality').long_name,
  zipCode: findObjByType(obj, 'postal_code').long_name,
  streetName: findObjByType(obj, 'route').long_name,
  streetNumber: findObjByType(obj, 'street_number').long_name
});

Ramda可能会有所帮助,但是如果普通的JavaScript可以用更少的代码来简化功能,并且易于阅读,那么不要因为使用功能库而编写钝的代码。

编辑:阅读@ user3297291回答后,我不得不承认他的Ramda解决方案非常优雅,但我的观点仍然成立。如果您可以在保持可读性的同时减少编写代码,则永远不要编写更多代码。

stackblitz

上的解决方案

答案 3 :(得分:2)

函数样式的强度取决于保证,函数既将值作为 input ,又将值作为 output 。如果未完全定义函数的输出,则我们函数输出的任何使用者都可能遭受未定义的行为。空检查耗尽了写入能力,运行时异常是一种偏头痛。我们可以通过遵循职能纪律来避免两者。

您的问题中提出的问题很重要。要检索的数据是深层嵌套的,访问地址组件需要进行古怪的搜索和匹配。要开始编写转换,我们必须完全定义函数的域(输入)和共域(输出)。

域很简单:您问题中的输入数据是一个对象,因此我们的转换必须为 all 对象生成有效的结果。共域稍微更具体-因为我们的转换有可能以多种方式失败,所以我们的函数将返回有效的结果对象,为空。

作为类型签名,这是下面的样子–

type Result =
  { latitude: Number
  , longitude: Number
  , city: String
  , zipCode: String
  , streetName: String
  , streetNumber: String
  }

transform : Object -> Maybe Result

为了给出简单的单词,给定有效的输入数据,我们的transform将返回有效的结果,例如-

Just { latitude: 1, longitude: 2, city: "a", zipCode: "b", streetName: "c", streetNumber: "d" }

当收到无效数据时,我们的transform将不返回任何内容。–

Nothing

没有其他返回值是可能的。这意味着我们的函数保证它返回部分或稀疏结果,如-

{ latitude: 1, longitude: 2, city: undefined, zipCode: "b", streetName: "c", streetNumber: undefined }

功能学科还说我们的功能不应该有副作用,因此我们的转换还必须确保它不会引发错误,例如–

TypeError: cannot read property "location" of undefined
TypeError: data.reduce is not a function

此线程中的其他答案未采取此类预防措施,当输入数据格式错误时,它们会引发错误或产生稀疏结果。我们严格的方法将避免这些陷阱,从而确保您transform函数的任何使用者都不必处理空检查或捕获潜在的运行时错误。

您的问题的核心是我们处理了许多潜在值。我们将提供data.maybe软件包,该软件包提供:

  

用于可能不存在的值或可能失败的计算的结构。 Maybe(a)显式建模Nullable类型中隐含的效果,因此不存在与使用nullundefined相关的问题-像NullPointerException或{{1 }}。

听起来很合适。我们将从草绘一些代码开始,然后在空中挥舞双手。假设我们有一个TypeError函数,它接受一个getAddress和一个String,并且也许返回一个Object-

String

我们开始写// getAddress : String -> Object -> Maybe String ...

transform

好的,是的。我们甚至还没有完成,而嵌套的const { Just } = require ("data.maybe") // transform : Object -> Maybe Result const transform = (data = {}) => getAddress ("locality", data) .chain ( city => getAddress ("postal_code", data) .chain ( zipCode => getAddress ("route", data) .chain ( streetName => Just ({ city, zipCode, streetName }) ) ) ) transform (data) // Just {city: "Pyrmont", zipCode: "2009", streetName: "Pirrama Road"} transform ({}) // Nothing 调用完全是一团糟!如果仔细观察,这里有一个简单的模式。功能学科说,当您看到模式时,应该抽象;那是个古怪的词,意思是创建函数

在我们陷入.chain地狱之前,让我们考虑一种更通用的方法。我必须在深层嵌套的对象中找到六(6)个可能的值,如果可以全部获取,我想构造一个.chain值-

Result

恢复理智-上面,我们编写了一个简单的功能// getAddress : String -> Object -> Maybe String // getLocation : String -> Object -> Maybe Number const { lift } = require ("ramda") // make : (Number, Number, String, String, String, String) -> Result const make = (latitude, longitude, city, zipCode, streetName, streetNumber) => ({ latitude, longitude, city, zipCode, streetName, streetNumber }) // transform : Object -> Maybe Result const transform = (o = {}) => lift (make) ( getLocation ("lat", o) , getLocation ("lng", o) , getAddress ("locality", o) , getAddress ("postal_code", o) , getAddress ("route", o) , getAddress ("street_number", o) ) transform (data) // Just {latitude: -33.866651, longitude: 151.195827, city: "Pyrmont", zipCode: "2009", streetName: "Pirrama Road", …} transform ({}) // Nothing ,该功能接受六(6)个自变量来构造make。使用Result,我们可以在lift context 中应用make,并发送Maybe值作为参数。但是,如果任何值为Maybe,结果我们将一无所获,并且将不应用Nothing

大多数艰苦的工作已经在这里完成。我们只需要完成实现makegetAddress。我们将从getLocation开始,这是两者中的简单者。

getLocation

在开始之前我们还没有// safeProp : String -> Object -> Maybe a // getLocation : String -> Object -> Maybe Number const getLocation = (type = "", o = {}) => safeProp ("geometry", o) .chain (safeProp ("location")) .chain (safeProp (type)) getLocation ("lat", data) // Just {value: -33.866651} getLocation ("lng", data) // Just {value: 151.195827} getLocation ("foo", data) // Nothing ,但是我们在进行过程中通过发明便利性来使事情变得容易。功能学科说功能应该很简单并且可以做一件事。这样的功能更易于编写,阅读,测试和维护。它们具有附加的优势,它们可以组合并且可以在程序的其他区域重用。此外,当一个函数具有 name 时,它使我们可以更直接地编码我们的意图-safePropgetLocation查找的序列-几乎没有对该函数的其他解释可能。

在这个答案的每一部分中,我都揭示了另一个潜在的依赖性,但这似乎是令人讨厌的,但这是故意的。我们将始终专注于全局,仅在必要时放大较小的部分。由于safeProp的实现要困难得多,因为我们的函数必须筛选以找到特定地址组成部分的无序列表。如果我们在进行过程中增加了更多功能,请不要感到惊讶–

getAddress

有时使用// safeProp : String -> Object -> Maybe a // safeFind : (a -> Boolean) -> [ a ] -> Maybe a const { includes } = require ("ramda") // getAddress : String -> Object -> Maybe String const getAddress = (type = "", o = {}) => safeProp ("address_components", o) .chain ( safeFind ( o => safeProp ("types", o) .map (includes (type)) .getOrElse (false) ) ) .chain (safeProp ("long_name")) 将一堆微小的功能一起弄乱,可能会带来更大的麻烦。当然,可以实现无点语法,但是无数实用程序函数的复杂序列通过 saying 程序实际上应执行的操作几乎没有作用。在3个月内读完pipe后,您还记得您的意图吗?

相比之下,pipegetLocation都是简单明了的。它们不是毫无意义的,但是它们会与读者交流应该完成的工作。此外,域和共域是在 total 中定义的,这意味着我们的getAddress可以与任何其他程序组成并且可以正常工作。好的,让我们来揭示剩余的依赖关系-

transform

const Maybe = require ("data.maybe") const { Nothing, fromNullable } = Maybe const { identity, curryN, find } = require ("ramda") // safeProp : String -> Object -> Maybe a const safeProp = curryN ( 2 , (p = "", o = {}) => Object (o) === o ? fromNullable (o[p]) : Nothing () ) // safeFind : (a -> Boolean) -> [ a ] -> Maybe a const safeFind = curryN ( 2 , (test = identity, xs = []) => fromNullable (find (test, xs)) ) 之上是必需的,因为这些函数具有默认参数。这是对提供更好自我记录功能的折衷。如果删除了默认参数,则可以使用更传统的curryN

因此,让我们来看一下我们的功能。如果输入有效,我们将获得预期结果。–

curry

由于我们的transform (data) .getOrElse ("invalid input") // { latitude: -33.866651 // , longitude: 151.195827 // , city: "Pyrmont" // , zipCode: "2009" // , streetName: "Pirrama Road" // , streetNumber: "48" // } 返回了Maybe,因此我们可以在提供格式错误的输入时轻松恢复。

transform

see the results的副本上运行该程序。

希望这种方法的优点显而易见。由于不仅有了Maybe和transform ({ bad: "input" }) .getOrElse ("invalid input") // "invalid input" transform之类的高级抽象,我们不仅获得了更强大,更可靠的safeProp,而且编写起来也很容易。

在分开之前,让我们考虑一下大型safeFind组成。它们有时会中断的原因是因为Ramda库中的 all 函数并非全部 total –其中一些返回非值pipe。例如,undefined可能会返回head,管道中的下一个函数将收到undefined作为输入。一旦undefined感染了您的管道,所有的安全保证都将消失。另一方面,通过使用专门设计用于处理可为空的值的数据结构,我们消除了复杂性,同时提供了保证。

扩展这个概念,我们可以寻找undefined库或提供我们自己的库。这样做的目的是加强我们在通用模块中的意图。 DecodergetLocation是我们用来使getAddress成为可能的自定义帮助程序-但更笼统地说,它是一种解码器,因此它有助于我们以这种方式进行思考。另外,当遇到错误时,解码器数据结构可以提供更好的反馈-即,代替transform只能向我们发出无法产生值的信号,我们可以附上原因或其他有关特定故障的信息。 decoders npm软件包值得研究。

请参阅Scott的答案以另一种方式使用名为 lens 的高级抽象来解决此问题。但是请注意,该函数是不纯的–需要采取额外的预防措施,以防止该函数针对格式错误的输入引发运行时错误。


Scott的评论提出了一个有效的场景,您可能会想要稀疏结果。我们可以将我们的Nothing类型重新定义为-

Result

当然,这意味着我们必须重新定义type Result = { latitude: Maybe Number , longitude: Maybe Number , city: String , zipCode: String , streetName: String , streetNumber: String } 才能构建此新结构。不过最重要的是,transform的使用者知道共同域的定义很明确。

另一种选择是保留原始的Result类型,但是在找不到纬度或经度值时指定默认值。–

Result
如果选择,const transform = (o = {}) => lift (make) ( getLocation ("lat", o) .orElse (_ => Just (0)) , getLocation ("lng", o) .orElse (_ => Just (0)) , getAddress ("locality", o) , getAddress ("postal_code", o) , getAddress ("route", o) , getAddress ("street_number", o) ) 中的

每个字段可以是可选的。无论哪种方式,我们都必须明确定义域和共域,并确保我们的Result遵守其诺言。这是将它安全地集成到较大程序中的唯一方法。