Scala 3 — Book

Чистые функции

Language

Еще одна концепция, которую Scala предлагает для помощи в написании функционального кода, — это возможность писать чистые функции. Чистая функция (pure function) может быть определена следующим образом:

  • функция f является чистой, если при одних и тех же входных данных x она всегда возвращает один и тот же результат f(x)
  • результат функции зависит только от входных данных и её реализации
  • чистые функции только вычисляют результат, ничего не меняя за пределами этих функций

Из этого следует:

  • чистая функция не изменяет свои входные параметры
  • она не мутирует какое-либо скрытое состояние
  • у неё нет “черных ходов”: он не читает данные из внешнего мира (включая консоль, веб-сервисы, базы данных, файлы и т.д.) и не записывает данные вовне

В результате этого определения каждый раз, когда вызывается чистая функция с одним и тем же входным значением (значениями), всегда будет выдаваться один и тот же результат. Например, можно вызывать функцию double бесконечное число раз с входным значением 2, и всегда получать результат 4.

Примеры чистых функций

Учитывая это определение, методы в пакете scala.math._ являются чистыми функциями:

  • abs
  • ceil
  • max

Эти методы String также являются чистыми функциями:

  • isEmpty
  • length
  • substring

Большинство методов в классах коллекций Scala также работают как чистые функции, включая drop, filter, map и многие другие.

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

Примеры “грязных” функций

И наоборот, следующие функции “грязные” (impure), потому что они нарушают определение pure function:

  • println — методы, взаимодействующие с консолью, файлами, базами данных, веб-сервисами и т.д., “грязные”
  • currentTimeMillis — все методы, связанные с датой и временем, “грязные”, потому что их вывод зависит от чего-то другого, кроме входных параметров
  • sys.error — методы генерации исключений “грязные”, потому что они не “просто возвращают результат”

“Грязные” функции часто делают одно из следующего:

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

В общем, следует остерегаться функций с возвращаемым типом Unit. Поскольку эти функции ничего не возвращают, логически единственная причина, по которой они когда-либо вызываются, - это достижение какого-то побочного эффекта. Как следствие, часто использование этих функций является “грязным”.

Но грязные функции все же необходимы …

Конечно, приложение не очень полезно, если оно не может читать или писать во внешний мир, поэтому рекомендуется следующее:

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

Важно отметить, что есть способы сделать “нечистое” взаимодействие с внешним миром более “чистым”. Например, можно услышать об использовании IO монады для обработки ввода-вывода. Эти темы выходят за рамки данного документа, поэтому для простоты можно думать, что ФП приложения имеют ядро из “чистых” функций, которые объединены с другими функциями для взаимодействия с внешним миром.

Написание “чистых” функций

Примечание: в этом разделе для обозначения методов Scala часто используется общепринятый в отрасли термин “чистая функция”.

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

def double(i: Int): Int = i * 2

Вот чистая функция, которая вычисляет сумму списка целых чисел с использованием рекурсии:

def sum(xs: List[Int]): Int = xs match {
  case Nil => 0
  case head :: tail => head + sum(tail)
}
def sum(xs: List[Int]): Int = xs match
  case Nil => 0
  case head :: tail => head + sum(tail)

Вышеописанные функции соответствуют определению “чистых”.

Ключевые моменты

Первым ключевым моментом этого раздела является определение чистой функции:

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

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

Contributors to this page: