Основы функционального программирования на Scala | OTUS

Основы функционального программирования на Scala

Сегодня многие приложения пишутся с учётом концепции ООП и используют императивный код. Но несмотря на широкую популярность ООП в современной разработке, такой подход имеет ряд недостатков: 1. В большинстве высокоуровневых языков обработка ошибок реализована посредством механизма исключений. Сама идея прерывания выполнения при некорректном состоянии хороша, но реализация в виде выброса исключения с последующим его перехватом в другом месте требует от команды разработчиков железной дисциплины, иначе простая линейная вычислительная цепочка превратится в древообразную или даже в запутанный граф. А вот в функциональной парадигме предоставляются абстракции для прозрачной и простой обработки ошибок. 2. Императивный код хорош для описания последовательных вычислений, но сегодня большинство приложений асинхронны и многопоточны, поэтому уследить за всем сложно. Вспоминаем ситуацию, когда 2 потока пытаются изменить одно и то же значение либо первый поток ждёт второй поток, когда второй, в свою очередь, первый. 3. Отсутствует математически строгая, аксиоматизированная и пригодная к повседневному использованию теория, описывающая систему типов и паттерны проектирования. Да, сами паттерны проектирования являются неплохим шагом в сторону стандартизации способов проектирования, но их по большему счёту можно скорее назвать сборником указаний и советов, нежели строгой теории. А комбинируя паттерны, вы не сможете со 100%-ной уверенностью сказать, что в итоге получится.

А что насчёт функционального подхода?

В функциональном программировании подошли с другой стороны. К примеру, отказались от возможности менять переменные настолько, насколько это возможно. Функция с точки зрения информатики — не равна функции с точки зрения математики.Та же функция датчика случайных чисел не является математической функцией, ведь она не принимает параметров, в результате чего с точки зрения математики она должна быть константой, но каждый раз такая функция выдаёт разный результат, чего в математике не бывает. Если же запретить переменным меняться (уничтожить глобальное изменяемое состояние), функции станут действительно математическими, поэтому можно будет построить строгую математическую теорию. Как раз такая теория и была построена и сегодня она называется λ-исчислениями. Такая теория описывает типы и полиморфизм.

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

Сами монады бывают 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

image2_1-20219-a9628e.png

Далее в меню создания проекта выбираем версию SBT и Scala. После того, как вы нажмёте кнопку создания проекта, IDEA отдаст соответствующее указание о создании SBT-проекта (об этом свидетельствует надпись dump project structurefrom SBT), в результате чего структура папок появится не сразу — имейте это в виду.

После формирования проекта вы сможете увидеть приблизительно следующую структуру папок:

image1_1-20219-91c11a.png

Естественно, наибольший интерес представляет папка src, а также её подпапки, плюс файл build.sbt. Этот файл описывает структуру проекта и применяется для определения модулей и подмодулей, управления версиями библиотек и зависимостями. Сюда же дописываются импорты сторонних библиотек.

Итак, в папке main/scala создаём newscalaclass и выбираем там object. Его имя должно совпадать с именем файла, где он расположен, в обратном случае возможны ошибки. Создав object, увидите:

object HelloWorld{

}

Следующим шагом создаём в теле object точку входа в программу — метод main() (в языке программирования Scala методы объявляются ключевым словом def). Как только начнёте набирать defmain, автодополнение IDEA предложит сгенерировать метод. Итоговый код будет следующим:

object HelloWorld {
        def main(args: Array[String]): Unit = {

        } 
}

Если параметры командной строки args не нужны, код можно сократить:

object HelloWorld {
       def main: Unit = {

            }
}

Тут есть ряд особенностей: — в списке параметров сначала идут имена этих параметров, а потом типы параметров после двоеточия; — тип возвращаемого значения указывают через двоеточие после списка параметров (в языке программирования Scala все функции всегда возвращают значение, но если надобности в этом нет, применяется тип Unit — аналог ключевого слова void, который указывает на отсутствие какого-нибудь возвращаемого значения).

После добавления строчки println("Helloworld"), вы можете запустить программу (нажмите зелёный треугольник напротив main). В консоли появится классическая строка «Hello world».

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".

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто