Ramda JS лучший способ получить самую узкую географическую привязку

Я пытаюсь использовать API Google Адресов, чтобы получить название места, в котором я нахожусь.

Возвращенная структура данных имеет следующие типы:

descriptor1: 'street number' | 'neighborhood' | 'postcode' | 'route' | 'locality' | 'postal_town' | 'administrative_area_level_2' | 'administrative_area_level_1' | 'country'

places: [
  {
    address_components: [{
      long_name: 'string',
      short_name: 'string',
      types: {
        0: descriptor1,
        1?: descriptor2
      }
    }],
    other_fields not relevant here
    }
]

Нет никакой гарантии, сколько адресных компонентов будет в том или ином месте, и есть ли у них какие-либо из них. Нет гарантии, какие типы будут и не будут представлены.

Я хотел бы написать код, который возвращает long_name первого address_component, который имеет поле R.get(R.lensPath('types', '0')) из 'neighborhood' если таковой существует locality иначе тогда postal_town, administrative_area_level_2, затем administrative_area_level_1 а потом country,

Итак, я начинаю с R.pluck('address_components', places), Теперь я могу создать объект, сократить список до объекта, вставить первый из каждого интересующего меня ключа в объект и затем найти значение. что-то вроде:

const interestingTypes = ['neighborhood', 'locality', 'postal_town', 'administrative_area 2', 'administrative_area_1', 'country']
const res = R.mergeAll(R.pluck('address_components', places).map((addressComponentList) => addressComponentList.reduce((memo, addressComponent) => {
     if (interestingTypes.indexOf(addressComponent.types[0]) !== -1) {
       if (!memo[addressComponent.types[0]]) {
         memo[addressComponent.types[0]] = addressComponent.long_name
       }  
     }
     return memo
   },{})))
res[R.find((type) => (Object.keys(res).indexOf(type) !== -1), interestingTypes)]

Хотя это, безусловно, правда, что это можно сделать чуть более идиоматичным, заменяя все родное .reduce а также .map с R.map/R.reduce это на самом деле не решает фундаментальных проблем.

1) Это будет повторять каждый элемент списка даже после нахождения результата.

2) Результирующая структура все еще должна быть повторена (например, с поиском), чтобы на самом деле найти самую узкую границу.

Как бы выглядела чистая функциональность, желательно ленивая реализация? Какие особенности Рамды могут пригодиться? Могу ли я использовать линзы для этого каким-то образом? Состав функции? Что-то другое?

И это нормально, чтобы смешивать и сочетать родной map/reduce с рамдой? Конечно, нативные вызовы лучше, чем библиотечные вызовы, когда это возможно?

2 ответа

Одним из подходов было бы создать ленивую версию R.reduceRight:

const lazyReduceR = R.curry((fn, acc, list) => {
  function _lazyReduceR(i) {
    return i === list.length
      ? acc
      : fn(list[i], () => _lazyFoldR(i + 1))
  }
  return _lazyReduceR(0)
})

Затем это можно использовать для создания функции, которая найдет минимальный элемент (непустого) списка с известной нижней границей:

const boundMinBy = R.curry((byFn, lowerBound, list) =>
  lazyReduceR((x, lzMin) => {
    if (byFn(x) === lowerBound) {
      return x;
    } else {
      const min = lzMin()
      return byFn(x) < byFn(min) ? x : min
    }
  }, list[0], R.tail(list)))

Если нижняя граница когда-либо встречается, рекурсия останавливается и немедленно возвращает этот результат.

С boundMinBy доступно, мы можем создать таблицу поиска типов адресов для сортировки значений заказа:

const sortOrder = {
  neighborhood: 0,
  locality: 1,
  postal_town: 2,
  administrative_area_level_2: 3,
  administrative_area_level_1: 4,
  country: 5
}

Наряду с функцией, которая будет генерировать значение порядка сортировки для данного компонента адреса:

const sortOrderOfAddress = address => sortOrder[address.types[0]]

И тогда мы можем составить его вместе с конвейером, таким как:

const process = R.pipe(
  R.prop('places'),
  R.chain(R.pipe(
    R.prop('address_components'),
    R.unless(
      R.isEmpty,
      R.pipe(
        boundMinBy(sortOrderOfAddress, 0),
        R.prop('long_name'),
        R.of
      )
    )
  ))
)

R.chain используется выше, чтобы объединить адреса всех мест и отфильтровать любые адреса мест, где address_components пустой.

Я включил пример во фрагмент ниже, если вы хотите проверить его с некоторыми данными.

const lazyReduceR = R.curry((fn, acc, list) => {
  function _lazyReduceR(i) {
    return i === list.length
      ? acc
      : fn(list[i], () => _lazyReduceR(i + 1))
  }
  return _lazyReduceR(0)
})

const boundMinBy = R.curry((byFn, lowerBound, list) =>
  lazyReduceR((x, lzMin) => {
    if (byFn(x) === lowerBound) {
      return x;
    } else {
      const min = lzMin()
      return byFn(x) < byFn(min) ? x : min
    }
  }, list[0], R.tail(list)))

const sortOrder = {
  neighborhood: 0,
  locality: 1,
  postal_town: 2,
  administrative_area_level_2: 3,
  administrative_area_level_1: 4,
  country: 5
}

const sortOrderOfAddress = address => sortOrder[address.types[0]]

const process = R.pipe(
  R.prop('places'),
  R.chain(R.pipe(
    R.prop('address_components'),
    R.unless(
      R.isEmpty,
      R.pipe(
        boundMinBy(sortOrderOfAddress, 0),
        R.prop('long_name'),
        R.of
      )
    )
  ))
)

////

const data = {
  places: [{
    address_components: [{
      long_name: 'a',
      types: ['country']
    }, {
      long_name: 'b',
      types: ['neighborhood']
    }, {
      long_name: 'c',
      types: ['postal_town']
    }]
  }, {
    address_components: [{
      long_name: 'd',
      types: ['country']
    }, {
      long_name: 'e',
      types: ['locality']
    }, {
      long_name: 'f',
      types: ['administrative_area_level_2']
    }]
  }]
}

console.log(process(data))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.23.0/ramda.min.js"></script>

И это нормально, чтобы смешивать и сопоставлять родную карту / уменьшить с рамдой?

Абсолютно. Но вы также должны учитывать когнитивные издержки, связанные с их сочетанием.

Конечно, нативные вызовы лучше, чем библиотечные вызовы, когда это возможно?

Лучше как? Библиотечные функции разработаны, чтобы избежать некоторой прискорбной сложности в спецификациях для нативных функций.

Более того, когда были написаны основные функции Ramda, они имели значительно лучшую производительность, чем их собственные аналоги. Это могло измениться по мере того, как собственные движки развивались и функции Ramda стали более сложными. Но также вполне возможно, что это не так.

Другие вопросы по тегам