Рамда: сложить объект - PullRequest
6 голосов
/ 04 июня 2019

Я строю PWA и строю по логике с Рамдой. Я пытаюсь создать функцию, которая при ответе Google Places Detail возвращает пользовательский объект адреса.

Позвольте мне описать это в коде, показывая вам мой тест:

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

Как видите, мой желаемый адресный объект более "плоский" (из-за отсутствия лучшего термина). Я изо всех сил пытаюсь написать эту функцию преобразования. Я попытался сделать это, используя evolve Рамды, но это сохраняет ключи. Мне нужно преобразовать объект, используя evolve, а затем reduce объект, распространяющий ключи.

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

Я успешно извлек соответствующую информацию, используя evolve, и переименовал ключи, используя renameKeys из дополнения Ramda, но я не могу понять, как впоследствии сгладить этот объект. Как ты это делаешь? Или, может быть, есть еще более простой способ достичь желаемой трансформации?

Edit:

Я нашел способ добиться своего преобразования, но он очень многословен. Я чувствую, что есть более простой способ извлечь адресные данные. В любом случае, вот мое текущее решение:

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 ]

3 голосов
/ 04 июня 2019

Объективы, вероятно, являются лучшим выбором для этого. У Ramda есть общая lens функция и специальные функции для свойства объекта (lensProp), для индекса массива (lensIndex), и для более глубокого пути (lensPath), но он не включает один, чтобы найти совпадающее значение в массиве по id. Но сделать свое дело несложно.

Объектив создается путем передачи двух функций в lens: метод получения, который принимает объект и возвращает соответствующее значение, и метод установки, который принимает новое значение и объект и возвращает обновленную версию объекта.

Здесь мы пишем 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 для этой проблемы, так как мы не используем установщик:

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. Это оставлено в качестве упражнения для читателя.


(Введение к этому было едва изменено из более раннего ответа моего.)


Обновление

Я также попробовал совершенно независимый подход. Я думаю, что это менее читабельно, но, вероятно, более производительно. Интересно сравнить варианты.

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>

Эта версия разбивает задачу на две части: одну для более простых полей, latitude и longitude, а другую для других, которые сложнее сопоставить, а затем просто объединяет результат применения каждого из них к ответу. .

Более простые поля не требуют комментариев. Это просто простое приложение applySpec и path. Другой, инкапсулированный как matchType, принимает спецификации, соответствующие типам на входе (и имени поля для извлечения), именам свойств для вывода. Он строит индекс desc, основанный на типах (здесь используется JSON.stringify, хотя есть очевидные альтернативы). Затем он уменьшает массив объектов, которые находят любой объект, свойство types которого находится в индексе, и связывает его значение с соответствующим именем поля.

Это интересный вариант. Я все еще предпочитаю свой оригинал, но для больших массивов это может существенно повлиять на производительность.

Еще одно обновление

Прочитав ответ от пользователя 633183, я подумал о том, как бы я хотел использовать что-то подобное. Здесь можно много чего сказать об использовании Maybe s. Но есть два разных способа, которыми я бы хотел взаимодействовать с результатами. Один позволяет мне работать по полю, каждый из которых обернут в свой 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 для массивов и других складных типов.(Рамда, по длинным, сложным причинам , не рассматривает Объекты как складные.)

В остальном это очень похоже на ответ от @ user633183, но написано в моих собственных идиомах.Вероятно, единственная другая часть, на которую стоит обратить внимание, - это maybeSpec, которая во многом похожа на R.applySpec, но оборачивает каждое поле в Just или Nothing.

(обратите внимание, что яЯ использую Maybe от Ramda-Fantasy . Этот проект был прекращен, и я, вероятно, должен был выяснить, какие изменения потребовались для использования одного из современных проектов там.это лень. Единственное, что требуется, я думаю, это заменить вызовы Maybe на любую функцию, которую они предлагают [или вашу собственную] для преобразования значений nil в Nothing и всех остальных в Just s.)

2 голосов
/ 06 июня 2019

Сила функционального стиля зависит от гарантии , что обе функции принимают значение как input и возвращают значение как output . Если вывод функции не является полностью определенным, любой потребитель вывода нашей функции подвержен потенциально неопределенному поведению. Нулевые проверки утомляют писать, а исключения во время выполнения являются мигренью; мы можем избежать обоих, придерживаясь функциональной дисциплины.

Проблема, представленная в вашем вопросе, является нетривиальной. Данные для извлечения глубоко вложены, и для доступа к компонентам адреса требуется эксцентричный поиск и сопоставление. Чтобы начать писать наше преобразование, мы должны полностью определить домен нашей функции (входной) и кодомен (выходной).

Домен прост: входные данные в вашем вопросе являются объектами, поэтому наше преобразование должно дать действительный результат для всех объектов. Кодомен более специфичен - поскольку преобразование может завершиться с ошибкой любым количеством способов, наша функция будет возвращать либо действительный объект результата, либо , либо . Ничего.

В качестве сигнатуры типа вот как это выглядит -

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, поэтому не имеет никаких проблем, связанных с использованием null или undefined - например, NullPointerException или 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

Хорошо, yikes. Мы даже не закончили, и эти вложенные .chain звонки - полный беспорядок! Если вы посмотрите внимательно, здесь есть простой шаблон. Функциональная дисциплина говорит, что когда вы видите шаблон, вы должны аннотация ; это слово ботаник, означающее сделать функцию .

Прежде чем мы углубимся в ад .chain, давайте рассмотрим более обобщенный подход. Я должен найти шесть (6) возможных значений в глубоко вложенном объекте, и если я смогу получить все из них, я хочу построить значение 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

Правильность восстановлена. Выше мы пишем простую функцию make, которая принимает шесть (6) аргументов для построения Result. Используя lift, мы можем применить make в контексте из Maybe, отправив значения Maybe в качестве аргументов. Однако, если любое значение равно Nothing, в результате мы ничего не получим, и make не будет применено.

Большая часть тяжелой работы уже выполнена здесь. Нам просто нужно завершить реализацию getAddress и 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

У нас не было safeProp до того, как мы начали, но мы облегчаем себе жизнь, изобретая удобство по мере продвижения вперед. Функциональная дисциплина говорит, что функции должны быть простыми и выполнять одну задачу. Такие функции легче писать, читать, тестировать и поддерживать. Они имеют дополнительное преимущество, заключающееся в том, что они могут быть составлены и более пригодны для повторного использования в других областях вашей программы. Кроме того, когда функция имеет имя , она позволяет нам кодировать наши намерения более напрямую - getLocation - это последовательность 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"))

Иногда скомбинирование множества крошечных функций с использованием pipe может доставить больше хлопот, чем оно того стоит. Конечно, синтаксис без точки может быть достигнут, но сложные последовательности бесчисленных служебных функций мало что делают для , говоря , что на самом деле должна делать программа. Когда вы прочитаете это pipe через 3 месяца, вспомните ли вы о своих намерениях?

Напротив, и getLocation, и getAddress просты и понятны. Они не бесполезны, но они сообщают читателю, какую работу предполагается выполнить. Кроме того, домен и кодомен определены в total , что означает, что наш 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"
// }

И поскольку наш transform возвращает значение Maybe, мы можем легко восстановить, если предоставлен неправильный ввод -

transform ({ bad: "input" }) .getOrElse ("invalid input")
// "invalid input"

Запустите эту программу на repl.it до см. Результаты .

Надеюсь, преимущества этого подхода очевидны. Мало того, что мы получили более надежный и надежный transform, его было легко писать благодаря высокоуровневым абстракциям, таким как Maybe, safeProp и safeFind.

И давайте подумаем об этих больших pipe композициях, прежде чем мы расстанемся. Причина, по которой они иногда ломаются, заключается в том, что не все функции в библиотеке Ramda являются total - некоторые из них возвращают ненулевое значение, undefined. Например, head может потенциально возвратить undefined, и следующая функция в конвейере получит undefined в качестве ввода. Как только undefined заражает ваш трубопровод, все гарантии безопасности теряются. С другой стороны, используя структуру данных, специально разработанную для обработки значений, допускающих обнуляемость, мы снимаем сложность и в то же время предоставляем гарантии.

Развивая эту концепцию, мы могли бы найти библиотеку Decoder или предоставить нашу собственную. Целью этого было бы укрепление наших намерений в общем модуле. getLocation и getAddress - это пользовательские помощники, которые мы использовали, чтобы сделать возможным transform, но в более общем смысле это форма декодера, поэтому нам помогает думать об этом таким образом. Кроме того, структура данных декодера может обеспечить лучшую обратную связь при возникновении ошибок - то есть вместо Nothing, который только сигнализирует нам о том, что значение не может быть получено, мы можем приложить причину или другую информацию, касающуюся конкретного сбоя. Пакет декодеров npm заслуживает внимания.

см. ШотландецОтвет для решения этой проблемы другим способом с использованием высокоуровневой абстракции под названием lens .Тем не менее, обратите внимание, что функция нечиста - необходимы дополнительные меры предосторожности, чтобы функция не генерировала ошибки времени выполнения для искаженных входных данных.


Комментарий Скотта представляет допустимый сценарий, в котором вы можете хотеть разреженныйрезультат.Мы можем переопределить наш тип 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 может быть необязательным, если вы того пожелаете.В любом случае, мы должны четко определить домен и кодомен и убедиться, что наш transform выполняет свое обещание.Это единственный способ, которым можно безопасно включить в большую программу.

2 голосов
/ 04 июня 2019

Вот как это сделать простым 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
});

Рамда может быть полезен, но давайте не будем увлекатьсянаписание тупого кода ради использования функциональной библиотеки, если простой JavaScript может добиться цели с меньшим количеством кода, который также легче читать.

РЕДАКТИРОВАТЬ: После прочтения ответа @ user3297291 я должен признать, что его решение Ramda довольно элегантно, но мои очки все еще стоят.Никогда не пишите больше кода, если вы можете писать меньше, сохраняя читабельность.

Решение для stackblitz

2 голосов
/ 04 июня 2019

Возможно, не так много улучшений, но у меня есть несколько предложений:

  • Вы можете использовать indexBy вместо (трудно читаемой) встроенной функции уменьшения.
  • Разделив логику адреса и местоположения и сделав составного помощника для объединения двух, легче читать, что происходит (используя juxt и 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}}}}; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
...