Как реализовать композицию функций?
Композиция функций в 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 можно компоновать аналогично обычным функциям.
-
Каррирование и частичное применение функций тоже являются частью композиции.