Функция бессмысленного уменьшения с аккумулятором в качестве окончательного аргумента - Функциональное программирование - Javascript - Immutable.js - PullRequest
0 голосов
/ 30 сентября 2018

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

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

import { curry, get, omit, pipe, set, reduce } from 'lodash/fp'

const mv = curry(
  (oldPath, newPath, source) =>
    get(oldPath, source)
      ? pipe(
          set(newPath, get(oldPath, source)),
          omit(oldPath)
        )(source)
      : source
)

test('mv', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { a: 'z', q: 'y', c: 'x' }
  const result = mv('b', 'q', largeDataSet)

  expect(result).toEqual(expected)
})

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

test('mvMore', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { a: 'z', q: 'y', m: 'x' }
  const keysToRename = [['b', 'q'], ['c', 'm']]

  const result = reduce(
    (acc, [oldPath, newPath]) => mv(oldPath, newPath, acc),
    largeDataSet,
    keysToRename
  )

  expect(result).toEqual(expected)
})

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

test('pipe mvMore and similar transforms', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { u: 'z', r: 'y', m: 'x' }
  const keysToRename = [['b', 'q'], ['c', 'm']]
  const keysToRename2 = [['q', 'r'], ['a', 'u']]
  const mvCall = (source, [oldPath, newPath]) => mv(oldPath, newPath, source)
  const reduceAccLast = curry((fn, it, acc) => reduce(fn, acc, it))

  const result = pipe(
    // imagine other similar transform
    reduceAccLast(mvCall, keysToRename),
    // imagine other similar transform
    reduceAccLast(mvCall, keysToRename2)
  )(largeDataSet)

  expect(result).toEqual(expected)
})

Мой вопрос заключается в том, является ли это антипаттерномкакой-то, или если есть лучший способ достичь того же результата.Меня пугает то, что обычно аргумент-накопитель функции-редуктора используется как внутреннее состояние, а набор данных повторяется;однако здесь все наоборот.Большинство итеративных функций-редукторов будут мутировать аккумулятор с пониманием того, что он используется только для внутреннего использования.Здесь набор данных передается от редуктора к редуктору в качестве аргумента накопителя, потому что нет смысла перебирать большой набор данных, где есть только списки из нескольких операций, которые нужно выполнить с набором данных.Пока функции итераторов редуктора, например, mv, не изменяют аккумулятор, есть ли проблемы с этим шаблоном или есть что-то простое, что мне не хватает?


Основано на ответе @ tokland Iпереписал тесты для использования Immutable.js, чтобы посмотреть, стоили ли гарантии неизменности и потенциального прироста производительности.В интернете была некоторая шумиха о том, что Immutable.js не подходит для функционального программирования в стиле без точек.В этом есть доля правды;впрочем, не сильно.Из того, что я могу сказать, все, что нужно сделать, это написать несколько основных функций, которые вызывают методы, которые вы хотите использовать, например, map, filter, reduce.Функции Lodash, которые не работают с массивами Javascript или объектами, все еще могут использоваться;другими словами, функции Lodash, которые имеют дело с функциями, такими как curry и pipe, или со строками, такими как upperCase, кажутся хорошими.

import { curry, pipe, upperCase } from 'lodash/fp'
import { Map } from 'immutable'

const remove = curry((oldPath, imm) => imm.remove(oldPath))
const get = curry((path, imm) => imm.get(path))
const set = curry((path, source, imm) => imm.set(path, source))
const reduce = curry((fn, acc, it) => it.reduce(fn, acc))
const reduceAcc = curry((fn, it, acc) => reduce(fn, acc, it))
const map = curry((fn, input) => input.map(fn))

const mv = curry((oldPath, newPath, source) =>
  pipe(
    set(newPath, get(oldPath, source)),
    remove(oldPath)
  )(source)
)

const mvCall = (acc, newPath, oldPath) => mv(oldPath, newPath, acc)

function log(x) {
  console.log(x)
  return x
}

test('mv', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', c: 'x' })
  const result = mv('b', 'q', largeDataSet)

  expect(result).toEqual(expected)
})

test('mvMore', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', m: 'x' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const result = reduce(mvCall, largeDataSet, keysToRename)

  expect(result).toEqual(expected)
})

test('pipe mvMore and similar transforms', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ u: 'Z', r: 'Y', m: 'X' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const keysToRename2 = Map({ q: 'r', a: 'u' })

  const result = pipe(
    reduceAcc(mvCall, keysToRename),
    reduceAcc(mvCall, keysToRename2),
    map(upperCase)
  )(largeDataSet)

  const result2 = keysToRename2
    .reduce(mvCall, keysToRename.reduce(mvCall, largeDataSet))
    .map(upperCase)

  expect(result).toEqual(expected)
  expect(result2).toEqual(expected)
})

Кажется, что у Typescript есть некоторые проблемы с обработкойФункции упорядочения, поэтому вы должны увеличить // @ts-ignore до pipe, если вы тестируете с tsc.

Ответы [ 3 ]

0 голосов
/ 01 октября 2018

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

Тем не менее, ваш код может иметь проблемы с производительностью, в зависимости от размера объектов (ввод, сопоставления клавиш).Каждый раз, когда вы меняете ключ, вы создаете новый объект.Если вы видите, что это проблема, вы обычно используете эффективную неизменяемую структуру, которая повторно использует данные для ввода (необязательно для отображений, поскольку вы не обновляете их).Посмотрите, например, на Карта из immutable.js.

0 голосов
/ 01 октября 2018

Основываясь на ответе @ tokland, я переписал тесты для использования Immutable.js, чтобы посмотреть, стоят ли гарантии неизменности и потенциального прироста производительности.В интернете была некоторая шумиха о том, что Immutable.js не подходит для функционального программирования в стиле без точек.В этом есть доля правды;впрочем, не сильно.Из того, что я могу сказать, все, что нужно сделать, это написать несколько основных функций, которые вызывают методы, которые вы хотите использовать, например, map, filter, reduce.Функции Lodash, которые не работают с массивами Javascript или объектами, все еще могут использоваться;другими словами, функции Lodash, которые имеют дело с функциями, такими как curry и pipe, или со строками, такими как upperCase, кажутся нормальными.Функции упорядочения, поэтому вы должны выбросить // @ts-ignore с до pipe, если вы тестируете с tsc.

0 голосов
/ 30 сентября 2018

Я думаю, что ответ на ваш вопрос - да и нет.Под функциональным программированием я имею в виду, что чистые функции - это вещь, и вы пытаетесь сделать это функциональным образом, но изменяете входные данные.Поэтому я думаю, что вам нужно рассмотреть возможность использования метода конвертации , аналогичного тому, как lodash/fp делает это:

Хотя модули lodash / fp и его методы поставляются предварительно конвертированными, естьвремена, когда вы можете захотеть настроить конверсию.Вот тогда пригодится метод преобразования.

// Каждый параметр по умолчанию true.

var _fp = fp.convert({
  // Specify capping iteratee arguments.
  'cap': true,
  // Specify currying.
  'curry': true,
  // Specify fixed arity.
  'fixed': true,
  // Specify immutable operations.
  'immutable': true,
  // Specify rearranging arguments.
  'rearg': true
});

Обратите внимание на конвертер immutable.Так что это часть yes моего ответа ... но часть no будет означать, что вам по-прежнему нужен подход immutable по умолчанию, чтобы он был действительно чистым / функциональным.

...