Как реализовать композицию функций?

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

Базовая идея

Композиция функций позволяет объединить две или более функций, чтобы получить новую функцию. Если есть:

f: B => C

g: A => B

То их композиция — это функция:

f ∘ g: A => C

Это означает: сначала применяется g, затем результат передаётся в f.

В Scala существуют два основных способа композиции:

1. andThen

val f: Int => Int = x => x + 1
val g: Int => Int = x => x \* 2
val h: Int => Int = f andThen g

Здесь h(x) = g(f(x))

h(3) // f(3) = 4; g(4) = 8 → результат: 8

2. compose

val f: Int => Int = x => x + 1
val g: Int => Int = x => x \* 2
val h: Int => Int = f compose g

Здесь h(x) = f(g(x))

h(3) // g(3) = 6; f(6) = 7 → результат: 7

Отличие andThen и compose

Оператор Последовательность вызова Формула
f andThen g g(f(x)) результат f(x) → в g
--- --- ---
f compose g f(g(x)) результат g(x) → в f
--- --- ---

Пример с преобразованием строки

val trim: String => String = \_.trim
val toUpper: String => String = \_.toUpperCase
val addDot: String => String = _ + "."
val pipeline: String => String = trim andThen toUpper andThen addDot
pipeline(" hello ") // "HELLO."

Композиция с произвольными типами

val parse: String => Int = \_.toInt
val isEven: Int => Boolean = _ % 2 == 0
val parseAndCheckEven: String => Boolean = parse andThen isEven
parseAndCheckEven("42") // true

Композиция методов

Методы можно тоже компоновать, если их преобразовать в функции:

def double(x: Int): Int = x \* 2
def addOne(x: Int): Int = x + 1
val composed = (double \_).andThen(addOne \_)
composed(5) // (5 \* 2) + 1 = 11

(double _) означает "преобразуй метод double в функцию".

Композиция с помощью каррирования

def multiply(x: Int)(y: Int): Int = x \* y
val double: Int => Int = multiply(2)
val triple: Int => Int = multiply(3)
val composed: Int => Int = double andThen triple
composed(4) // (2 \* 4) = 8  (3 \* 8) = 24

Композиция коллекционных функций

Часто композиция используется в map, filter, flatMap и других трансформациях:

val names = List(" anna", "John ", " mike ")
val normalize: String => String = \_.trim.toLowerCase.capitalize
val result = names.map(normalize)
// List("Anna", "John", "Mike")

Если нормализация включает несколько шагов, их можно заранее скомпоновать:

val trim: String => String = \_.trim
val lower: String => String = \_.toLowerCase
val capitalize: String => String = \_.capitalize
val normalize = trim andThen lower andThen capitalize

Композиция через custom combinators

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

def combine\[A, B, C\](f: B => C, g: A => B): A => C = f compose g
val square: Int => Int = x => x \* x
val half: Int => Double = x => x / 2.0
val combined = combine(half, square) // x => half(square(x))
combined(4) // square(4) = 16; half(16) = 8.0

Композиция как объектно-функциональный стиль

В Scala функции — это объекты, и можно комбинировать их точно так же, как и классы:

val inc: Int => Int = _ + 1
val dbl: Int => Int = _ \* 2
val transform: Int => Int = inc andThen dbl
transform(3) // (3 + 1) \* 2 = 8

Можно также определять композиции более декларативно:

def pipeline(x: Int): Int =
(x + 1)
.pipe(_ \* 2)
.pipe(_ + 3)
pipeline(4) // ((4 + 1) \* 2) + 3 = 13

Композиция с Function.chain

Для последовательного применения списка функций:

val fns: List\[String => String\] = List(\_.trim, \_.toUpperCase, _ + "!")
val chain = fns.reduce(_ andThen \_)
chain(" hello ") // "HELLO!"

Использование библиотеки Cats или Scalaz

В продвинутых библиотеках есть абстракции, которые позволяют компоновать функции не только по типу A => B, но и эффекты (F[A] => F[B]):

import cats.syntax.all._
val f: Int => Option\[Int\] = x => Some(x + 1)
val g: Int => Option\[Int\] = x => Some(x \* 2)
val h: Int => Option\[Int\] = f andThen (\_.flatMap(g))

Итого по функциям в Scala:

  • andThen — вызывает функции слева направо: f andThen g = g(f(x)).

  • compose — вызывает функции справа налево: f compose g = f(g(x)).

  • reduce(_ andThen _) — для цепочки функций.

  • Вложенные def или val можно компоновать аналогично обычным функциям.

  • Каррирование и частичное применение функций тоже являются частью композиции.