Основы функционального программирования на Scala
Сегодня многие приложения пишутся с учётом концепции ООП и используют императивный код. Но несмотря на широкую популярность ООП в современной разработке, такой подход имеет ряд недостатков:
А что насчёт функционального подхода?
В функциональном программировании подошли с другой стороны. К примеру, отказались от возможности менять переменные настолько, насколько это возможно. Функция с точки зрения информатики — не равна функции с точки зрения математики.Та же функция датчика случайных чисел не является математической функцией, ведь она не принимает параметров, в результате чего с точки зрения математики она должна быть константой, но каждый раз такая функция выдаёт разный результат, чего в математике не бывает. Если же запретить переменным меняться (уничтожить глобальное изменяемое состояние), функции станут действительно математическими, поэтому можно будет построить строгую математическую теорию. Как раз такая теория и была построена и сегодня она называется λ-исчислениями. Такая теория описывает типы и полиморфизм.
Математики-топологи создали довольно абстрактную теория категорий, которую они применили для сокращения доказательств. Постепенно теория категорий нашла применение во многих отраслях математики. Информатики выяснили, что такая теория подходит при описании операций с реальными данными, которые не всегда бывают корректны. К примеру, понятие монады можно использовать при описании вычислений, способных привести к ошибке.
Сами монады бывают 2-х видов. Первый — «чистая» монада, представляющая корректные данные. Второй— некорректная монада, необходимая для представления некорректных данных. Над этими 2-мя типами данных определены следующие операции: 1. pure — функция для создания «чистой» монады. 2. flatMap — использование преобразования, которое порой приводит к некорректному результату. Допустим, операция деления целых чисел корректно обрабатывает все данные за исключением деления на 0. В таком случае создаётся некорректная монада и её не изменит применение к ней операции flatMap. 3. map — применение к данным операции, которая не способна вызвать исключений по своей природе, допустим, операции сложения.
Следует отметить, что функциональное программирование находится посередине между низкоуровневыми операциями и высокими уровнями абстракции, такими как уровень приложений либо сервисов. Как раз с учётом данных обстоятельств и создали объектно-функциональный стиль, который позволяет применять императивный код для низкоуровневых функций, чистый функциональный код в середине приложения и, соответственно, паттерны проектирования и другие ООП-технологии на самом высоком уровне абстракции. Из этого следует, что функциональный подход лучше использовать не для создания замкнутых систем, а в большей степени для описания бизнес-логики и создания слоёв приложений. И именно в этом ключе функциональное программирование используется сегодня — оно построено внутри объектных абстракций над низкоуровневым императивным кодом.
Чтобы показать, зачем нужна та либо другая функциональная конструкция, начнём с небольшого введения в мультипарадигменный язык программирования, имеющий мощную поддержку функционального подхода — Scala.
Статья, которую вы сейчас читаете, выйдет в двух частях и опишет фундаментальные языковые конструкции, а также некоторые средства сокращения кода.
Переходим к Scala
Первая версия Scala вышла в 2004 г. на платформе Java. Нынешняя версия языка имеет версии для JavaScript (Scala-js) и LLVM (Scala Native).
Поначалу, многие реализованные решения были созданы с целью устранить такие недостатки Java, как отсутствие синтаксического сахара и слабая типобезопасность. При этом новый язык сохранил возможность почти бесшовной интеграции с Java, то есть вы сможете использовать Java-библиотеки без ущерба для производительности.
Крупные корпорации оценили язык по достоинству: Scala активно применяется в LinkedIn, Netflix, Twitter, Sony и пр.
Устанавливаем IDE и SBT
Для использования Scala под платформу Java, потребуются установленные JRE и JDK. При этом для знакомства с языком целесообразно применять IntelliJ IDEA CommunityEdition со спецплагином ScalaPlugin (он доступен при установке). Также пригодится система построения проектов SBT (есть под разные операционные системы и скачивается при создании проекта).
Далее в статье последуют примеры, для запуска которых вполне подойдёт online-компилятор Scalastie.
Итак, чтобы создать первый проект, делаем следующее:
new project → scala → sbt
Далее в меню создания проекта выбираем версию SBT и Scala. После того, как вы нажмёте кнопку создания проекта, IDEA отдаст соответствующее указание о создании SBT-проекта (об этом свидетельствует надпись dump project structurefrom SBT), в результате чего структура папок появится не сразу — имейте это в виду.
После формирования проекта вы сможете увидеть приблизительно следующую структуру папок:
Естественно, наибольший интерес представляет папка src, а также её подпапки, плюс файл build.sbt. Этот файл описывает структуру проекта и применяется для определения модулей и подмодулей, управления версиями библиотек и зависимостями. Сюда же дописываются импорты сторонних библиотек.
Итак, в папке main/scala создаём newscalaclass и выбираем там object. Его имя должно совпадать с именем файла, где он расположен, в обратном случае возможны ошибки. Создав object, увидите:
object HelloWorld{ }
Следующим шагом создаём в теле object точку входа в программу — метод
object HelloWorld { def main(args: Array[String]): Unit = { } }
Если параметры командной строки args не нужны, код можно сократить:
object HelloWorld { def main: Unit = { } }
Тут есть ряд особенностей: — в списке параметров сначала идут имена этих параметров, а потом типы параметров после двоеточия; — тип возвращаемого значения указывают через двоеточие после списка параметров (в языке программирования Scala все функции всегда возвращают значение, но если надобности в этом нет, применяется тип Unit — аналог ключевого слова void, который указывает на отсутствие какого-нибудь возвращаемого значения).
После добавления строчки
object HelloWorld extends App{ println("Hello world") }
Если вы любите писать код в чистых текстовых редакторах, то в этом случае всё же не стоит пренебрегать IDEA. Дело в том, что в Scala есть много хитрых конструкций, а у IDEA — большое количество весьма полезных языко-специфических функций. Благодаря этому, среда разработки убережёт вас от ряда глупых ошибок в процессе написания кода.
Переменные в Scala
В Scala есть два типа переменных — val и var.
Val является неизменяемой переменной, которой вы можете присвоить значение лишь при инициализации. А вот как выглядит синтаксис введения постоянной:
val valueName: TypeName = value
Как и у параметров функции, сначала располагается имя постоянной, а после двоеточия — имя типа. В языке есть довольно продвинутая система типов, поэтому во многих случаях вы сможете опускать имя типа, а компилятор выведет его автоматически (IDEA это тоже умеет, достаточно поставить курсор на имя переменной и нажатьCtrl+Q).
Чтобы объявить переменную, используйте ключевое слово var:
var valueName: TypeName = value
Как и в случае с постоянной, тип во многих случаях можно опускать. Что касается переменных, то их можно объединять в кортежи, используя удобный синтаксис запаковки в кортеж:
val tuple: (Type1, Type2, ... , TypeN) = (val1, val2, … , valN )
Распаковка из кортежа:
val (val1: Type1, … valN: TypeN) = tuple
Кстати, типы тоже можете опускать, сокращая код:
val tuple = (val1, val2, … , valN ) val (val1, … val2) = tuple
Методы в Scala
Чтобы определить методы, используем ключевое слово def. Синтаксис следующий:
def methodName (parameter1: Type1, parameter2: Type2, … , parameterN: TypeN): returnType = { // method body }
Обратите внимание, что в определении нет слова return. Дело в том, что последнее значение в функции является возвращаемым. Аналогично и с оператором if: вопреки логике,которая принята в императивных языках, оператор if (condition) value_if_true else value_if_false имеет возвращаемое значение с типом, общим над value_if_true/value_if_false и значение, в зависимости от условия равное value_if_true/value_if_false.
Есть в функциях и синтаксический сахар. К примеру, если функция параметров не принимает, скобки можно не писать. Также можно не указывать тип возвращаемого значения. Как и в случаях с переменными, тип будет выведен (в среде разработки IDEA данный тип напечатается фантомным текстом). Очередная «сахарная» особенность заключается в том, что когда тело метода является коротким и содержит всего одну инструкцию, фигурных скобок можно не писать. Вот корректный код:
def five = 5
Следует отметить, что функция может быть значением, при этом она будет иметь функциональный тип, записываемый как SourceType =>ResultType.
Допустим, функция, увеличивающая число на 1 (здесь num + 1 — возвращаемое значение, а Int —тип):
def inc (num: Int) = num +1
будет иметь тип Int =>Int.
В этом случае мы сможем ввести постоянные, а их значениями станут наши функции:
val incFunc: Int=>Int = inc
Какова разница между val и def? Она заключается в том, что val вычисляется однажды и потом применяется при каждом упоминании, а значение типа def при упоминании вычисляется каждый раз. Это можно проверить следующим кодом:
val valrand = Random.nextInt() def defrand = Random.nextInt() println(valrand) println(valrand) println(defrand) println(defrand)
В нашем случае первые 2 строчки вывода будут совпадать, а вторые 2 будут различаться.
Когда же у функции входных параметров много, тип запишется следующим образом:
(Type1, Type2, … TypeN) =>ResultType
Давайте посмотрим на функцию сложения 2-х чисел:
def add(a: Int, b: Int) = a+b
Она будет иметь тип: (Int, Int) =>Int.
Когда же нам потребуется вернуть несколько значений из функции, можно воспользоваться распаковкой и упаковкой в кортеж.
Есть для функций и синтаксический сахар, позволяющий создавать функцию без ввода для неё отдельного метода:
(argument_list) =>value
При этом value вполне себе может быть блоком, где можно вводить дополнительные переменные, совершая некоторые действия:
val sum: (Int, Int) => Int = (a,b) => a+b val f2: Int => Int = a =>{ val v1 = Random.nextInt() val v2 = v1 % 10 -5 a - v2 }
Так как для функций есть тип, имеется возможность создания функции от функций, т. е. функции высшего порядка — здесь достаточно лишь дописать параметру функциональный тип. Например, мы можем написать функцию, которая к 2-м числам применяет заданное преобразование:
def transform (a: Int, b: Int, f: (int, Int) => Int): Int = f(a,b)
Существует ещё одна особенность, связанная с функциями: она заключается в наличии двух семантик передачи параметров: call by name и call by value. Последняя семантика используется по умолчанию: сначала вычисляется значение, а потом оно передаётся в функцию. Что касается Call by name, то тут, напротив, значение сначала передаётся в функцию, а потом вычисляется в каждом месте его упоминания. Если надо указать эту семантику, следует перед типом поставить знак =>:
var i =0 def inc = { i += 1 i } def callByValueDemonstration (num: Int) = { println(num) println(num) println(num) } def callByNameDemonstration (num: =>Int) = { println(num) println(num) println(num) } callByValueDemonstration(inc) callByNameDemonstration(inc)
В итоге вы должны получить вывод следующего содержания: 1 1 1 2 3 4. Это значит, что если вы желаете передать в функцию генератор случайных чисел, следует задействовать семантику callByName.
Вместо заключения
Это вводная часть статьи про функциональное программирование на Scala. В ней мы ознакомились с основами Scala: рассмотрели окружение, особенности ввода переменных и функций, а также 2 вида семантики методов — callByName и callByValue.
В следующий раз поговорим про объектную модель Scala и особенности, касающиеся синтаксического сахара.
Материал написан на основании статьи Ивана Камышана "Основы функционального программирования с примерами на Scala — часть 1".