Перейти к содержимому

Extends scala что это

  • автор:

Почему пингвины падают в пропасть: особенности декомпозиции в Scala

В этой статье наш ведущий инженер по обработке данных Артём Корсаков разбирает некоторые особенности использования Scala и показывает на шуточных примерах «подводные камни», на которые часто натыкаются разработчики.

Как и во многих других языках программирования, в Scala часто используется декомпозиция. По сути, это разбиение сложного типа данных на более простые части и извлечение из них каких-то данных. В Scala 3 для этого используются трейты, а также сочетание сопоставления с образцом (pattern matching) и кейс-классов (case classes).

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

Звучит слишком запутанно? Тогда немного пофантазируем!

Представим себя в роли Адама, решившего дать названия всем животным, и начавшего с птиц. Вот мы встречаем одну птицу, затем другую… с точки зрения программирования этот процесс можно обозначить как работу с потоком данных, в котором каждому элементу нужно присвоить наименование, соответствующее его структуре. И тут возникает первая проблема: мы начинаем думать, а что же такое птица?!

Смотрим на сапсана и видим, что он перелетает из пункта А в пункт Б над бездонной пропастью за 10 секунд и никто, кроме птиц, так больше не может. Крокодил, змея, корова — все падают в пропасть!

Отлично, мы нашли отличительный признак! Попробуем выразить его на Scala:

trait CanBeBird: def wings: Int def flyTime: Int

Ура! Фанфары! Мы знаем, что такое птица! Сейчас начнём раздавать имена…

Сапсан, Орёл, Сокол… — какой поток прекрасных птиц!

trait CanBeBird: def wings: Int def flyTime: Int println(s"Я могу пролететь над бездонной пропастью за $flyTime секунд") object Peregrine extends CanBeBird: def wings: Int = 2 def flyTime: Int = 10 object Eagle extends CanBeBird: def wings: Int = 2 def flyTime: Int = 15 object Falcon extends CanBeBird: def wings: Int = 2 def flyTime: Int = 14 val birds = List(Hummingbird, Eagle, Falcon) // Я могу пролететь над бездонной пропастью за 10 секунд // Я могу пролететь над бездонной пропастью за 15 секунд // Я могу пролететь над бездонной пропастью за 14 секунд

Рано или поздно в этой очереди появится пингвин! Он точно там будет! Без пингвинов жизнь была слишком беззаботной, а этого ни в коем случае нельзя допустить!

Пингвин точно упадёт в пропасть — других вариантов нет!

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

Но ведь пингвин и остальные падшие твари — тоже птицы?! Да, и что же теперь делать?

Есть две распространённые ошибки, совершаемые в похожей ситуации:

1) Выдать исключение или null

object Penguin extends CanBeBird: def wings: Int = 2 def flyTime: Int = throw new IllegalArgumentException("Я упаду в пропасть!")

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

Второй распространённой ошибкой является использование Option , что вроде бы выглядит как-то по Scala’вски. Функциональное программирование и все дела!

Но давайте посмотрим на код:

trait CanBeBird: def wings: Int def flyTime: Option[Int] = None object Hummingbird extends CanBeBird: def wings: Int = 2 override def flyTime: Option[Int] = Some(10) . object Penguin extends CanBeBird: def wings: Int = 2 val birds = List(Hummingbird, Eagle, Falcon, Penguin) birds.foreach < bird =>bird.flyTime match case Some(time) => println(s"Я могу пролететь над бездонной пропастью за $time секунд") case _ => println("А я упаду в пропасть!") > // Я могу пролететь над бездонной пропастью за 10 секунд // Я могу пролететь над бездонной пропастью за 15 секунд // Я могу пролететь над бездонной пропастью за 14 секунд // А я упаду в пропасть!

Проблема решена? Не совсем!

Такой способ приводит к возникновению антипаттерна «God object». Родитель начинает разрастаться до бесконечности и, как правильно заметили в комментарии к онлайн-трансляции Scala-митапа в Музее Криптографии, становится своеобразной википедией. Родитель «может все», тогда как дочерние элементы со временем могут даже не иметь общих методов, хотя наследуются от одного родителя. Дочерние элементы «глушат» родительскую функциональность. И получается, что ключевое слово extends используется с точностью до наоборот — родитель наследует от своего дочернего элемента!

На самом деле, здесь Адаму следовало бы выделить более абстрактный интерфейс для птиц, и для всяких нелетающих использовать именно его:

trait CanBeBird: def wings: Int trait CanBeFlyingBird extends CanBeBird: def wings: Int def flyTime: Int object Hummingbird extends CanBeFlyingBird: def wings: Int = 2 def flyTime: Int = 20 object Penguin extends CanBeBird: def wings: Int = 2 val birds: List[CanBeBird] = List(Hummingbird, Penguin) birds.foreach < case bird: CanBeFlyingBird =>println(s"Я могу пролететь над бездонной пропастью за $ секунд") case _ => println("А я упаду в пропасть!") > // Я могу пролететь над бездонной пропастью за 20 секунд // А я упаду в пропасть!

В этом случае ответственность также перекладывается на клиента, но это делается явно!

Пользователь сам решает, что ему делать с птицей-которая-умеет-летать и просто-птицей . Он не получает сюрпризов в виде исключений, null и прочего — всё поведение кода открыто.

Первый раз в абстрактный класс

Выше мы рассмотрели абстрактный интерфейс, а что же такое абстрактные классы?

Abstract class — это класс, который не может быть инициализирован напрямую, а только через его конкретные подклассы. Он может содержать конструкторы и поля, которые должны быть реализованы в его подклассах.

Давайте отвлечёмся от птиц и представим что-нибудь… более абстрактное!

Допустим, что мы работаем с геометрическими фигурами, и у нас есть класс Shape с подклассами Circle и Rectangle .

abstract class Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape

Теперь мы можем использовать декомпозицию при работе с объектами этих классов. Например, мы можем вычислить площадь фигуры следующим образом:

def area(shape: Shape): Double = shape match < case Circle(radius) =>math.Pi * radius * radius case Rectangle(width, height) => width * height > 

Здесь match используется для сопоставления с образцом. Если объект shape является экземпляром класса Circle , то мы извлекаем радиус и используем его для вычисления площади круга. Если же shape является экземпляром класса Rectangle , то мы извлекаем ширину и длину, а затем используем их для вычисления площади прямоугольника.

С использованием трейтов этот пример выглядел бы следующим образом:

sealed trait Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape

Главное различие между «trait» и «abstract class» заключается в том, что «trait» может быть добавлен к любому классу, даже если этот класс уже наследует другой класс, тогда как «abstract class» может быть наследован только одним классом.

Если совсем упростить, то «trait» — это набор методов и полей, которым нужно следовать, чтобы создавать определённые классы. В отличие от него, «abstract class» — это класс-шаблон, уже содержащий в себе некоторые общие методы и поля, которые будут использоваться в его подклассах.

О том, что ещё может предложить Scala в плане декомпозиции, а также про «эффект трамплина» эксперты «Криптонита» разбирали на Scala-митапе, прошедшем 20 апреля 2023 года в Музее Криптографии. Переходите по ссылке. Нам нужны ваши лайки, а также шпицы и лаппхунды! 🙂

  • scala
  • data engineering
  • Блог компании Криптонит
  • Программирование
  • Scala
  • Data Engineering

Extends scala что это

В этом уроке вы узнаете:

  • Видимое ограничение («Классы-типы»)
  • Типы высшего порядка и специальный полиморфизм
  • F-ограниченный полиморфизм / рекурсивные типы
  • Структурные типы
  • Абстрактные типы членов
  • Тип чисток и манифесты
  • Пример: Finagle

Видимое ограничение («Классы-типы»)

Неявные функции в Scala позволяют использовать функции по требованию, когда это может помочь при выводе типа, например:

scala> implicit def strToInt(x: String) = x.toInt strToInt: (x: String)Int scala> "123" res0: java.lang.String = 123 scala> val y: Int = "123" y: Int = 123 scala> math.max("123", 111) res1: Int = 123

Видимое ограничение, подобно ограничению типа, требует функцию, которая существует для данного типа, например:

scala> class Container[A defined class Container

Это говорит, что A должен быть «видим» подобно Int. Давайте попробуем.

scala> (new Container[String]).addIt("123") res11: Int = 246 scala> (new Container[Int]).addIt(123) res12: Int = 246 scala> (new Container[Float]).addIt(123.2F) :8: error: could not find implicit value for evidence parameter of type (Float) => Int (new Container[Float]).addIt(123.2) ^

Другие ограничения типов

Методы могут запросить конкретные «доказательства» для типа, а именно:

A =:= B A должен быть равен B
A

A должен быть подтипом B
A

A должен выглядеть как B
scala> class Container[A](value: A) < def addIt(implicit evidence: A =:= Int) = 123 + value >defined class Container scala> (new Container(123)).addIt res11: Int = 246 scala> (new Container("123")).addIt :10: error: could not find implicit value for parameter evidence: =:=[java.lang.String,Int]

Кроме того, учитывая наше предыдущее неявное значение, мы можем ослабить ограничение для видимости:

scala> class Container[A](value: A) < def addIt(implicit evidence: A defined class Container scala> (new Container("123")).addIt res15: Int = 246
Обобщенное программирование с помощью видов

В стандартной библиотеке Scala, виды в основном используются для реализации обобщенных функций коллекций. Например, функция «min» (Seq[]), использует эту технику:

def min[B >: A](implicit cmp: Ordering[B]): A = < if (isEmpty) throw new UnsupportedOperationException("empty.min") reduceLeft((x, y) =>if (cmp.lteq(x, y)) x else y) >

Основными преимуществами этого являются:

  • Элементам коллекции не требуется реализовывать Ordered, хотя Ordered по-прежнему использует статическую проверку типов.
  • Вы можете определить свой собственный порядок сортировки без необходимости использовать дополнительную библиотеку:
scala> List(1,2,3,4).min res0: Int = 1 scala> List(1,2,3,4).min(new Ordering[Int] < def compare(a: Int, b: Int) = b compare a >) res3: Int = 4

Небольшое замечание, есть виды в стандартной библиотеке, которые переводят Ordered в Ordering (и наоборот).

trait LowPriorityOrderingImplicits < implicit def ordered[A >
Ограничения контекста и implicitly[]

В Scala 2.8 введена сокращенная форма для передачи и для доступа с использованием неявных аргументов.

scala> def foo[A](implicit x: Ordered[A]) <> foo: [A](implicit x: Ordered[A])Unit scala> def foo[A : Ordered] <> foo: [A](implicit evidence$1: Ordered[A])Unit

Неявные значения могут быть доступны через implicitly

scala> implicitly[Ordering[Int]] res37: Ordering[Int] = scala.math.Ordering$Int$@3a9291cf

В совокупности это часто приводит к меньшему количеству кода, особенно при передаче с использованием видов.

Типы высшего порядка и специальный полиморфизм

Scala может абстрагировать типы «высшего порядка». Это похоже на каррирование функции. Например, в то время как «унарные типы» имеют конструкторы вроде этого:

List[A]

То есть мы должны удовлетворять определенному «уровню» типовых переменных с целью получения конкретных типов (подобно тому, как uncurried функция должна применяться только к одному списку аргументов, при вызове), типам высшего порядка требуется больше:

scala> trait Container[M[_]] < def put[A](x: A): M[A]; def get[A](m: M[A]): A >scala> val container = new Container[List] < def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head >container: java.lang.Object with Container[List] = $anon$1@7c8e3f75 scala> container.put("hey") res24: List[java.lang.String] = List(hey) scala> container.put(123) res25: List[Int] = List(123)

Заметьте, что Container является полиморфным в параметрическом типе («тип контейнер»).

Если мы объединим использование контейнеров с неявными выражениями, мы получим «специальный» полиморфизм: возможность писать обобщенные контейнеры поверх контейнеров.

scala> trait Container[M[_]] < def put[A](x: A): M[A]; def get[A](m: M[A]): A >scala> implicit val listContainer = new Container[List] < def put[A](x: A) = List(x); def get[A](m: List[A]) = m.head >scala> implicit val optionContainer = new Container[Some] < def put[A](x: A) = Some(x); def get[A](m: Some[A]) = m.get >scala> def tupleize[M[_]: Container, A, B](fst: M[A], snd: M[B]) = < | val c = implicitly[Container[M]] | c.put(c.get(fst), c.get(snd)) | >tupleize: [M[_],A,B](fst: M[A],snd: M[B])(implicit evidence$1: Container[M])M[(A, B)] scala> tupleize(Some(1), Some(2)) res33: Some[(Int, Int)] = Some((1,2)) scala> tupleize(List(1), List(2)) res34: List[(Int, Int)] = List((1,2))

F-ограниченный полиморфизм

Часто необходим доступ к конкретному подклассу в (обобщенном) трейте. Например, представьте себе некоторый трейт, который является обобщенным, но может быть сравним с конкретным подклассом данного трейта.

trait Container extends Ordered[Container]

Тем не менее, сейчас требуется сравнение

def compare(that: Container): Int

И поэтому мы не можем получить доступ к конкретному подтипу, например:

class MyContainer extends Container

код не скомпилируется, так как мы определяем Ordered для Container, а не конкретный подтип.

Чтобы это согласовать, мы используем F-ограниченный полиморфизм.

trait Container[A 

Странный тип! Но заметьте, как Ordered параметризован с помощью A, который сам по себе является Container[A]

class MyContainer extends Container[MyContainer]

Они сейчас упорядочены:

scala> List(new MyContainer, new MyContainer, new MyContainer) res3: List[MyContainer] = List(MyContainer@30f02a6d, MyContainer@67717334, MyContainer@49428ffa) scala> List(new MyContainer, new MyContainer, new MyContainer).min res4: MyContainer = MyContainer@33dfeb30

Учитывая, что все они являются подтипами Container[_], мы можем определить другой подкласс и создать смешанный список Container[_]:

scala> class YourContainer extends Container[YourContainer] < def compare(that: YourContainer) = 0 >defined class YourContainer scala> List(new MyContainer, new MyContainer, new MyContainer, new YourContainer) res2: List[Container[_ >: YourContainer with MyContainer : YourContainer with MyContainer 

Обратите внимание, как результирующий тип в настоящее время ограничен снизу YourContainer с MyContainer. Это работа системы вывода типов. Интересно, что этот тип не имеет дополнительного смысла, он только обеспечивает логическую нижнюю границу для списка. Что произойдет, если мы попытаемся использовать Ordered сейчас?

(new MyContainer, new MyContainer, new MyContainer, new YourContainer).min :9: error: could not find implicit value for parameter cmp: Ordering[Container[_ >: YourContainer with MyContainer : YourContainer with MyContainer 

Ordered[] не существует для единого типа. Это слишком плохо.

Структурные типы

Scala имеет поддержку структурных типов — тип выражается интерфейсом structure вместо конкретного типа.

scala> def foo(x: < def get: Int >) = 123 + x.get foo: (x: AnyRef)Int scala> foo(new < def get = 10 >) res0: Int = 133

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

Абстрактные типы членов

В трейте, вы можете оставить тип членов абстрактным.

scala> trait Foo < type A; val x: A; def getX: A = x >defined trait Foo scala> (new Foo < type A = Int; val x = 123 >).getX res3: Int = 123 scala> (new Foo < type A = String; val x = "hey" >).getX res4: java.lang.String = hey

Часто это полезный трюк, когда делается внедрение зависимостей, например.

Вы можете обратиться к абстрактному типу переменной, используя хеш-оператор:

scala> trait Foo[M[_]] < type t[A] = M[A] >defined trait Foo scala> val x: Foo[List]#t[Int] = List(1) x: List[Int] = List(1)

Тип очистки и манифесты

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

scala> class MakeFoo[A](implicit manifest: Manifest[A]) < def make: A = manifest.erasure.newInstance.asInstanceOf[A] >scala> (new MakeFoo[String]).make res10: String =

Пример: Finagle

trait Service[-Req, +Rep] extends (Req => Future[Rep]) trait Filter[-ReqIn, +RepOut, +ReqOut, -RepIn] extends ((ReqIn, Service[ReqOut, RepIn]) => Future[RepOut]) < def andThen[Req2, Rep2](next: Filter[ReqOut, RepIn, Req2, Rep2]) = new Filter[ReqIn, RepOut, Req2, Rep2] < def apply(request: ReqIn, service: Service[Req2, Rep2]) = < Filter.this.apply(request, new Service[ReqOut, RepIn] < def apply(request: ReqOut): Future[RepIn] = next(request, service) override def release() = service.release() override def isAvailable = service.isAvailable >) > > def andThen(service: Service[ReqOut, RepIn]) = new Service[ReqIn, RepOut] < private[this] val refcounted = new RefcountedService(service) def apply(request: ReqIn) = Filter.this.apply(request, refcounted) override def release() = refcounted.release() override def isAvailable = refcounted.isAvailable >>

Можно определить запросы с помощью filter.

trait RequestWithCredentials extends Request < def credentials: Credentials >class CredentialsFilter(credentialsParser: CredentialsParser) extends Filter[Request, Response, RequestWithCredentials, Response] < def apply(request: Request, service: Service[RequestWithCredentials, Response]): Future[Response] = < val requestWithCredentials = new RequestWrapper with RequestWithCredentials < val underlying = request val credentials = credentialsParser(request) getOrElse NullCredentials >service(requestWithCredentials) > >

Обратите внимание, как основной сервис требует определения запроса, и что это проверяется статически. Фильтры можно рассматривать как преобразователи.

Множество фильтров могут быть объединены вместе:

val upFilter = logTransaction andThen handleExceptions andThen extractCredentials andThen homeUser andThen authenticate andThen route

Пишите безопасный код!

Built at @twitter by @stevej, @marius, and @lahosken with much help from @evanm, @sprsquish, @kevino, @zuercher, @timtrueman, @wickman, @mccv and @garciparedes; Russian translation by appigram; Chinese simple translation by jasonqu; Korean translation by enshahar;

Основы модели ООП в Scala

Scala имеет гибкую ООП-модель, которая состоит из трейтов, обжектов, классов и кейс-классов, то есть модель чем-то напоминает аналогичную в Java. Вот, например, объявления класса:

 
class ClassName(field1: Type1, field2: Type2,  , fieldN: TypeN)  // class body > 

Объявленный таким образом класс инстанцируется посредством конструкции new ClassName(field1, field2, … , fieldN). Параметры, которые вводятся в скобках после имени класса, — private-поля класса, но мы можем изменить область их видимости. К примеру, если нам надо обеспечить доступ на чтение извне, то перед именем поля следует поставить модификатор val. Если надо обеспечить запись и чтение — модификатор var. Классы в Scala являются ссылочными типами и наследуют базовый класс AnyRef (неявно). Таким образом, присваивание объекта с мутирующими (var) полями другой переменной лишь копирует ссылку.

Внутри класса мы можем вводить многие вложенные структуры, необходимые для внутреннего использования. При этом по умолчанию все переменные, методы и постоянные, которые объявлены в теле класса, являются публичными. Мы можем их защитить посредством модификаторов private и protected. Что касается вложенных классов, то они извне недоступны.

Вторая ООП-сущность — trait. Trait, в отличие от класса, не может иметь конструктора, следовательно, не может быть инстанцирован. Вдобавок к этому, он способен содержать абстрактные методы, тогда как класс — лишь в том случае, если помечен модификатором abstract.

Рассмотрим синтаксис трейта:

 
trait TraitName  > 

Существует ещё одна ООП-сущность, которую мы рассматривали в этой статье. Речь идёт об object. Его отличительная черта — наличие единственного экземпляра, создаваемого автоматически, то есть он является синглтоном. Лишь object может быть точкой входа в программу. Вдобавок к этому, можно импортировать его вложенные типы, значения и методы в область видимости другого файла. Всё это даёт возможность применять object в качестве средства структурной организации программы, что в свою очередь даёт возможность группировать статические методы и константы.

Особенности наследования в Scala

Особенностей несколько: во-первых, почти всё можно наследовать от нескольких trait посредством синтаксиса extends trait1 with trait2 with trait3 . with traitN.

 
object HelloWorld extends App 

В нашем случае App является предопределённым трейтом, оборачивающим содержимое внутрь метода main.

Зачем нам нужен синтаксис extends … with … with …? Может, мы можем обойтись одним ключевым словом? К сожалению, не можем. Если же мы разрешим множественное наследование, то столкнёмся с проблемой ромбовидного наследования. В языке программирования Scala данная проблема решается путём определения главного трейта, реализация которого и наследуется. Речь идёт о трейте, который идёт первым после extends (кстати, это может быть и не трейт вовсе, а класс либо абстрактный класс).

Также мы не можем наследовать типы от объекта.

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

 
trait T <> val t = new T  // имплементация абстрактных методов > 

Однако писать данный код бывает весьма громоздким занятием. Для этого в Scala есть companion object — объекты-компаньоны. Для них не существует специального ключевого слова, поэтому для их определения надо создать в файле с классом либо трейтом object с тем же именем, что и у класса. У такого обжекта-компаньона вы сможете определить метод apply:

 
trait Foo  def bar: Int > object Foo  def apply(int: Int) = new Foo  override def bar = int > > 

Тогда появится возможность создавать «экземпляры» трейта посредством синтаксиса Foo.apply(1) .

Так как этот код громоздкий, специально для метода apply разрешили не писать его название. Данная «магия» работает для всего, что называют apply в любой сущности. Вот, как это выглядит:

 
Foo(1)

То же самое мы можем делать и с классами. Также следует заметить, что семантика метода apply может существенно отличаться от вышеприведённого примера, ведь никто не ограничивает в том, что именно будет делать этот метод. Данная особенность активно применяется и в стандартных, и в сторонних библиотеках.

Материал написан на основании статьи Ивана Камышана"Основы функционального программирования с примерами на Scala — часть 2".

Советы начинающему скалисту (часть 2)

Сегодня мы обсудим ряд скалических идиом, которые не поместились в первую часть статьи. Мы рассмотрим вопросы интероперации языка с Java и, главное, неправильное использование объектно-ориентированных особенностей Scala.

Структура цикла

  • Часть 1. Функциональная
  • Часть 2. Обо всем и ни о чем

Длина выражений

В Scala практически все является выражением, и даже если что-то возвращает Unit , вы всегда можете получить на выходе ваш () . После длительного программирования на языках, где превалируют операторы (statements), у многих из нас (и я не являюсь исключением) возникает желание запихнуть все вычисления в одно выражение, составив из них длинный-предлинный паровозик. Следующий пример я нагло утащил из Effective Scala. Допустим, у нас есть последовательность кортежей:

val votes = Seq(("scala", 1), ("java", 4), ("scala", 10), ("scala", 1), ("python", 10))

Мы можем лихо её обработать (разбить на группы, просуммировать внутри групп, упорядочить по убыванию) единственным выражением:

val orderedVotes = votes .groupBy(_._1) .map < case (which, counts) =>(which, counts.foldLeft(0)(_ + _._2)) >.toSeq .sortBy(_._2) .reverse

Этот код прост, понятен, выразителен? Возможно — для съевшего собаку скалиста. Однако, если мы разобьём выражение на именованные составляющие, легче станет всем:

val votesByLang = votes groupBy < case (lang, _) =>lang > val sumByLang = votesByLang map < case (lang, counts) =>val countsOnly = counts map < case (_, count) =>count > (lang, countsOnly.sum) > val orderedVotes = sumByLang.toSeq .sortBy < case (_, count) =>count > .reverse

Наверное, этот пример недостаточно нагляден — чего уж, я даже поленился его сам придумать. Но поверьте, мне попадались очень длинные конструкции, которые их авторы даже не удосуживались переносить на несколько строк.

Очень часто в Scala приходят через Spark, а уж используя Spark так и хочется сцепить побольше «вагончиков»-преобразований в длинный и выразительный «поезд». Читать такие выражения сложно, их нить повествования теряется достаточно быстро.

Сверхдлинные выражения и operator notation

Надеюсь, всем известно, что 2 + 2 в Scala является синтаксическим сахаром для выражения 2.+(2) . Этот вид записи именуется операторной нотацией (operator notation). Благодаря ей в языке нет операторов как таковых, а есть лишь методы, пусть и с небуквенными именами, а сама она — мощный инструмент, позволяющий создавать выразительные DSL (собственно, для этого символьная нотация и была добавлена в язык). Вы можете сколь угодно долго записывать вызовы методов без точек и скобочек: object fun arg fun1 arg1 . Это безумно круто, если вы хотите сделать читаемый DSL:

myList should have length 10

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

«Поезда» и postfix notation

Постфиксные операторы, при определенных условиях, способны вскружить голову несчастному парсеру, поэтому в последних версиях Scala эти выражения нужно импортировать явно:

import language.postfixOps

Старайтесь не использовать эту возможность языка и проектировать ваши DSL так, чтобы и вашим пользователям не приходилось ее использовать. Это довольно просто сделать.

Неинициализируемые значения

Scala поддерживает неинициализированные значения. Например, это может вам пригодиться при создании beans. Давайте посмотрим на следующий Java-класс:

class MyClass < // По-умолчанию, любой наследник Object инциализируется в null. // Примитивные типы инициализируются значениями по-умолчанию. String uninitialized; >

Такого же поведения мы можем добиться и от Scala:

class MyClass < // Синтаксис с нижним подчеркиванием говорит Scala, что // данное поле не будет инциализировано. var uninitialized: String = _ >

Пожалуйста, не делайте этого бездумно. Инициализируйте значения везде, где можете. Используйте эту языковую конструкцию только если используемый вами фреймворк или библиотека яростно на этом настаивают. При неаккуратном использовании вы можете получить тонны NullPointerException . Однако знать об этой функции следует: однажды подобное знание сэкономит время. Если вы хотите отложить инициализацию, используйте ключевое слово lazy .

Никогда не используйте null

  • Всегда инициализируйте значения.
  • Оборачивайте Nullable , которые могут прийти извне в Option .
  • Не возвращайте null : используйте Option , Either , Try и др.
  • Видите предпосылки для появления null — быстрее исправляйте, пока ваши коллеги на радостях не завезли в проект специально предназначенный для борьбы с NPE язык.

Иногда встречаются ситуации, когда null-значения являются частью модели. Возможно, эта ситуация возникла ещё задолго до вашего прихода в команду, а уж тем более задолго до внедрения Scala. Как говорится: если пьянку нельзя предотвратить, ее следует возглавить. И в этом вам поможет паттерн, именуемый Null Object. Зачастую это всего-лишь еще один case-класс в ADT:

sealed trait User case class Admin extends User case class SuperUser extends User case class NullUser extends User

Что мы получаем? Null, пользователя и типобезопасность.

О перегрузках

Методы

В Scala существует возможность перегрузки конструкторов классов. И это — не лучший способ решить проблему. Скажу больше, это — не идиоматичный способ решения проблемы. Если говорить о практике, эта функция полезна, если вы используете Java-reflection и ваш Scala-код вызывается из Java или вам необходимо такое поведение (а почему бы в таком случае не сделать Builder)? В остальных случаях лучшая стратегия — создание объекта-компаньона и определение в нем нескольких методов apply .

Наиболее примечательны случаи перегрузки конструкторов из-за незнания о параметрах по-умолчанию (default parameters).

Совсем недавно, я стал свидетелем следующего безобразия:

// Все включено! case class Monster (pos: Position, health: Int, weapon: Weapon)

Ларчик открывается проще:

case class Monster( pos: Position, health: Short = 100, weapon: Weapon = new Claws )

Хотите наградить вашего монстра базукой? Да, не проблема:

val aMonster = Monster(Position(300, 300, 20), weapon = new Bazooka)

Мы сделали мир лучше, монстра — миролюбивее, а заодно перестали перегружать все, что движется. Миролюбивее? Определенно. Ведь базука — это еще и музыкальный инструмент (Википедия об этом умалчивать не станет).

Сказанное относится не только к конструкторам: люди часто перегружают и обычные методы (там где этого можно было избежать).

Перегрузка операторов

Считается достаточно противоречивой фичей Scala. Когда я только-только погрузился в язык, перегрузка операторов использовалась повсюду, всеми и везде, где только можно. Сейчас эта фича стала менее популярна. Изначально перегрузка операторов была сделана, в первую очередь, для того, чтобы иметь возможность составлять DSL, как в Parboiled, или роутинг для akka-http.

Не перегружайте операторы без надобности, и если считаете, что эта надобность у вас есть, то все-равно не перегружайте.

А если перегружаете (вам нужен DSL или ваша библиотека делает нечто математическое (или трудновыразимое словами)), обязательно дублируйте оператор функцией с нормальным именем. И думайте о последствиях. Так, Благодаря scalaz оператор |@| (Applicative Builder) получил имя Maculay Culkin. А вот и фотография "виновника":

Шокированный Maculay Culkin

Безусловно, после того, как вы многократно перегрузите конструкторы, вам для довершения картины захочется налепить геттеров и сеттеров.

О геттерах и сеттерах

Scala предоставляет отличное взаимодействие с Java. Она также способна облегчить вам жизнь при дизайне так называемых Beans. Если вы не знакомы с Java или концепцией Beans, возможно, вам следует с ней ознакомиться.

Слышали ли вы о Project Lombok? В стандартной библиотеке Scala имеется схожий механизм. Он именуется BeanProperty . Все, что вам нужно, — создать bean и добавить аннотацию BeanProperty к каждому полю, для которого хотите создать getter или setter.

Для того чтобы получить имя вида isProperty для переменных булева типа, следует добавить scala.beans.BooleanBeanProperty в вашу область видимости.

Аннотацию @BeanProperty можно так же использовать и для полей класса:

import scala.beans. class MotherInLaw < // По закону, она может сменить имя: @BeanProperty var name = "Megaera" // А эти ребята имеют свойство плодиться. @BeanProperty var numberOfCatsSheHas = 0 // Но некоторые вещи неизменны. @BooleanBeanProperty val jealous = true >

Для case class-ов тоже работает:

import scala.beans.BeanProperty case class Dino(@BeanProperty name: String, @BeanProperty var age: Int)

Поиграем с нашим динозавром:

// Начнем с того, что он не так стар как вы думаете val barney = Dino("Barney", 29) barney.setAge(30) barney.getAge // res4: Int = 30 barney.getName // res14: String = Barney

В виду того, что мы не сделали name переменной, при попытке использовать сеттер, мы получим следующее:

barney.setName :15: error: value setName is not a member of Dino barney.setName

Кстати о case-классах

Появление case-классов — это прорыв для JVM-платформы. В чем же их основное преимущество? Правильно, в их неизменяемости (immutability), а также наличию готовых equals , toString и hashCode . Однако, зачастую и в них можно встретить подобное:

// Внимание на var. case class Person(var name: String, var age: Int)

Иногда case-классы приходится-таки делать изменяемыми: например, если вы, имитируете beans, как в примере выше.

Но зачастую подобное случается тогда, когда глубокий джуниор не понимает, что такое имутабельность. С разработчиками уровнем повыше бывает не менее интересно, ведь они прекрасно осознают что делают:

case class Person (name: String, age: Int)

Однако про метод copy знают не все. Это норма. Подобное мне доводилось наблюдать не раз, чего уж там, в свое время я сам так хулиганил. Работает copy аналогично своему тезке, который определен для кортежей:

// Обновили возраст, получили новый инстанс. person.copy(age = 32)

О размерах case-классов

Иногда case-классы имеют свойство раздуваться до 15–20 полей. До появления Scala 2.11 этот процесс хоть как-то ограничивался 22 элементами. Но сейчас ваши руки развязаны:

case class AlphabetStat ( a: Int, b: Int, c: Int, d: Int, e: Int, f: Int, g: Int, h: Int, i: Int, j: Int, k: Int, l: Int, m: Int, n: Int, o: Int, p: Int, q: Int, r: Int, s: Int, t: Int, u: Int, v: Int, w: Int, x: Int, y: Int, z: Int )

Хорошо, я вам наврал: руки, конечно, стали свободнее, однако ограничения JVM никто не отменял.

Большие case-классы это плохо. Это очень плохо. Бывают ситуации, когда этому есть оправдание: предметная область, в которой вы работаете, не допускает агрегации, и структура представляется плоской; вы работаете с API, спроектированным глубокими идиотами, которые сидят на сильнодействующих транквилизаторах.

И знаете, чаще всего приходится иметь дело со вторым вариантом. Хочется, чтобы case-классы легко и непринужденно укладывались на API. И я вас пойму, если так.

Но я перечислил только уважительные оправдания монструозности ваших case-классов. Есть и наиболее очевидная: для того, чтобы обновить поле, глубоко запрятанного вглубь вложенных классов, приходится очень сильно помучиться. Каждый case-класс надо старательно разобрать, подменить значение и собрать. И от этого недуга есть средство: вы можете использовать линзы (lenses).

Почему линзы называются линзами? Потому что они способны сфокусироваться на главном. Вы фокусируете линзу на определенную часть структуры, и получаете ее, вместе с возможностью ее (структуру) обновить. Для начала объявим наши case-классы:

case class Address(street: String, city: String, postcode: String) case class Person(name: String, age: Int, address: Address)

А теперь заполним их данными:

val person = Person("Joe Grey", 37, Address("Southover Street", "Brighton", "BN2 9UA"))

Создаем линзу для улицы (предположим что наш персонаж захотел переехать):

import shapeless._ val streetLens = lens[Person].address.street

Читаем поле (прошу заметить что строковый тип будет выведен автоматически):

val street = streetLens.get(person) // "Southover Street"

Обновляем значение поля:

val person1 = streetLens.set(person)("Montpelier Road") // person1.address.street == "Montpelier Road"

Пример был нагло украден «из отсюда»

Аналогичную операцию вы можете совершить и над адресом. Как видите, это достаточно просто. К сожалению, а может быть и к счастью, Scala не имеет встроенных линз. Поэтому вам придется использовать стороннюю библиотеку. Я бы рекомендовал вам использовать shapeless . Собственно, приведенный выше пример, был написан с помощью этой весьма доступной для начинающего скалиста библиотеки.

Существует множество других реализаций линз, если хотите, вы можете использовать scalaz, monocle. Последняя предоставляет более продвинутые механизмы использования оптики, и я бы рекомендовал ее к дальнейшему использованию.

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

Переизобретение enum'ов

Берем опытного Java-разработчика и заставляем его писать на Scala. Не проходит и пары дней, как он отчаянно начинает искать enum'ы. Не находит их и расстраивается: в Scala нет ключевого слова enum или, хотя бы, enumeration . Далее есть два варианта событий: или он нагуглит идиоматичное решение, или начнет изобретать свои перечисления. Часто лень побеждает, и в результате мы видем вот это:

object Weekdays < val MONDAY = 0 // догадайтесь что будет дальше. >

А дальше то что? А вот что:

if (weekday == Weekdays.Friday)

Что не так? В Scala есть идиоматичный способ создания перечислений, именуется он ADT (Algebraic Data Types), по-русски алгебраические типы данных. Используется, например в Haskell. Вот как он выглядит:

sealed trait TrafficLight case object Green extends TrafficLight case object Yellow extends TrafficLight case object Red extends TrafficLight case object Broken extends TrafficLight

Многословно, самодельное перечисление, конечно, было короче. Зачем столько писать? Давайте объявим следующую функцию:

def tellWhatTheLightIs(tl: TrafficLight): Unit = tl match < case Red =>println("No cars go!") case Green => println("Don't stop me now!") case Yellow => println("Ooohhh you better stop!") >
warning: match may not be exhaustive. It would fail on the following input: Broken def tellWhatTheLightIs(tl: TrafficLight): Unit = tl match < ^ tellWhatTheLightIs: (tl: TrafficLight)Unit

Мы получаем перечисление, свободное от привязки к каким либо константам, а также проверку на полноту сопоставления с образцом. И да, если вы используете «enum для бедных», как их обозвал один мой небезызвестный коллега, используйте сопоставление с образцом. Это наиболее идиоматичный способ. Стоит заметить, об этом упоминается в начале книги Programming in Scala. Не каждая птица долетит до середины Днепра, так же как и не каждый скалист прочтет Magnum Opus.

Неплохо про алгебраические типы данных рассказано, как ни странно, в Википедии. Касательно Scala, есть достаточно доступный пост и презентация, которая, возможно, покажется вам интересной.

Избегайте булевых аргументов в сигнатурах функций

Признайтесь, вы писали методы, которые в качестве аргумента принимают Boolean? В случае с Java ситуация вообще катастрофичная:

PrettyPrinter.print(text, 1, true)

Что может означать 1? Доверимся интуиции и предположим что это количество копий. А за что отвечает true ? Это может быть что угодно. Ладно, сдаюсь, схожу в исходники и посмотрю, что это.

В Scala вы можете использовать ADT:

def print(text: String, copies: Int, wrapWords: WordWrap)

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

// К интам тоже применимо, // а вдруг это не количество копий, а отступы? PrettyPrinter.print(text, copies = 1, WordWrap.Enabled)

Об итерации

Рекурсия лучше

Хвостовая рекурсия работает быстрее, чем большинство циклов. Если она, конечно, хвостовая. Для уверенности используйте аннотацию @tailrec . Ситуации бывают разными, не всегда рекурсивное решение оказывается простым доступным и понятным, тогда используйте while . В этом нет ничего зазорного. Более того, вся библиотека коллекций написана на простых циклах с предусловиями.

For comprehensions не для итерации (по индексам)

Главное, что вам следует знать про генераторы списков, или, как их еще Называют, «for comprehensions», — это то, что основное их предназначение — не в реализации циклов.

Более того, использование этой конструкции для итерации по индексам будет достаточно дорогостоящей процедурой. Цикл while или использование хвостовой рекурсии — намного дешевле. И нагляднее.

«For comprehension» представляет собой синтаксический сахар для методов map , flatMap и withFilter . Ключевое слово yield используется для последующей агрегации значений в результирующей структуре. Используя «for comprehension» вы, на самом деле, используете те же комбинаторы, просто в завуалированой форме. Используйте их напрямую:

// хорошо 1 to 10 foreach println // плохо for (i 

Помимо того, что вы вызвали тот же код, вы еще и добавили некую переменную i , которой совершенно здесь не место. Если вам нужна скорость, используйте цикл while .

Об именах переменных

Занимательная история имен переменных в формате вопрос-ответ:

Вопрос: Откуда вообще взялись i , j , k в качестве параметров циклов?
Ответ: Из математики. А в программирование они попали благодаря фортрану, в котором тип переменной определяется ее именем: если первая буква имени начинается с I, J, K, L, M, или N, это автоматически означает, что переменная принадлежит к целочисленному типу. В противном случае, переменная будет считаться вещественной (можно использовать директиву IMPLICIT для того, чтобы изменить тип, устанавливаемый по умолчанию).

И этот кошмар живет с нами вот уже почти 60 лет. Если вы не перемножаете матрицы, то использованию i , j и k даже в Java нет оправдания. Используйте index , row , column — все что угодно. А вот если вы пишете на Scala, старайтесь вообще избегать итерации с переменными внутри for . От лукваого это.

Дополнением к этому разделу будет видео, в котором подробно рассказывается все, что вы хотели знать про генераторы списков.

О выражениях

Не используйте return

В Scala почти все является выражением. Исключение составляет return , который не следует использовать ни при каких обстоятельствах. Это не опциональное слово, как думают многие. Это конструкция которая меняет семантику программы. Подробнее об этом можете прочитать здесь

Не используйте метки

Представьте себе, в Scala есть метки. И я понятия не имею, зачем они туда были добавлены. До выхода Scala 2.8 по данному адресу располагалась еще и метка continue , позже она была устранена.

К счастью, метки не являются частью языка, а реализованы при помощи выбрасывания и отлова исключений (о том, что делать с исключениями, мы поговорим далее в этой статье).

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

Этот пример взят отсюда:

breakable < for (i 4) break // выскочить из цикла. > >

Об исключительных ситуациях

Это исключительная тема заслуживает большой и отдельной статьи. Возможно, даже серии статей. Рассказывать об этом можно долго. Во-первых, потому, что Scala поддерживает несколько принципиально разных подходов к обработке исключений. В зависимости от ситуации, вы можете выбрать тот, который подходит лучше всего.

В Scala нет checked exceptions. Так что если где-то у нас исключения и могут возникнуть — обязательно их обрабатывайте. И самое главное, не бросайтесь исключениями. Да, есть ситуации когда зеленые монстры вынуждают вас это делать. И более того, у зеленых монстров и их почитателей это вообще является нормой. Во всех остальных случаях — не бросайтесь исключениями. То, что в свое время Джоэл Спольски писал применительно к C++ и Java, к Scala применимо даже в большей степени. И в первую очередь именно из-за ее функциональной природы. Метки и goto недопустимы в функциональном программировании. Исключения ведут себя схожим образом. Бросив исключение, вы прерываете flow. Но, как уже было сказано выше, ситуации бывают разными, и если ваш фреймворк этого требует — Scala дает такую возможность.

Вместо того, чтобы возбуждать исключения, вы можете использовать Validation из scalaz, scala.util.Try , Either . Можно использовать и Option , если вам не жалко людей, которые будут поддерживать ваш код. И это будет все-равно лучше, чем бросаться исключениями.

Структурные типы

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

? extends App

Знаете что не так с этим кодом:

object Main extends App

Конкретно с этим примером «все так». Все будет хорошо и прекрасно работать, пока вы не усложните код достаточно, чтобы встретиться с непредсказуемым поведением. И виной тому один трейт из стандартной библиотеки. Он называется DelayedInit . Прочитать о нем вы можете здесь. Трейт App , который вам предлагается расширить в большинстве руководств, расширяет трейт DelayedInit . Подробнее об App в документации.

It should be noted that this trait is implemented using the DelayedInit functionality, which means that fields of the object will not have been initialized before the main method has been executed.

Следует учесть, что данный трейт реализован с использованием функциональности DelayedInit, что означает то, что поля объекта не будут проинициализированны до выполнения метода main.

В будущем это обещают исправить:

Future versions of this trait will no longer extend DelayedInit.

Плохо ли использовать App ? В сложных многопоточных приложениях я бы не стал этого делать. А если вы пишете «Hello world»? Почему бы нет. Я стараюсь лишний раз не связываться и использую традиционный метод main .

Коллекции

Очень часто в коде можно увидеть эмуляцию функций стандартной библиотеки Scala. Приведу простой пример:

tvs.filter(tv => tv.displaySize == 50.inches).headOption

Тоже самое, только короче:

tvs.find(tv => tv.displaySize == 50.inches)

Подобные «антипаттерны» не редкость:

list.size = 0 // плохо list.isEmpty // ok !list.empty // плохо list.nonEmpty // ok tvs.filter(tv => !tv.supportsSkype) // плохо tvs.filterNot(tv => tv.supportsSkype) // ok

Конечно, если вы используете IntelliJ IDEA, она вам обязательно подскажет наиболее эффективную комбинацию методов. Scala IDE, насколько мне известно, так не умеет.

О неэффективном использовании библиотеки коллекций Scala можно рассказывать сколь угодно долго. И это уже очень не плохо сделал Павел Фатин в своей статье Scala collections Tips and Tricks, с которой я вам очень рекомендую ознакомиться. И да, старайтесь не вызывать элементы коллекций по индексам. Нехорошо это.

Список литературы

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

Книги

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

Официальная документация

Статьи

  • Effective Scala, для которой существует и перевод на русский, хотя я, безусловно, советую вам ознакомиться с оригиналом.
  • Scala Collections Tips and Tricks Павла Фатина.
  • О том, чего лучше не делать в Scala, очень доступно изложено здесь.
  • Scala Collections Tips and Tricks.

Видео

  • Scala with style — доклад создателя языка о том, как идиоматично писать на Scala.
  • Martin Odersky, Scala — the Simple Parts
  • Daniel Spiewak, May Your Data Ever Be Coherent
  • For: What is it good for? — выступление, посвященное подробному разбору «for comprehensions».

Благодарности

Автор хотел бы выразить свою признательность

  • Владу Ледовских — за вычитку,
  • Павлу Кретову (@firegurafiku) — за безуспешные попытки привести этот текст к литературной норме, а также за помощь с разделом о typedef в первой части статьи,
  • Арсению Жижелеву (@primetalk) — за внесение многочисленных уточнений в изначальный текст,
  • Семёну Попугаеву (@senia) — за найденные неточности.

Отдельное спасибо EDU-отделу DataArt и всем тем, кто, проходя наши курсы по Scala, подталкивал меня к написанию этой статьи. Спасибо вам, уважаемые читатели, за то, что дочитали (или хотя бы промотали) до конца.

  • Программирование
  • Scala
  • Функциональное программирование

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *