Scala лямбда функция как параметр
Лямбда-выражение относится к выражению, которое использует анонимную функцию вместо переменной или значения. Лямбда-выражения более удобны, когда у нас есть простая функция, которую можно использовать в одном месте. Эти выражения быстрее и выразительнее, чем определение целой функции. Мы можем сделать наши лямбда-выражения многоразовыми для любых преобразований. Он может перебирать коллекцию объектов и выполнять какие-то преобразования к ним.
Синтаксис :
Пример :
Работа с лямбда-выражениями
-
Мы можем передавать значения в лямбду, как обычный вызов функции.
Пример :
// Scala программа для показа
// обработка лямбда-выражения
def main(args : Array[String])
val ex 1 = (x : Int) => x + 2
// с несколькими параметрами
val ex 2 = (x : Int, y : Int) => x * y
println(ex 2 ( 2 , 3 ))
Выход:
// Scala программа для применения
// преобразование в коллекцию
def main(args : Array[String])
val l = List( 1 , 1 , 2 , 3 , 5 , 8 )
// возводим в квадрат каждый элемент списка
val res = l.map( (x : Int) => x * x )
Выход:
Мы видим, что определенная анонимная функция для выполнения операции квадрата не может использоваться повторно.
// Scala программа для применения
// преобразование в коллекцию
def main(args : Array[String])
val l 1 = List( 1 , 1 , 2 , 3 , 5 , 8 )
val l 2 = List( 13 , 21 , 34 )
val func = (x : Int) => x * x
// возводим в квадрат каждый элемент списков
val res 1 = l 1 .map( func )
val res 2 = l 2 .map( func )
Выход:
// Scala программа для прохождения лямбды
// как параметр функции
// преобразовать функцию с целым числом x и
// функция f в качестве параметра
// f принимает Int и возвращает Double
def transform( x : Int, f : Int => Double)
def main(args : Array[String])
// лямбда передается в f: Int => Double
val res = transform( 2 , r => 3.14 * r * r)
Выход:
В приведенном выше примере функция преобразования принимает целое число x, а функция f применяет преобразование к x, определенному функцией f. Лямбда, переданная в качестве параметра в вызове функции, возвращает тип Double . Следовательно, параметр f должен подчиняться лямбда-определению.
в сообщениях блога Scala, которые дают ему" мы использовали этот тип-лямбда-трюк " handwave.
хотя у меня есть некоторая интуиция об этом (мы получаем анонимный параметр типа A без необходимости загрязнять определение с ним?), Я не нашел четкого источника, описывающего, что такое тип лямбда-трюк, и каковы его преимущества. Это просто синтаксический сахар, или он открывает некоторые новые измерения?
преимущества точно такие же, как те, которые предоставляются анонимными функциями.
пример использования, с Scalaz 7. Мы хотим использовать Functor который может отображать функцию над вторым элементом в Tuple2 .
Scalaz предоставляет некоторые неявные преобразования, которые могут вывести аргумент типа Functor , поэтому мы часто избегаем писать их вообще. Предыдущая строка может быть переписана как:
если вы используете IntelliJ, вы можете включить Настройки, Стиль Кода, Скала, Складывание, Тип Лямбды. Это тогда скрывает грубые части синтаксиса, и представляет более приемлемым:
будущая версия Scala может напрямую поддерживать такой синтаксис.
ответ:
одно подчеркивание в полях после P подразумевает, что это конструктор типа принимает один тип и возвращает другой тип. Примеры конструкторов типов с таким типом: List , Option .
дать List an Int , конкретный тип, и это дает вам List[Int] , еще один конкретный тип. Дай List a String и это дает вам List[String] . Так далее.
и List , Option можно рассматривать как функции уровня типа arity 1. Формально мы говорим, что они имеют вид * -> * . Звездочка обозначает тип.
теперь Tuple2[_, _] - это конструктор типов с видом (*, *) -> * т. е. вам нужно дать ему два типа, чтобы получить новый тип.
так как их подписи не совпадают, вы не можете заменить Tuple2 на P . Что вам нужно сделать, это частично применить Tuple2 на одном из его аргументы, которые дадут нам конструктор типа с видом * -> * , и мы можем заменить его на P .
к сожалению, Scala не имеет специального синтаксиса для частичного применения конструкторов типов, и поэтому нам приходится прибегать к чудовищности, называемой типом lambdas. (Что у вас есть в вашем примере.) Они называются так потому, что они аналогичны лямбда-выражениям, которые существуют на уровне значений.
следующий пример может справка:
Edit:
больше параллелей уровня значения и уровня типа.
в случае, если вы представили, параметр типа R является локальным для функции Tuple2Pure и поэтому вы не можете просто определить type PartialTuple2[A] = Tuple2[R, A] , потому что просто нет места, где вы можете поместить этот синоним.
Большинство приложений в мире написано в традициях ООП (объектно-ориентированного программирования) и использует императивный код. Несмотря на то, что объектно-ориентированный подход очень популярен и прост для понимания, он не лишён своих недостатков.
Недостатки ООП
Во-первых, в большинстве языков высокого уровня обработка ошибок реализована с помощью механизма исключений. Идея о том, что выполнение кода нужно прерывать, как только достигнуто некорректное состояние, хороша, но её реализация в виде выброса исключения и перехвата его в другом месте требует от коллектива программистов железной дисциплины. Иначе простая линейная цепочка вычислений превратится в древообразную, а то и в запутанный граф. Функциональная парадигма предоставляет абстракции для простой и прозрачной обработки ошибок.
Во-вторых, императивный код подобен тому, как человек представляет себе порядок каких-то действий. Такой подход очень хорош для описания последовательных вычислений, однако в современном мире большинство приложений асинхронные и многопоточные, и здесь уследить за взаимодействием императивного кода, выполняющегося во многих потоках, очень сложно. Наверное, каждый программист сталкивался с ситуациями, когда два потока пытаются изменить одно и то же значение, или же когда один поток ждёт другой поток, а тот, в свою очередь, ждёт первый?
В-третьих, не существует математически строгой, аксиоматизированной и пригодной к повседневному применению теории, описывающей паттерны проектирования и систему типов. Паттерны проектирования — это, конечно, большой шаг в сторону стандартизации способов проектирования, но и они — скорее сборник советов и указаний, нежели строгая теория. Исходя из этого, комбинируя паттерны, вы никогда с полной уверенностью не будете знать, что получите в итоге, если вы не делали этого ранее, что, в свою очередь, ведёт к разного рода «неожиданностям» при разработке.
Функциональный подход
В функциональном программировании решили подойти с другой стороны. Во-первых, решили отказаться от возможности изменять переменные настолько, насколько это возможно (существуют фундаментальные ограничения, не позволяющие сделать это полностью). Функция в смысле информатики не является функцией в смысле математики: например, функция датчика случайных чисел (на основе физического датчика) не является математической функцией — она не принимает параметров, и потому, с точки зрения математики, должна быть константой, но каждый раз эта функция выдаёт разный результат. Такого в математике не бывает. Если же мы запретим переменным изменяться, то есть уничтожим глобальное изменяемое состояние, то функции станут действительно математическими, и можно будет построить строгую математическую теорию. Такая теория была построена и ныне носит название λ-исчисления. Эта теория описывает типы и полиморфизм.
Математиками-топологами была построена достаточно абстрактная теория категорий, которую они использовали для сокращения доказательств. Со временем теория категорий нашла своё применение во множестве отраслей математики. Информатики выяснили, что эта теория подходит для описания операций с реальными данными, которые не всегда могут быть корректными. Например, понятие монады можно применить для описания вычислений, которые могут привести к ошибке.
Монады бывают двух видов. Первый вид — «чистая» монада, она представляет корректные данные. Второй вид — некорректная монада, она нужна для представления некорректных данных. Над этими двумя типами данных определены операции:
- pure — функция создания «чистой» монады.
- flatMap — применение преобразования, которое может привести к некорректному результату. К примеру, операция деления целых чисел будет корректно обрабатывать все данные, кроме деления на 0. В этом случае будет создана некорректная монада и применение к ней операции flatMap её не изменит.
- map — применение к данным операции, которая не может вызвать исключений по своей природе, например, операции сложения.
Важно понимать, что функциональное программирование занимает нишу в середине между низкоуровневыми операциями и очень высокими уровнями абстракции — например, такими, как уровень сервисов или приложений. Исходя из этого был создан объектно-функциональный стиль, позволяющий использовать императивный код для низкоуровневых функций, чистый функциональный код в середине приложения и паттерны проектирования и прочие ООП-технологии на наивысшем уровне абстракции. Таким образом, функциональный подход лучше применять не для создания замкнутых систем, а скорее для слоёв приложений и описания бизнес-логики. Именно в таком ключе функциональное программирование и применяется на сегодняшний день — построенное над низкоуровневым императивным кодом, внутри объектных абстракций.
Для того чтобы приводить жизненные примеры и показывать, зачем нужна та или иная функциональная конструкция, начнём с краткого, но довольно исчерпывающего введения в мультипарадигменный язык программирования с сильной поддержкой функционального подхода — Scala.
В этой статье будут описаны фундаментальные языковые конструкции и некоторые средства сокращения кода.
О Scala
Изначально язык Scala был разработан Мартином Одерски (который также работал над Generics в Java и над компилятором Javac) в EPFL. Первая версия языка была выпущена в мир в 2004 году на платформе Java. Нынешняя версия языка (2-я) имеет версии для платформ JavaScript (Scala-js) и под LLVM (Scala Native).
Изначально многие решения, реализованные в этом языке, были направлены на устранение недостатков языка Java, таких как слабая типобезопасность и отсутствие синтаксического сахара. Притом новый язык сохранил возможность практически бесшовной интеграции с Java — вы можете использовать все библиотеки, написанные на Java, без падения производительности.
Язык был оценен по достоинству крупными корпорациями: например, он активно используется в Twitter, LinkedIn, Netflix, Sony и других.
Установка IDE и SBT
Чтобы использовать Scala под платформу Java, вам понадобятся установленные JRE и JDK. Для знакомства с языком разумно использовать IDE IntelliJ IDEA Community Edition со специальным плагином Scala Plugin (доступен прямо при установке IDE) и систему построения проектов SBT (есть под большинство ОС, скачивается при создании проекта).
Примеры из статьи можно запускать, используя онлайн-компилятор Scalastie.
Для того чтобы создать ваш первый проект, нужно сделать следующее:
new project → scala → sbt
В меню создания проекта нужно выбрать версию SBT и Scala, последними на момент написания статьи являются 1.2.1 и 2.12.6 соответственно.
После нажатия кнопки создания проекта IDEA отдаст указание о создании проекта SBT (это можно понять по надписи dump project structure from SBT), поэтому структура папок проекта появится не сразу.
После формирования проекта вы можете увидеть подобную структуру папок:
Нас в основном будут интересовать папка src и её подпапки main/scala и test/scala, а также файл build.sbt. Файл build.sbt описывает всю структуру проекта и используется для определения модулей, подмодулей, управления зависимостями и версиями библиотек. Сюда же стоит дописывать импорты сторонних библиотек.
В папке main/scala нужно создать new scala class и выбрать там object (о том, что это такое и какую роль он играет в Scala, будет сказано ниже). Обратите внимание, что имя обджекта должно совпадать с именем файла, в котором он расположен, иначе вы можете столкнуться с ошибками при попытке запустить вашу первую программу.
После создания обджекта вы увидите следующий код:
В теле обджекта нужно создать точку входа в программу, а именно метод main() (в Scala методы объявляются ключевым словом def ), автодополнение IDEA предложит вам сгенерировать метод как только вы начнёте набирать def main . В итоге должен получиться такой код:
Если же вам не нужны параметры командной строки args , вы можете сократить свой код до:
Отметим несколько отличительных особенностей: во-первых, в списке параметров сначала идут имена параметров, а потом их типы после двоеточия, во-вторых, тип возвращаемого значения указывается через двоеточие после списка параметров (в Scala все функции всегда возвращают значение, но когда необходимости в этом нет, используется тип Unit — это аналог ключевого слова void , указывающий на отсутствие какого-либо возвращаемого значения).
Добавив строчку println("Hello world") , вы сможете запустить эту программу, нажав на зелёный треугольник напротив метода main . Если всё сделано правильно, в консоли должна появиться строка «Hello world».
Мы настоятельно рекомендуем всем любителям чистых текстовых редакторов на этапе знакомства с языком пользоваться IDEA (пусть это и противоречит философии Unix). Scala содержит большое количество достаточно хитрых конструкций, поддержку которых редко когда можно встретить в редакторе. Кроме того, у IDEA есть много полезных языко-специфических функций. С их помощью IDEA может спасти вас от некоторых глупых ошибок ещё во время написания кода.
Переменные
В языке Scala есть 2 типа переменных — val и var .
val — это неизменяемая переменная, ей можно присвоить значение только при инициализации. Синтаксис введения постоянной выглядит так:
val valueName: TypeName = value
Опять же, как и у параметров функции, имя постоянной находится сначала, а имя типа — после двоеточия, за именем постоянной. В Scala существует продвинутая система типов, поэтому в большинстве случаев вы можете опускать имя типа, компилятор выведет его за вас (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
Методы
Для определения методов используется ключевое слово def . Синтаксис для определения метода выглядит так:
В определении вы наверняка не увидели слово return . Это потому, что последнее значение в функции является возвращаемым. То же самое касается и оператора if : вопреки принятой в императивных языках логике, оператор if (condition) value_if_true else value_if_false имеет возвращаемое значение, имеющее тип, общий над value_if_true / value_if_false и значение, в зависимости от условия равное value_if_true / value_if_false .
В функциях не обошлось без синтаксического сахара. Если функция не принимает параметров, то вы можете не писать скобки. Кроме того, можно не указывать тип возвращаемого значения. Как и в случае с переменными, тип будет выведен (в IDEA этот тип будет напечатан фантомным текстом). Ещё одна «сахарная» особенность — если тело метода достаточно короткое и содержит всего лишь одну инструкцию, вы можете не писать фигурных скобок. Например, такой код будет абсолютно корректен:
Хотелось бы отметить, что функция может быть значением и при этом будет иметь функциональный тип, который записывается как SourceType => ResultType .
К примеру, функция, которая увеличивает число на 1 (здесь num + 1 — возвращаемое значение, а Int — его тип):
def inc (num: Int) = num +1
будет иметь тип Int => Int .
Тогда мы можем ввести постоянные, значениями которых будут наши функции:
val incFunc: Int=>Int = inc
В чём же разница между val и def ? Дело в том, что значение val вычисляется однажды и дальше используется при каждом упоминании, а значение типа def вычисляется каждый раз при упоминании. Проверить это можно следующим фрагментом кода:
Первые две строчки вывода будут совпадать, а вторые две — различаться.
Если же у функции много входных параметров, тип можно записать так:
(Type1, Type2, … TypeN) => ResultType
К примеру, функция сложения двух чисел:
def add(a: Int, b: Int) = a+b
будет иметь тип: (Int, Int) => Int .
Если же нам нужно вернуть несколько значений из функции, мы можем воспользоваться упаковкой и распаковкой в кортеж.
Для функций существует синтаксический сахар, позволяющий создать функцию, не вводя для неё отдельного метода. Он может быть вам знаком, потому как присутствует в других языках:
value вполне может быть блоком, в котором вы можете вводить дополнительные переменные и совершать некоторые действия. Примеры:
Поскольку для функций существует тип, мы можем создавать функции от функций, то есть функции высшего порядка. Для этого достаточно просто дописать параметру функциональный тип. К примеру, можно написать функцию, которая к двум числам применяет заданное преобразование:
def transform (a: Int, b: Int, f: (int, Int) => Int): Int = f(a,b)
Есть ещё один нюанс, связанный с функциями. Существует 2 семантики передачи параметров: call by name и call by value. Call by value используется по умолчанию: значение сначала вычисляется, а потом передаётся в функцию. Call by name, наоборот, сначала передаётся в функцию, а потом вычисляется в каждом месте упоминания. Для указания этой семантики перед типом ставится знак => . Продемонстрировать это можно следующим примером:
Вы должны получить следующий вывод: 1 1 1 2 3 4 . Таким образом, если вы хотите передать в функцию генератор случайных чисел, вы должны использовать семантику callByName.
Заключение
Мы познакомились с основами языка программирования Scala: рассмотрели, как настроить окружение, как вводить переменные, функции. Мы также рассмотрели два вида семантики методов — callByName и callByValue.
В следующей статье мы рассмотрим объектную модель языка Scala и некоторые особенности, связанные с синтаксическим сахаром.
Не так давно я заинтересовался одним из многочисленных ныне языков под JVM — Scala. Причин тому много, основная — всё нарастающее со временем чувство неудобства при работе с cpp-подобными языками. Взгляд мой попеременно падал на Ruby, Groovy, Python, но все они оставляли впечатление инструментов, не совсем подходящих для моего обычного круга рабочих задач (Python-таки хорош, но у нетипизированных языков есть свои ограничения). Scala же, напротив, показалась вполне годным языком. Так как поиск по хабру никаких статей о ней не выловил (было несколько, но мягко говоря не вводных), я решил написать маленький обзор и поделиться им с массами.
Немного философии языка в вольном изложении
Какие основные цели преследовали создатели языка? Согласно моим мироощущениям они такие:
Во-первых, совместимость. Среди своих задач разработчики ставили поддержание совместимости с Java-языком и тысячами примеров говнокода разработок на ней для решения самых разнообразных задач.
Во-вторых, интенсивное насыщение языка функциональными фичами, которые, в основном, (но далеко не полностью) составляют его отличия от Java.
В-третьих, облегчение нашего с вами труда. Действительно, компилятор Scala понимает программиста с полуслова, писать код специально, чтобы втолковывать ему, что я не верблюд, мне не довелось пока.
В-четвёртых, поддержка и стимулирование написания модульных, слабосвязанных программных компонентов в сочетании с широкими возможностями адаптации уже существующих. Цели не то, чтобы совсем противоположные, но порождающие известные трудности для одновременного достижения. Что ж, посмотрим что получится.
В-пятых, это поддержка параллелизма. К сожалению у меня руки и голова до этой области не дошли (надеюсь пока), но акцент на этом моменте делается постоянно на всех ресурсах по языку.
Для экспериментов с языком достаточно поставить соответствующий плагин на любимую IDE отсюда.
Итак, давайте посмотрим на сам язык…
Общие идеи языка, примеры синтаксиса
Самое, пожалуй, важное, — это «унифицированная модель объектов». Этот термин расшифровывается авторами так: «каждое значение — объект, каждая операция — вызов метода». Это, конечно, не «всё — объект», но сущностей в сравнении с Java убыло, а чем меньше сущностей — тем легче жизнь :) В прикладном плане это означает, что числа и символы сделались неизменяемым объектами, обитающими в общей куче, все операции приобретают ссылочную семантику. Например, код 5 + 5 вполне валиден, и породит новый объект в куче, который оперативненько оприходует сборщик мусора (на самом деле, я тихо надеюсь, что компилятор поймёт глубину замысла и порождать он ничего не будет :) ).
После столь возвышенного введения можно глянуть на решение классической задачи:
- Можно объявлять отдельные объекты. Ничего необычного в этом нет, подобная возможность имеется, например в Groovy. Ведут себя такие объекты так же как написанные на Java реализации шаблона Singelton.
- Объявление фукции выглядит непривычно, но вполне читабельно: [ключевое слово def] [имя]([список параметров]):[возвращаемый тип] = [блок кода].
- В качестве типа, не несущего информационной нагрузки, выступает тип Unit. Он вполне аналогичен void в C-подобных языках.
- Объявление параметра функции (а на самом деле и локальной переменной тоже) выглядит как [имя]:[тип].
- Для параметризации типа используется не привычные нам <> , а казалось бы, навсегда закреплённые за масивами [] .
- Для обращения к элементам массива(экое непотребство) используются () .
- Имеется какие-то встроенные функции, доступные в коде по умолчанию без всяких дополнительных импортов.
Как из него следует, использование оператора . совершенно не необходимо. Синтаксис языка вполне допускает использование вместо него пробела (также аргументы метода можно писать без скобок и запятых сразу после имени метода). И как мы видим, это оказывается вполне полезно: в первой строке высокоприоритетный оператор . заставляет нас писать ненужные, засоряющие код скобки, во второй получается более краткая и наглядная форма записи.
В качестве подспорья разработчику Scala поддерживает также интерактивный режим. То есть, можно запустить интерпретатор и по одной вводить комманды. Интерпретатор, встроенный в IDE, как-то нерегулярно работает, его отдельный вариант есть в репозитариях Убунты, думаю у остальных дистрибутивов тоже всё хорошо, счастливым обладателям Windows как всегда придётся помучаться :) Интерпретатор запускается самым необычным способом:
$ scala
Welcome to Scala version 2.7.3final (Java HotSpot(TM) Server VM, Java 1.6.0_16).
Type in expressions to have them evaluated.
Type :help for more information.
scala>
Совсем маленький пример:
scala> 1 to 10
res0: Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Тут мы видим пример вызова метода с параметром. Если кто не догадался, у объекта класса Int 1 вызывается метод to с параметром того же типа 10, результат — диапазон значений.
Попробуем-ка мы теперь написать ещё одну функцию. Пусть она нам считает сумму чисел в заданном диапазоне, итак:
scala> def sum(a: Int, b: Int): Int = <
| var result = 0
| for (i <- a to b) result += i
| result
| >
sum: (Int,Int)Int
- При помощи ключевого слова var мы можем объявлять локальные переменные
- Результатом вычисления блока является последнее выражение в нём
- В нашем распоряжении имеется цикл for, который может выполнять вычисления для значений в заданном диапазоне (на самом деле для объектов в любом объекте — контейнере)
Операции над функциями
Что же мы такого можем с ними тут делать? Да что угодно =) Функции являются полноценными объектами программы. Их можно хранить как свойства объектов, передавать как параметры и возвращаемые значения и собственно создавать во время выполнения. Данные свойства позволяют строить так называемые функции высокого порядка, оперирующие себе подобными.
Для иллюстрации рассмотрим ставший классическим пример вычисления суммы:
scala> def sum(f: Int => Int, a: Int, b: Int): Int =
| if (a > b) 0 else f(a) + sum(f, a + 1, b) sum: ((Int) => Int,Int,Int)Int
В данном примере определяется функция sum, представляющая знакомый, надеюсь, всем оператор суммы. Параметры имеют следующий смысл:
f — функция преобразования целого числа из пределов суммирования в элемент суммы. Обратите внимание на объявление типа параметра: знак => означает, что параметр — функция, типы принимаемых значений перечисляются слева от него в круглых скобках (если параметр один, как в данном примере, их допустимо опустить), тип возвращаемого результата справа.
Работает она тривиально: вычисляет значение функции в нижней границе диапазона и складывает его с результатом вычисления себя самой в диапазоне на 1 меньшем.
Также в этом примере видна ещё одна особенность языка — if является выражением, имеющим значение (кстати, использованный ранее for — тоже выражение, его результат типа Unit ). Если условие истина, то его результат первый вариант, иначе — второй.
a и b — пределы суммирования.
Ещё пара функций id и square , они равны своему параметру и его квадрату соответственно.
scala> def id(x: Int): Int = x
id: (Int)Int
scala> def square(x: Int): Int = x * x
square: (Int)Int
Тут надо сделать ещё одно лирическое отступление: функции в Scala имеют декларативный стиль объявления. Они описывают не как получить результат, а чему он равен. Но если требуется организовать последовательные вычисления в теле функции, нет проблем — у нас есть блоки.
Теперь можно воспользоваться тем, что мы написали ранее.
scala> sum(id, 1, 5)
res1: Int = 15
scala> sum(square, 1, 5)
res2: Int = 55
Здесь происходит кульминация этой части — мы берём и передаём функцию в другую функцию. Никаких интерфейсов, анонимных классов, делегатов: вот оно — маленькое счастье.
Я здесь намеренно не стал приводить примеры вложенных и анонимных функций, карринга. Всё это Scala умеет, но всё нельзя включить в небольшой обзор. Думаю приведёный пример достаточен для понимания важности и, главное, — удобства функций высокого порядка как инструмента программирования. Напоследок могу посоветовать почитать эту главу замечательной книги по программированию.
Особенности классов
class Complex(r: Double, i: Double) <
def real = r
def image = i
def magnitude = Math .sqrt(r*r + i*i)
def angle = Math .atan2(i, r)
def + (that: Complex) = new Complex( this .real + that.real, this .image + that.image)
object Main <
def main(args:Array[ String ]) :Unit = <
val first = new Complex(1, 5)
val second = new Complex(2, 4)
val sum = first + second
println(first)
println(second)
println(sum)
>
>
Во-первых, клас объявлен с какими-то параметрами. Как несложно догадаться по продолжению, это параметры конструктора, которые доступны всё время жизни объекта.
Во-вторых, в классе объявлено несколько методов — селекторов. Одно семейство для декартового представления и одно для полярного. Как видим оба они используют параметры конструктора.
В-третьих, в классе объявлен оператор сложения. Объявлен он как обычный метод, принимает также Complex и возвращает его же.
Ну и наконец, для этого класса переопределена, без сомнения, знакомая всем Java-программистам функция toString . Важно отметить что на переопределение методов в Scala всегда необходимо явно указывать при помощи ключевого слова override .
- Занимает неоправданно много для своей функциональности места на экране
- Не умеет сравнивать себя с себе подобными
class Complex(val real: Double, val image: Double) extends Ordered[Complex] <
def magnitude = Math .sqrt(real*real + image*image)
def angle = Math .atan2(image, real)
def + (that: Complex) = new Complex( this .real + that.real, this .image + that.image)
def compare(that: Complex): Int = this .magnitude compare that.magnitude
override def toString = real+ " + i*" +image+ " | " +magnitude+ "*e^(i*" +angle+ "))"
>
object Main <
def main(args:Array[ String ]) :Unit = <
val first = new Complex(1, 5)
val second = new Complex(2, 4)
if (first > second )
println( "First greater" )
if (first < second )
println( "Second greater" )
if (first == second )
println( "They're equal" )
>
>
- У параметров конструктора появилось ключевое слово val и исчезли соответствующие селекторы. Да, всё вполне очевидно, это разрешение компилятору создать селекторы для них автоматически.
- Добавилось наследование от незнакомого нам класса (а точнее trait'а) Ordered. Да не простого, а параметризованного нашим классом. Как следует из названия, он должен помочь нам с упорядочиванием наших экземпляров.
- Появился метод compare, который сравнивает два комплексных числа посредством сравнения их модулей.
- В тестовом методе появились использования операторов >, <, ==. Их мы явно не определяли.
- Во-первых, этот класс использует уже знакомое нам ключевое слово var в своём теле, да не просто а с диковинным модификатором private[this] . Значение этого ключевого слова в теле класса абсолютно аналогично таковому в внутри блока(и даже, скажу по секрету, в конструктор его тоже можно запихать) и делает из имени после него изменяемый аттрибут класса. Диковинный модификатор заявляет, что переменная должна быть доступна только данному объекту. Можно было написать, например, private[User] и она стала бы доступна другим, нам подобным, объектам, или указать имя пакета (что-то это мне напоминает).
- Далее объявлена функция возвращающая наше поле в верхнем регистре.
- И в заключение, странная функция name_= , получающая строку в виде параметра, проверяющая что она не null и записывающая её в наше поле.
Внимание, вывод: метод с именем <что-то>_= вызывается при использовании конструкции <объект>.<что-то> = <что-то другое>. Насколько я знаю в Scala это второй хак (первый — преобразование () в вызов метода apply ), как Гвидо завещал c неявным преобразованием использования оператора в вызов метода.
Pattern-matching
- Создать функцию-конструктор с именем, совпадающим с класом.
- Имплементировать в классе toString, equals, hashCode на основе аргументов конструктора.
- Создать селекторы для всех аргументов конструктора.
case class KnownUser(val name: String ) extends User
case class AnonymousUser() extends User
object Test <
val users = List (KnownUser( "Mark" ), AnonymousUser(), KnownUser( "Phil" ))
def register(user: User): Unit = user match <
case KnownUser(name) => println( "User " + name + " registered" )
case AnonymousUser() => println( "Anonymous user can't be registered" )
>
def main(args: Array[ String ]) =
users. foreach ( register )
>
Итак, общая картина кода: есть абстрактный класс пользователя, есть два его казуальных потомка: известный и анонимный пользователи. Мы хотим зарегистрировать некий список пользователей на (здесь включаем фантазию) встречу. Для чего и используем pattern-matching, который позволяет нам определить разное поведение метода для разных типов объектов и обеспечивает выборку данных из этих объектов.
- Конструкторы других case-классов. Тут всё вполне рекурсивно, глубина вложенности шаблона
ограничивается безумием программистане ограничивается. - Переменные шаблона. Они становятся доступны в теле функции вычисления результата.
- Символы _ обозначающие любое, неинтересующее нас значение.
- Литералы языка. Например 1 или "Hello" .
Люди знакомые с базовыми принципами ООП конечно сразу заметят, что проблема эта вполне решается использованием виртуальных функций (более того предлагаемый подход является не лучшей практикой). Однако их использование несёт в себе две трудности: во-первых усложняет поддержку кода при большом числе таких функций (нам ведь захочется регистрировать пользователей и на события, и в группы, и в блоги и т.п., что для каждого случая создавать виртуальный метод?), во-вторых не решает проблемы с тем, что объекты одного типа могут принципиально иметь разную структуру и не иметь возможности предоставлять некоторые данные.
На вторую проблему хочется обратить особое внимание. Как выглядил бы приведённый выше код в Java? Один класс, если пользователь анонимный выставляем в имени null и проверяем каждый раз (эстеты вроде меня заводят методы типа isAnonymous , состоящие из сравнения поля с тем же null ). Проблемы налицо — неявно и небезопасно. Таких примеров великое множество, когда разные вариации структуры объектов объединяются в один класс, а неиспользуемые в конкретном случае забиваются null 'ами, или того хуже придумывается значение по умолчанию. Scala позволяет явно описывать вариации структуры объектов, и предоставляет удобный механизм для работы с этими вариациями.
- У нас много функций. Да если у нас пара сотен операций, используемых по паре раз, зависящих от структуры и содержания объектов, система на основе case classes — pattern matching будет явно лучше поддерживаема.
- У нас мало классов. match из пары выриантов всегда хорошо читаем.
- У нас есть значительные вариации структуры объектов, которые однако надо хранить и обрабатывать единообразно.
Вывод типов
Заключение
Мда… К началу поста уже и скролить долго. Явно пора заканчивать. А сказать хотелось бы ещё про многое: про интереснейший механизм описания обобщённых классов, про неявные преобразования и то, что они на самом деле явные, ленивую инициализацию констант.
Мне и самому ещё только предстоит изучить модель многопоточности и своеобразный набор примитивов для её реаизации, разобраться с языковой поддержкой xml, поиграться с DSL-строением, посмотреть на их флагманский проект — Lift…
- Scala является весьма лаконичным и выразительным языком
- Она предоставляет мощный инструментарий для создания простых и красивых программ
UPD: поправил грамматику, спасибо всем оказавшим в этом помощь. Особенно ganqqwerty за массовые разборки с запятыми.
_________
All source code was highlighted with Source Code Highlighter .
Текст подготовлен в ХабраРедакторе
val - константа
var - Изменяемая переменная:
- все типы являются классами (нет встроенных типов): Byte, Char, Short, Long, Float, Double, Boolean
- String используется java.lang.String но добавляет дополнительные функции (StringOps, RichInt, RichDouble . )
Операторы
- Все операторы, на самом деле методы: - нет инкремента (++) и декремента (--) , только i+=1
- Некоторые функции (типа sqrt) можно использовать без объявления пакета после import - s(i) - java аналог s.charAt(i)
явный вызов в scala: s.apply(i)
- () - аналог void
- ; - нужна только если в строке несколько выражений
- <> - несколько операторов в блоке, возвращает значение - это будет последнее выражение
- результатом присвоения является () - в java = присваеваемому
Условия
- Условия возвращают значение это удобней, т.к. может быть использовано для инициализации const
аналогом c++ является x > 0 ? 1 : -1
Вывод в консоль
, где:
$a - переменная
$<> - выражение
Циклы
Функции
- Однострочный вариант: - Многострочный: - Указание типа обязательно для рекурсии, иначе нет: - Можно использовать return для немедленного выхода из функции
- параметр функции может иметь значение по умолчанию: - передавать параметры можно по имени в любом порядке: - функция с переменным числом параметров: - Чтобы передать список как несколько параметров, нужно добавить : _* - def w = .
логически похоже на переменную, которая каждый раз переинициализируется при обращении
- Способы передачи параметров в функцию:
call by value - каждый аргумент расчитывается единожды и передается в функцию
call by name (по ссылке) - аргумент не вычисляется, если он не используется в теле функции (вычисление происходит внутри функции)
т.е. если в параметре сложное выражение, то оно будет выполнено несколько раз (но если оно не испольузется внутри, то не будет ненужного вычисления как в call by value)
- Частичные (partial) функции:
partial функция - реализуется чреез pattern matching и может работать только для нужных case:
определена ли функция для значения можно проверить через isDefinedAt если вызывать для несуществующего значения, то будет исключение: может применяться в lambda функциях как параметр и будет вызываться только для применимых значений:
- Возврат Option значения
обработка ситуации NULL указателя через Option и getOrElse или обработка None
- Исключающее ИЛИ в параметрах или возвращаемом значении
Either[A, B] - исключающее или как возвращаемый параметр - может использоваться как обработку ошибок без Try
"Right - это правильно". Поэтому же map и flatMap применяются к правильной ветви, то есть к правой. И "хороший" результат оборачивают в Right, а ошибку в Left.
- чтобы перехватить иключение, можно использовать объект Try:
Процедуры
Ленивые переменные
Операция для инициализации w вызовется при первом обращении.
Исключения
Массивы
- Фиксированный массив: - с начальной инициализацией: - Переменной длины:
Аналог java arraylist: - добавить элемент в массив: - несколько значений (каждое станет значением) - обход массива:
-- если индекс не нужен: -- если нужен индекс: until почти то же самое, что to , создается множество -1 значение
- yeld - обход с преобразованием в новый массив (описано в циклах)
- обход и фильтрация массива: - arr.indices - список индексов (ключей) массива
- встроенные методы: sum, max, count(_ > 0), sorted (_ < _) //направление сравнения
- arr.mkString(",") - преобразование массива в строку или toString - дополнительно возвращает тип данных
Ассоциативные массивы
- с начальными значениями фиксированный: - изменяемый массив: - доступ почти аналогичен java: короткий вариант записи: - изменение: - добавление, как в обычном массиве: += //если ключ уже есть, то он перетрется
- удалить ключ: -= "Key"
- обход ассоц. массива: - чтобы получить только значения: arr.values, ключи: arr.KeySet
- отсортированный массив хранится в виде Красно-чёрного дерева Красно-чёрное дерево схоже по структуре с B-деревом с параметром 2, в котором каждый узел может содержать от 1 до 3 значений и, соответственно, от 2 до 4 указателей на потомков
- хуже сбалансировано, чем btree (много уровней дерева из-за маленькой степени = 2)
+ но легче балансировать при удалении:
* ищется меньшее значение справа от удаляемого (или большее слева)
* найденый указатель перемещается в точку удаления
* ссылка от родителя на перемещаемый элементы (пред. пункт) удаляется
Дерево просто балансируется, тогда как btree при удалении элементы он лишь помечается к удалению, из-за чего оно растет в объеме и требует переиодического ребилда.
Дерево медленнй hash массива при вставке и доступе по ключу, но быстрей для поиска по диапазону.
Кортежи (tuples)
Классы
- аналогично java: - Геттеры / сеттеры генерируются автоматически - параметры главного конструктора - автоматически генерируют свойства и геттеры/сеттеры к нему (поле становится приватным) - указание требований к параметрам класса: - можно вкладывать классы в классы и функции в функции
Объекты
- нет статичных методов, вместо этого используется object - объект этого типа может иметь только 1 экземпляр (реализует singleton) - Объекты компаньоны - должны определяться в одном файле и имеют доступ к приватным свойствам/методам друг друга
- метод apply - служит для создания и возвращения класса компаньона Пример применения apply:
- Array(100) - вернет объект-массив с 1 элементом = 100
- new Array(100) - выполнит конструктор и создаст пустой массив из 100 элементов
- Любая программа должна начинаться с main: Но если унаследовать App, то это можно делать в конструкторе:
- перечисления - enum
Нет встроенного типа, но есть класс теперь можно обращаться:
Пакеты и импортирование
Наследование
Файлы и регулярные выражения
- чтение всего файла в буфер: -- если файл большой, то используем буферизированное чтение: - чтение URL - двоичные файлы читаются как в java
- запись также как в java
- сериализация: - выполнение команд командной строки: - строки в тройных кавычках не надо экранировать: - NIO - новая реализация отображения файлов в память, самый быстрый вариант
-- чтение данных
-- складывание в буфер, который мы читаем
+ эти процессы происходят в параллель (асинхронность), что дает повышенную производительность при чтении, так и при записи (т.к. данные обмениваются через быстрый буфер в памяти)
Трейты (интерфейсы)
Операторы
Функции высшего порядка
- функцию можно положить в переменную: _ означает необходимость параметра для функции
- Общая сигнатура функции высшего порядка:
f - тип функции, которая принимает A и возвращает B - метод класса: переменная: (параметр: тип).функция(параметр: тип) переменная: (входные параметры) => выходной параметр = вызов функции с параметрами
- анонимная функция можно передавать ее как параметр, без переменных: более короткая запись: - функция, параметром которой является функция: - функция возвращает функцию: в функцию mulBy положена функция "(x : Double) => factor * x" - каррирование - преобразование ф. с 2 параметрами в 2 функции с 1 параметром:
каррирование удобно для создания новых функций:
т.е. это цепочка из 2 функций вложенных друг в друга(если параметров каррирования 2)
первый описывает преобразование
второй статичные параметры, которое использует действие (преобразование)
- передача абстрактного кода: - return может быть использован чтобы определить тип возвращаемых данных, если одновременно используется анонимная функция без типа, которая должна посчитать результат
- monad - Монада
имеет 2 обязательных метода:
-- flatMap (примеменяет функцию f(T): монада(T) для всех элементов)
-- unit (T->монада[T]) - помещение элемента во внутренний контейнер (int в List/Set/Map)
К примеру, List - монада, т.к. помещает элементы во внутренний контейнер-список над которым можно применить flatmap
Правила монады:
-- ассоциативность: m.flatMap(f).flatMap(g) == m.flatMap(x=> f(x).flatMap(g))
-- левый unit: unit(x).flatMap(f) == f(x) //вызов flatmap c функцией f над list из 1 элемента = вызову f(один элемент)
-- правый unit: m.flatMap(unit) == m (m - уже монада = unit(x))
Коллекции
- наследование объектов: - Преобразование типов через: .to*
- все коллекции по умолчанию неизменяемые (Immutable)
т.е. если мы изменяем коллекцию, то создается копия с изменениями
- Vector - immutable расширяемый массив (ArrayBuffer - muttable)
Технически хранится в виде b-дерева, с элементами в узле (32 элемента в блоке)
- Range - последовательность чисел
Технически хранит: начало, конец и шаг
- на последовательностях сделано: Stack, Queue, Prior Queue, ListBuffer
- for yield - более короткая замена для Map/flatmap/withfilter это аналог более длинной записи: - можно приводить java объекты к scala коллекциям
Сопоставление с образцом
Аннотации
Обработка XML
Обобщенные типы
- обобщенный класс (generic): - если хотим сравнивать: - если хотим поменять местами, то наоборот T должен быть супертипом: - класс можно обозначить Comparable неявно через implicit - для работы с массивом обобщенных типов, нужен "ClassTag" - можно одновременно указывать обе границы: - обобщенный тип нескольких трейтов: - Ограничение типов
- вариантность и род:
-- ковариантность - тип A производится (может находится только в возвращаемых результатах)
коваринтыми типами не могут быть мутабельные структуры, т.к. элемент и его тип можно поменять
Так, если класс Cat наследуется от класса Animal, то естественно полагать, что перечисление IEnumerable<Cat> будет потомком перечисления IEnumerable<Animal> т.е. +A может быть любым детализирующим подтипом A (doc/cat от Animal)
-- контрвариантность - наоборот - тип только во входных параметрах Принцип subtype по Liskov: обязательно тип и его подтим должны реализовывать одни и теже действия (не должно быть разницы в методах)
-A может быть любым более общим типом A (может принимать Animal , как doc/cat)
но может использоваться и как тип для подтипа в результате: -- вариантность:
все функции варианты:
потребляет A => и возвращает R -- род - тип типов
Int, String, List[Int]: T - простой обобщенный тип
List, Vector : T[_] - обобщенный тип, который принимает параметр подтипа
Map, Function : T[_, _]
пример:
контейнер для int - F может быть List, Vector
также можно накладывать ограничения:
T[X] - контейнер Xов должен быть подтипом Seq[X]
-- если хотим использовать ковариантый результат в параметре, то нужно определить подтип от типа:
B >: ковариантый тип
- псевдоним для типа: и обобщенного: также можно указывать ограничения: типы компоненты - псевдонимы без определения: компонентный тип с уточнением:
Дополнительные типы
Неявные преобразования
- функции с неявными преобразованием параметров Int к Fraction - добавление своей функции в стандартный класс:
-- добавляем функцию read -- неявное преобразование класса File к RichFile: -- вместо функции можно использовать класс: дальше обычно создаем объект File
помещаем функции в файл
и обязательно импортируем без префикса: чтобы минимизировать число неявных преобразований, вызов можно делать внутри кода класса
- необязательные параметры: можно заранее объявить неявный разделитель: - Неявная передача параметра ord в зависимости от типа другого параметра (A) Параметр можно задать явно: sort(xs)(Ordering.Int.reverse)
- из 2 реализаций implicit выбирается наиболее точная:
-- по типу -- по вложенности
- можно посмотреть, что подставится в implicit для заданного типа вызовом:
Конкурентное программирование Scala
Реализация многопоточности в JVM
Атомарные изменчивые переменные
такие переменные изменяются за 1 такт, поэтому не могут меняться одновременно в нескольких потоках
на уровне процессора это операция: compareAndSet - установить значение, если compare выполнилось удачно
полный список описан тут: java.util.concurrent.atomic: AtomicBoolean, AtomicInteger, AtomicLong и AtomicReference
Пример использования: Аналог через synchronized: - блоки синхронизации могут вкладываться друг в друга - это лучше, чем делать 1 глобальный лок: - lazy переменные инициируются 1 раз при первом обращении
для проверки инициализации используются дополнительное атомарное поле-флаг
- muttable объекты нужно использовать с блокировками
- шаблон producer / consumer
между потребителем и производителем находится конкурентная очередь с данными:
- BlockingQueue - реализация в Scala: add/remove/element - с исключениями (poll/ofer/peak - со специальным значение) (take, put - блокирующие)
-- ArrayBlockingQueue - очередь фиксированного размера
-- LinkedBlockingQueue - неограниченная очередь (когда производители гарантированно работают быстрее потребителей)
- Конкурентные объекты: -- concurrent.TrieMap() - позволяет обходить массив по его копии, что не блокирует его самого и не дает получить изменяемые данные
-- ConcurrentSkipListSet[Int] //lock free структуры (оптимистическая блокировка с атомарной проверкой)
Параллельные коллекции данных
- параллельный запуск обработки колекции = вызов функции par -- можно распараллелить цикл: -- преобразование параллельной коллекции в последовательную:
- Left операторы не могут обрабатываться параллельно, т.к. используют результат предыдущего выполнения
- Reduce операции сохраняющий тип данных : (A, A) => A могут быть распараллены
-- Дополнительно должно выполняться условие: a op b == b op a
( к примеру сложение просто парралелиться, но не вычитание, т.к. оно зависит от порядка )
- Если хочется результат другого типа, отличного от параметров, то нужно использовать aggregate - Если нужно, чтобы функция принимала или обычную или параллельную коллекцию, то нужно использовать тип GenSeq это интерфейс обоих контейнеров
- Установка уровня параллельности для блока: --- Установка параллельности для оператора: - для преобразования последовательной коллекции к параллельной используются сплиттеры (надстройка над итератором)
-- линейноссылочные структуры List/Stream не имеют эффективного алгоритма Split
при вызове par у этих структур они сначала преобразуются к обычным массивам
- Пример создания своей параллельной коллекции (в данном случае строки): сплиттер должен быть отнаследован от трейта: - необязательные функции:
psplit - ручное задание размеров для сплита
- пример сплита на 2 части: - psplit создает сплиттер с половиной строки: - Комбинаторы - слияние сплитов обратно при использовании методов трансформации: map, filter groupBy
Объекты Future
- создание отдельного потока из пула потоков: -- другой вариант ожидания: -- альтернативный вариант без блокировки:
по результату работы асинхронно вызовется функция: -- объект Future должен возвращать результат или генерировать exception в случае ошибки
- размер пула по умолчанию = числу ядер
-- если нужно другое значение, то нужно создать свой пул потоков: - если нужно выполнить несколько потоков парраллельно, но данные нужны из обоих, то - еще вариант параллельной обработки нескольких future через map: - или через async: - если функции нужно вызывать последовательно, то это future блоки надо определить как функции, а не переменные как ранее - обработка исключения SQLException внутри Future: - разделение данных на части и их параллельная обработка:
- Через Promise можно вернуть несколько значений из потока: - Пример параллельной merge сортировки через future в GitHub
Реактивное программирование
Программная транзакционная память
Акторы
взаимодействие независимых программ посредством сообщений (модель producer / consumer )
- Общие данные хранятся в памяти актора. Новые сообщения поступают через очередь, и обрабатываются внутри последовательно.
Акторы могут даже находится на разных компах, логика не зависит от их положения
- для работы нужен фреймворк Akka
- Первое: нужно объявить шаблон-класс акторов, который описывает его поведение и порядок приема сообщений
на основе этого шаблона будут создаваться акторы: - создание актора по шаблону: - отсылка сообщений в акторы:
отправка происходит в очередь, не дожидаясь окончания обработки - поведение и состояние акторов. Актор со счетчиком:
работает как конечный автомат
при достижении 0 , актор больше не принимает receive - полностью переключается на done -- переключать во время работы можно на любую функцию, используя: - Иерархия акторов
-- Дочерний актор: -- Родительский вызывает дочерний: -- получить путь в иерархии акторов: -- в случае исключения в акторе, вышестоящий его перезапускает
- события в акторе:
-- preStart - перед началом обработки -- preRestart - перед рестартом в случае ошибки -> еще раз вызывает postStop
-- postRestart - после рестарта, перед обработкой -> еще раз вызывает preStart
-- postStop - окончание обработки
пример использования: - отправка данных в актор с ожиданием ответа сендер отправляет событие и ждет ответа: - Диспетчеризация акторов:
Дочерние акторы можно:
-- перезапустить - restart (стратегия по умолчанию)
-- возобновить без перезапуска - Resume
-- остановить - stop
-- распространить ошибку на все дочерние акторы - Escalate - Удаленное взаимодействие акторов:
-- настраиваем конфиг взаимодействия: -- создаем актор: -- получаем ссылку на удаленный актор "ActorIdentity", после посылаем сообщения: - Минусы:
-- Нельзя посылать один и тот же тип сообщения из разных receiver. Для разграничения нужно будет включать уникальный ключ.
-- сложно настроить ожидание определенной цепочки событий (послать в 2 актора и ждать ответа)
Читайте также: