Extends scala что это
Перейти к содержимому

Extends scala что это

  • автор:

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;

Extends scala что это

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

  • Метод apply
  • Объекты
  • Функции, тоже являются Объектами
  • Пакеты
  • Сопоставление с образцом
  • Case классы
  • try-catch-finally

Метод apply

Метод apply – это синтаксический сахар, который применяется для класса или объекта с единственной целью.

object FooMaker < def apply() = new Foo >scala> class Bar < | def apply() = 0 | >defined class Bar scala> val bar = new Bar bar: Bar = Bar@47711479 scala> bar() res8: Int = 0

Здесь наш экземпляр объекта выглядит так, будто мы просто вызываем метод, но это не так. Подробнее об этом позже!

Объекты

Объекты используются для хранения одного экземпляра класса. Чаще всего они используются с фабриками объектов.

object Timer < var count = 0 def currentCount(): Long = < count += 1 count >>

Как можно это использовать?

scala> Timer.currentCount() res0: Long = 1

Классы и Объекты могут иметь похожие имена. В этом случае Объект называется ‘Объект-спутник’(Companion Object). Чаще всего мы будем использовать Объекты-спутники с Фабриками объектов.

Далее представлен простой пример, который показывает, как можно использовать Объект-спутник, для того чтобы исключить необходимость в использовании ключевого слова ‘new’ для создания экземпляра объекта.

class Bar(foo: String) object Bar

Функции, тоже являются Объектами

В Scala, мы часто говорим об объектно-функциональном стиле. Что это значит? Чем на самом деле является Функция?

Функция – это набор трейтов. В частности, функция, которая принимает один аргумент является экземпляром трейта Function1. Этот трейт определяет метод apply() , который является синтаксическим сахаром, о нем мы узнали ранее, он позволяет вам вызывать объект, словно он является функцией.

scala> object addOne extends Function1[Int, Int] < | def apply(m: Int): Int = m + 1 | >defined module addOne scala> addOne(1) res2: Int = 2

Существует функция с 22 аргументами. Почему с 22? Это произвольное магическое число. Я никогда не нуждался в функции с более чем 22 аргументами.

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

Означает ли это, что каждый раз, когда вы определяете метод в своем классе, вы фактически получаете экземпляр Function*? Нет, методы в классах – это просто методы. Методы-одиночки, определенные в REPL будут экземплярами Function*.

Классы могут расширять Function* и в этих случаях они могут быть вызваны при помощи ().

scala> class AddOne extends Function1[Int, Int] < | def apply(m: Int): Int = m + 1 | >defined class AddOne scala> val plusOne = new AddOne() plusOne: AddOne = scala> plusOne(1) res0: Int = 2

Запись extends Function1[Int, Int] мы можем переписать, используя extends (Int => Int)

class AddOne extends (Int => Int)

Пакеты

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

package com.twitter.example

В верхней части файла объявляется все, что будет в этом пакете.

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

package com.twitter.example object colorHolder

После этого у вас есть доступ к членам пакета напрямую

println("the color is: " + com.twitter.example.colorHolder.BLUE)

Обратите внимание, что Scala REPL говорит вам когда вы объявляете объект:

scala> object colorHolder < | val Blue = "Blue" | val Red = "Red" | >defined module colorHolder

Это дает вам небольшую подсказку, которую разработчики Scala используют для проектирования объектов, которые станут частью модульной системы Scala.

Сопоставление с образцом

Одна из самых часто используемых возможностей Scala.

val times = 1 times match < case 1 =>"one" case 2 => "two" case _ => "some other number" >

Сопоставление с использованием условий

times match < case i if i == 1 =>"one" case i if i == 2 => "two" case _ => "some other number" >

Заметьте, как мы пытаемся поймать значение переменной ‘i’.

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

Сопоставление типов

Вы можете использовать match , чтобы управлять значениями типов различными способами.

def bigger(o: Any): Any = < o match < case i: Int if i < 0 =>i - 1 case i: Int => i + 1 case d: Double if d < 0.0 =>d - 0.1 case d: Double => d + 0.1 case text: String => text + "s" > >

Сопоставление методов класса

Вспомните про наш калькулятор, который мы рассматривали ранее.

Давайте проведем классификацию по типам.

def calcType(calc: Calculator) = calc match < case calc.brand == "HP" && calc.model == "20B" =>"financial" case calc.brand == "HP" && calc.model == "48G" => "scientific" case calc.brand == "HP" && calc.model == "30B" => "business" case _ => "unknown" >

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

Case Классы

Case классы используются для удобного хранения и поиска соответствий по содержимому класса. Вы можете создавать их без использования ‘new’.

scala> case class Calculator(brand: String, model: String) defined class Calculator scala> val hp20b = Calculator("HP", "20b") hp20b: Calculator = Calculator(hp,20b)

У case классов есть метод ToString, работающий автоматически, и который опирается на аргументы конструктора.

scala> val hp20b = Calculator("HP", "20b") hp20b: Calculator = Calculator(hp,20b) scala> val hp20B = Calculator("HP", "20b") hp20B: Calculator = Calculator(hp,20b) scala> hp20b == hp20B res6: Boolean = true

case классы могут иметь методы, как и обычные классы.

Case Классы и сопоставление с образцом

case классы предназначены для использования вместе с сопоставлением с образцом. Давайте упростим наш классификатор из примера с калькулятором.

val hp20b = Calculator("HP", "20B") val hp30b = Calculator("HP", "30B") def calcType(calc: Calculator) = calc match < case Calculator("HP", "20B") =>"financial" case Calculator("HP", "48G") => "scientific" case Calculator("HP", "30B") => "business" case Calculator(ourBrand, ourModel) => "Calculator: %s %s is of unknown type".format(ourBrand, ourModel) >

А это другой способ для последнего сопоставления

case Calculator(_, _) => "Calculator of unknown type"

мы можем не объявлять, что это Calculator совсем.

case _ => "Calculator of unknown type"

Или мы можем связать найденное значение с другим именем

case c@Calculator(_, _) => "Calculator: %s of unknown type".format(c)

Исключения

Исключения доступны в Scala при использовании синтаксиса try-catch-finally, который использует сопоставление с образцом.

try < remoteCalculatorService.add(1, 2) >catch < case e: ServerIsDownException =>log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.") > finally

try тоже ориентирован на выражения

val result: Int = try < remoteCalculatorService.add(1, 2) >catch < case e: ServerIsDownException => < log.error(e, "the remote calculator service is unavailble. should have kept your trustry HP.") 0 >> finally

Этот код не является примером прекрасного стиля программирования, а просто пример того, как try-catch-finally вычисляет выражения, подобно всему остальному в Scala.

Finally будет вызван после того, как исключение будет обработано.

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;

Композиция классов с трейтами

Примеси (Mixin) - это трейты, которые используются для создания класса.

  • Scala 2
  • Scala 3
abstract class A  val message: String > class B extends A  val message = "I'm an instance of class B" > trait C extends A  def loudMessage = message.toUpperCase() > class D extends B with C val d = new D println(d.message) // I'm an instance of class B println(d.loudMessage) // I'M AN INSTANCE OF CLASS B 

У класса D есть суперкласс B и трейт C . Классы могут иметь только один суперкласс, но много трейтов (используя ключевое слово extends и with соответственно). Трейты и суперкласс могут иметь один и тот же супертип.

abstract class A: val message: String class B extends A: val message = "I'm an instance of class B" trait C extends A: def loudMessage = message.toUpperCase() class D extends B, C val d = D() println(d.message) // I'm an instance of class B println(d.loudMessage) // I'M AN INSTANCE OF CLASS B 

У класса D есть суперкласс B и трейт C . Классы могут иметь только один суперкласс, но много трейтов (используя ключевое слово extends и разделитель , соответственно). Трейты и суперкласс могут иметь один и тот же супертип.

Теперь давайте рассмотрим более интересный пример, начиная с абстрактного класса:

  • Scala 2
  • Scala 3
abstract class AbsIterator  type T def hasNext: Boolean def next(): T > 
abstract class AbsIterator: type T def hasNext: Boolean def next(): T 

Класс имеет абстрактный тип T и методы стандартного итератора.

Далее создаем конкретную реализацию класса (все абстрактные члены T , hasNext , и next должны быть реализованы):

  • Scala 2
  • Scala 3
class StringIterator(s: String) extends AbsIterator  type T = Char private var i = 0 def hasNext = i  s.length def next() =  val ch = s charAt i i += 1 ch > > 
class StringIterator(s: String) extends AbsIterator: type T = Char private var i = 0 def hasNext = i  s.length def next() = val ch = s charAt i i += 1 ch 

StringIterator принимает String и может быть использован для обхода по строке (например, чтоб проверить содержит ли строка определенный символ).

Теперь давайте создадим трейт который тоже наследуется от AbsIterator .

  • Scala 2
  • Scala 3
trait RichIterator extends AbsIterator  def foreach(f: T => Unit): Unit = while (hasNext) f(next()) > 

У этого трейта реализован метод foreach , который постоянно вызывает переданную ему функцию f: T => Unit на каждом новом элементе ( next() ) до тех пор пока в итераторе содержатся элементы ( while (hasNext) ). Поскольку RichIterator - это трейт, ему не нужно реализовывать элементы абстрактного класса AbsIterator .

trait RichIterator extends AbsIterator: def foreach(f: T => Unit): Unit = while hasNext do f(next()) 

У этого трейта реализован метод foreach , который постоянно вызывает переданную ему функцию f: T => Unit на каждом новом элементе ( next() ) до тех пор пока в итераторе содержатся элементы ( while hasNext ). Поскольку RichIterator - это трейт, ему не нужно реализовывать элементы абстрактного класса AbsIterator .

Мы бы хотели объединить функциональность StringIterator и RichIterator в один класс.

  • Scala 2
  • Scala 3
class RichStringIter extends StringIterator("Scala") with RichIterator val richStringIter = new RichStringIter richStringIter.foreach(println) 
class RichStringIter extends StringIterator("Scala"), RichIterator val richStringIter = RichStringIter() richStringIter.foreach(println) 

Новый класс RichStringIter включает StringIterator как суперкласс и RichIterator как трейт.

Используя только одиночное наследование мы бы не смогли добиться того же уровня гибкости.

Contributors to this page:

Contents
  • Введение
  • Основы
  • Единобразие типов
  • Классы
  • Значения Параметров По умолчанию
  • Именованные Аргументы
  • Трейты
  • Кортежи
  • Композиция классов с трейтами
  • Функции Высшего Порядка
  • Вложенные Методы
  • Множественные списки параметров (Каррирование)
  • Классы Образцы
  • Сопоставление с примером
  • Объекты Одиночки
  • Регулярные Выражения
  • Объект Экстрактор
  • Сложные for-выражения
  • Обобщенные Классы
  • Вариантность
  • Верхнее Ограничение Типа
  • Нижнее Ограничение Типа
  • Внутренние классы
  • Члены Абстрактного Типа
  • Составные Типы
  • Самоописываемые типы
  • Контекстные параметры, также известные, как неявные параметры
  • Неявные Преобразования
  • Полиморфные методы
  • Выведение Типа
  • Операторы
  • Вызов по имени
  • Аннотации
  • Пакеты и Импорт
  • Объекты Пакета
  • English
  • Bosanski
  • Español
  • Français
  • 한국어
  • Português (Brasil)
  • Polski
  • 中文 (简体)
  • ภาษาไทย
  • Русский
  • 日本語

Советы начинающему скалисту (часть 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 не будет опубликован. Обязательные поля помечены *