Почему я люблю подбор шаблонов

Эту статью также можно прочитать в моем федеративном блоге WriteFreely.

Почему я люблю сопоставление шаблонов

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

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

Т.е:

cumlativeSums([1, 2, 3, 4, 5]) -> [1, 3, 6, 10, 15]
Войти в полноэкранный режим Выйти из полноэкранного режима

Довольно стандартный вопрос для оценки кодирования. Обманчиво простой, но не слишком сложный, чтобы вы не смогли его решить, если не знаете заранее. Кроме того, для него существует множество решений. Посмотрите этот вопрос на Stack Overflow для вдохновения.

JavaScript

Карри 🍛

Теперь, безусловно, самый крутой метод, который вы можете сделать — это использовать родную функцию map с Currying.

function sumArrayCurry(arr) {
  return arr.map(
    (
      (sum) => (value) =>
        (sum += value)
    )(0)
  )
}
Вход в полноэкранный режим Выход из полноэкранного режима

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

Array.prototype.reduce.

Метод, который пришел мне на ум, когда я впервые прочитал вопрос, заключался в использовании <some array>.reduce. Прочитав вопрос, я понял, что мне придется что-то делать с каждым элементом массива, а затем возвращать новый массив, содержащий результирующие значения.

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

function sumArrayReduce(arr) {
  const sums = []

  arr.reduce((prev, cur, index) => {
    return (sums[index] = prev + cur)
  }, 0)

  return sums
}
Вход в полноэкранный режим Выход из полноэкранного режима

Мне это нравится, потому что легко следить за логикой программиста и течением программы, а если вы не понимаете, что делает программа, вы можете легко посмотреть, что делает reduce. Единственная особенность этого решения заключается в том, что оно опирается на собственные функции JavaScript. На любом собеседовании по кодингу (а это, будем честны, единственная ситуация, где это может всплыть) вас, скорее всего, попросят не использовать родной API.

Рекурсия

Как я уже упоминал, я недавно перешел на Elixir. Я только что открыл для себя любовь к функциональному программированию после долгих лет ненависти, вызванной жестоким обращением со мной Scheme во время учебы в университете. Поскольку Elixir-решение, вероятно, будет использовать что-то с рекурсией, я хотел использовать это, не завися от родной функции JavaScript reduce.

function sumArrayRecursive(arr) {
  return sumArrayHelper(0, 0, [], arr)
}

function sumArrayHelper(prevSum, index, sums, arr) {
  if (!arr.length) {
    return sums
  }

  const curSum = arr[index] + prevSum
  sums.push(curSum)
  arr.shift()

  return sumArrayHelper(curSum, index++, sums, arr)
}
Вход в полноэкранный режим Выход из полноэкранного режима

Это решение действительно опирается на часть родного API, но оно исключает reduce. Оно также следует шаблону tail-recursive, хотя это мало что значит в современном мире JavaScript (Safari — единственный браузер, который поддерживает правильные вызовы tail source).

Красивый Elixir

Elixir делает функциональное программирование осмысленным и приятным благодаря таким вещам, как согласование шаблонов и хвостовая рекурсия. Особенно мне нравится согласование шаблонов. Для тех, кто не знаком с pattern matching, это означает то же самое, что и звучит: вы можете делать вещи, основываясь на том, как они выглядят. Это довольно распространено, когда речь идет о таких вещах, как случаи, условные операторы или, в нашем случае, определения функций.

defmodule ListHelper do
  def cumlative_sum(list) do
    p_cumlative_sum(0, [], list)
  end

    # 1
  defp p_cumlative_sum(_prev_sum, sums, []), do: Enum.reverse(sums)

    # 2
  defp p_cumlative_sum(prev_sum, sums, [head | tail]) do
    p_cumlative_sum(prev_sum + head, [prev_sum + head | sums], tail)
  end
end
Вход в полноэкранный режим Выход из полноэкранного режима

Здесь я создаю модуль под названием ListHelper только для того, чтобы я мог запустить программу внутри iex (интерактивный Elixir). Я определяю одну публичную функцию cumlative_sum/1, которая будет принимать список (в Elixir нет традиционных «массивов», только связанные списки). Я также определяю две частные функции для обработки рекурсии p_cumlative_sum/3. Эти частные функции имеют одинаковое имя и одинаковое количество параметров, но их отличает шаблон, по которому они работают.

Третий параметр определен как список. #1 p_cumlative_sum/3 будет соответствовать только тогда, когда третий аргумент является пустым списком, тогда как #2 будет соответствовать только тогда, когда список не пуст. Такое поведение аналогично рекурсивному решению JavaScript, где мы проверяем длину списка, прежде чем приступить к выполнению логических действий if(!arr.length) {...}.

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

Побочные эффекты

Также, на заметку, данные в Elixir неизменяемы. Это означает отсутствие побочных эффектов. В приведенном выше рекурсивном решении JavaScript есть очевидная проблема. Это вызов arr.shift(). Массив, переданный в функцию, будет изменен во время выполнения функции. Это означает, что после возвращения функции массив, который вы передали ей, будет пуст.

Побочные эффекты были моей самой большой проблемой при переходе от JavaScript к Elixir и обратно. Я хочу писать функционально, но несоответствия в JavaScript и все побочные эффекты, которые появляются, делают это очень трудным.

Резюме

Я не совсем понимаю, в чем смысл этого занятия, но я получил удовольствие, играя с обоими языками и решая простой алгоритм. Я ни в коем случае не эксперт в JavaScript или Elixir, и я не тратил слишком много времени на оптимизацию своих решений, так что воспринимайте мой код с некоторым 🧂 и 🌶.

Не стесняйтесь оставлять свои собственные решения вопроса или даже улучшать мои. Я уверен, что есть способ использовать рекурсию в JavaScript без необходимости использовать Array.prototype.shift, или даже способ убрать Enum.reverse/1 в решении Elixir.

Спасибо за прочтение! 👨💻

Оцените статью
Procodings.ru
Добавить комментарий