Статьи

Функциональное программирование с Groovy

Недавно я начал курс Coursera Functional Programming with Scala (преподаваемый Мартином Одерским — создателем Scala), который фактически служит одновременно введением в FP и Scala, но не раньше. Сам курс великолепен, однако попытка просмотра видео и одновременного использования нового синтаксиса Scala и концепций FP может потребовать некоторых усилий.

Я хотел проработать некоторые основные концепции FP в более знакомом контексте, поэтому собираюсь применить некоторые уроки / принципы / упражнения в Groovy.

Функции:

Если вы выполнили какое-либо программирование на Groovy, вы столкнетесь с функциями / замыканиями Groovy. Поскольку Groovy динамически печатается (по сравнению со статической типизацией Scala), вы можете играть в нее довольно быстро и свободно.

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

def sqrt(x: Double): Double = ...

Как вы можете видеть, Scala ожидает ввода значений (кроме того, на самом деле вам не всегда нужно указывать тип возвращаемого значения в Scala). Но в функции Groovy это:

def sqrt = {x-> ... }

Groovy-функция может быть определена и назначена для любой переменной, что позволяет передавать ее как объект первого класса. Если мы посмотрим на полное решение для вычисления квадратного корня числа (используя метод Ньютона — Чтобы вычислить квадратный корень из «x», мы начнем с оценки квадратного корня «y» и продолжим улучшать предположение путем принимая среднее значение х и х / у)

def sqrtIter(guess: Double, x: Double): Double =
  if (isGoodEnough(guess, x)) guess
  else sqrtIter(improve(guess, x), x)
 
def improve(guess: Double, x: Double) =
  (guess + x / guess) / 2
  
def isGoodEnough(guess: Double, x: Double) =
  abs(guess * guess - x) < 0.001
  
def sqrt(x: Double) = srqtIter(1.0, x)

Так что в Scala, если мы попробуем Groovy, мы увидим, что мы можем легко добиться почти того же:

//Improve the guess using Newton's method
def improve = { guess, x ->
  (guess + x / guess) / 2
}
 
//Check if our guess is good enough, within a chosen threshold
def isGoodEnough = {guess, x ->
  abs(guess * guess - x) < 0.001
}
 
//iterate over guesses until our guess is good enough
def sqrtIter = { guess, x ->
  if (isGoodEnough(guess, x)) guess
  else sqrtIter(improve(guess, x), x)
}
  
//wrap everythin in the square root function call
def sqrt = {x -> srqtIter(1.0, x)}

Рекурсия (и хвостовая рекурсия):


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

Если мы снова рассмотрим пример из курса Scala, на примере простой функции, которая вычисляет факториал для данного числа.
def factorial ={ n ->
  if (n == 0) 1 
  else n * factorial(n - 1)
}
 
factorial(4)

Эта простая функция рекурсивно вычисляет факториал, продолжая вызывать себя, пока все числа к нулю не будут учтены. Как вы можете видеть, нет изменяемого состояния — каждый вызов факториальной функции просто принимает входные данные и возвращает выходные данные (значение n никогда не изменяется и не переопределяется, n просто используется для вычисления выходных значений)

Здесь есть проблема, и как только вы попытаетесь вычислить факториал достаточно большого числа, вы столкнетесь с исключением StackOverflow — это происходит потому, что в JVM каждый раз, когда вызывается функция, в кадр добавляется фрейм. стека, поэтому рекурсивная работа довольно проста, чтобы достичь предела стека и столкнуться с этой проблемой. Обычный способ решить эту проблему — использовать рекурсию Tail-Call. Этот трюк состоит в том, чтобы иметь последний код, который оценивается в функции как рекурсивный вызов — обычно на языках FP компилятор / интерпретатор распознает этот шаблон, а на самом деле он просто запускает код как цикл (например, если мы знаем, что самый последний фрагмент кода в блоке кода вызывает сам себя, это на самом деле не так уж и отличается от простого наличия блока кода / функции внутри конструкции цикла)

В предыдущем факториальном примере может показаться, что последний код, который должен быть выполнен, является рекурсивным callfactorial (n-1) — однако значение этого вызова фактически возвращается в функцию и  затем умножается на n — так что фактически последний кусок кода, который должен оцениваться при вызове функции, на самом деле является n * возвращаемым значением факториала (n-1).
Давайте посмотрим, как переписать функцию, чтобы она была рекурсивной.

def factorial ={ n, accumulator=1 ->
  if (n == 1) accumulator 
  else factorial(n-1, n*accumulator)
}
 
factorial(4)

Теперь, используя аккумулятор, последним кодом, который должен быть оценен в функции, является наш рекурсивный вызов функции. В большинстве языков FP, включая Scala, этого достаточно — однако JVM автоматически не поддерживает рекурсию хвостового вызова, поэтому вам действительно нужно использовать более грубый подход в Groovy:

def factorial ={ n, accumulator=1 ->
  if (n == 1) accumulator 
  else factorial.trampoline(n-1, n*accumulator)
}.trampoline()
factorial(4)

Использование метода trampoline () означает, что функция теперь будет вызываться с использованием рекурсии хвостового вызова, поэтому никогда не должно возникать исключение StackOverflow. Это не так хорошо, как в Scala или других языках, но поддержка есть, поэтому мы можем продолжить.

Карринг:

Это похоже на композицию функций — идея заключается в том, что вы берете обобщенную функцию, а затем вы указываете ее на какое-то значение, чтобы сделать более конкретное применение функции. Например, если мы посмотрим на функцию с заданными значениями x и y, она возвращает z, представляющее собой значение x процентов от y (например, учитывая x = 10, y = 100, возвращается 10 процентов от 100, z = 10)

def percentage = { percentage, x ->
  x/100 * percentage
}

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

def tenPercent = percentage.curry(10)

Теперь, если вызывается функция tenPercent (x), она использует оригинальную функцию процент (), но каррирует значение 10 в качестве первого аргумента. (Если вам нужно каррировать другие позиции аргументов, вы также можете использовать функцию rcurry () для каррирования наиболее правого аргумента или ncurry (), которая также занимает позицию аргумента — для получения дополнительной информации обратитесь к документации Groovy по каррированию). 

Неизменность:

Неизменность частично поддерживается в Java обычно с использованием ключевого слова final (то есть переменные не могут быть изменены после первоначальной установки при создании объекта). Groovy также предоставляет быструю и простую аннотацию @Immutable, которую можно добавить в класс, чтобы сделать его неизменным. Но на самом деле, избегать неизменяемого состояния — это больше, чем просто иметь классы как неизменяемые — поскольку у нас есть функции в качестве объектов первого класса, мы можем легко назначать переменные и изменять их внутри функции — так что это скорее ваше мышление или философия чтобы привыкнуть к. Например:

def list = ['groovy', 'functional']
//This mutates the original list
list.add('programming')
//This creates a new list leaving the original unchanged
def newList = list.plus(2, 'programming')

Первый пример, вероятно, больше похож на код на Groovy / Java, который мы привыкли писать, но это изменяет состояние списка — когда второй подход оставляет исходный список неизменным.

Уменьшение карты:


В заключение отметим, что в FP есть некоторые функции, которые являются довольно распространенными методами — наиболее известной из которых в наши дни (отчасти благодаря Google) является Map-Reduce, но трио функций на самом деле Map, Reduce (также известное как Сложите) и Фильтр — вы можете прочитать больше о функциях 
здесь  (или просто погуглить их!), Но эти функции на самом деле очень хорошо коррелируют с основными функциями Groovy, которые вы, вероятно, часто используете (если вы программисты на Groovy).

карта

Карта является самой легкой для понимания из трех. Он принимает два входа — функцию и список. Затем он применяет эту функцию к каждому элементу в списке. Однако вы можете сделать то же самое с пониманием списка. 

Звучит знакомо? Это в основном функция .collect {} в Groovy

Уменьшить / Fold


Это немного сложнее для описания, но это то же самое, что и функция .inject {} в groovy.

Фильтр


И еще один простой способ — отфильтровать список для желаемых элементов, это функция Groovy .findAll {}

Как я уже сказал в начале, я новичок в FP и пришел из OO, но, надеюсь, вышесказанное не так уж далеко от истины!
По мере прохождения курса Coursera я попытаюсь опубликовать его снова, возможно, с некоторыми попытками в Groovy, чтобы понять, как это работает.