Чистые функции
Еще одна концепция, которую 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)
Вышеописанные функции соответствуют определению "чистых".
Ключевые моменты
Первым ключевым моментом этого раздела является определение чистой функции:
Чистая функция — это функция, которая зависит только от своих объявленных входных данных и своей реализации для получения результата. Она только вычисляет свой результат, не завися от внешнего мира и не изменяя его.
Второй ключевой момент заключается в том, что каждое реальное приложение взаимодействует с внешним миром. Таким образом, упрощенный способ представления о функциональных программах состоит в том, что они состоят из ядра чистых функций, которые обернуты другими функциями, взаимодействующими с внешним миром.
Ссылки: