Статьи

Комплексные числа в Scala

обзор

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

было бы невозможно с другими языками (Java), такими как операторная арифметика, плавное преобразование между сложными и действительными числами и «свободное» равенство и сравнение. Complex_number_illustration В этом посте я хотел бы воспроизвести эту часть моего выступления. Если вы интересуетесь Scala, но еще не освоили язык, это может быть хорошим знакомством с краткостью и мощью этого замечательного языка программирования.

Отправная точка

Наша отправная точка довольно проста:

1
class Complex(val re: Double, val im: Double)

Единственная строка выше — это полное определение класса. У него есть два поля Double , которые являются открытыми (так как это по умолчанию в Scala) и неизменяемыми (из-за ключевого слова val ). Вышеприведенная строка также неявно определяет конструктор с двумя аргументами по умолчанию, так что экземпляры Complex уже могут быть созданы и инициализированы. Давайте сделаем это в интерпретаторе Scala:

1
2
scala> val x = new Complex(1, 2)
x: Complex = Complex@3997ca20

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

Переопределяющие методы

Строковое представление по умолчанию для Complex выше довольно недружелюбно. Было бы намного лучше, если бы он содержал членов класса в формате, подходящем для комплексного числа. Чтобы достичь этого, мы, конечно, переопределим метод toString который наш класс наследует от Any , корня иерархии классов Scala.

1
2
3
4
class Complex(val re: Double, val im: Double) {
  override def toString =
    re + (if (im < 0) "-" + -im else "+" + im) + "*i"
}

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

1
2
scala> val x = new Complex(1, 2)
x: Complex = 1.0+2.0*i

Добавление методов и операторов

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

1
2
3
4
class Complex(val re: Double, val im: Double) {
  def add(c: Complex) = new Complex(re + c.re, im + c.im)
  ...
}

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

1
2
3
4
5
6
7
8
scala> val x = new Complex(1, 2)
x: Complex = 1.0+2.0*i
 
scala> val y = new Complex(3, 4)
y: Complex = 3.0+4.0*i
 
scala> x.add(y)
res0: Complex = 4.0+6.0*i

В Scala мы также можем вызывать наш метод, как и любой другой метод, используя операторную нотацию , с тем же результатом:

1
2
scala> x add y
res1: Complex = 4.0+6.0*i

А так как у нас есть операторная нотация, мы могли бы также вызвать наш метод + , а не add . Да, это возможно в Scala.

1
2
3
4
class Complex(val re: Double, val im: Double) {
  def +(c: Complex) = new Complex(re + c.re, im + c.im)
  ...
}

Теперь добавление x и y может быть выражено просто как:

1
2
scala> x + y
res2: Complex = 4.0+6.0*i

Если вы знакомы с такими языками, как C ++, это может выглядеть как перегрузка операторов . Но на самом деле неверно говорить, что в Scala есть перегрузка операторов. Вместо этого у Scala вообще нет операторов. Каждая оператороподобная конструкция, включая арифметические операции над простыми типами, на самом деле является вызовом метода. Это, конечно, гораздо более согласованно и проще в использовании, чем традиционная перегрузка операторов, которая рассматривает операторов как особый случай. В финальной версии нашего класса Complex мы добавим методы оператора -, * и / для других арифметических операций.

Перегрузка конструкторов и методов

Комплексные числа с нулевой мнимой частью на самом деле являются действительными числами, поэтому действительные числа можно рассматривать просто как особый тип комплексных чисел. Следовательно, должна быть возможность плавно преобразовывать эти два вида чисел и смешивать их в арифметических выражениях. Чтобы достичь этого в нашем примере класса, мы будем перегружать существующий конструктор и метод + чтобы они принимали Double вместо Complex :

1
2
3
4
5
6
class Complex(val re: Double, val im: Double) {
  def this(re: Double) = this(re, 0)
  ...
  def +(d: Double) = new Complex(re + d, im)
  ...
}

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

1
2
3
4
5
scala> val y = new Complex(2)
y: Complex = 2.0+0.0*i
 
scala> y + 2
res3: Complex = 4.0+0.0*i

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

Неявные преобразования

Если вместо y + 2 выше мы выполним 2 + y мы получим ошибку, поскольку ни у одного из простых типов Scala нет метода + принятия Complex в качестве аргумента. Чтобы улучшить ситуацию, мы можем определить неявное преобразование из Double в Complex :

1
implicit def fromDouble(d: Double) = new Complex(d)

С этим преобразованием становится возможным добавление экземпляра Complex в double:

1
2
scala> 2 + y
res3: Complex = 4.0+0.0*i

Неявные преобразования — это мощный механизм, позволяющий несовместимым типам взаимодействовать друг с другом. Это почти делает другие подобные функции, такие как перегрузка метода, устаревшими. Фактически, с вышеупомянутым преобразованием нам больше не нужно перегружать метод + . Действительно, есть веские причины предпочитать неявные преобразования перегрузке методов, как объясняется в разделе «Почему перегрузка методов отстает в Scala» . В финальной версии нашего класса Complex мы добавим неявные преобразования и из других простых типов.

Модификаторы доступа

Как истинно объектно-ориентированный язык, Scala предлагает мощные функции контроля доступа, которые могут помочь вам обеспечить правильную инкапсуляцию. Среди них есть знакомые модификаторы private и protected доступа, которые вы можете использовать в полях и методах для ограничения их видимости. В нашем классе Complex мы могли бы использовать закрытое поле для хранения абсолютного значения или модуля комплексного числа:

1
2
3
4
class Complex(val re: Double, val im: Double) {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))
  ...
}

Попытка получить доступ к modulus извне, конечно, приведет к ошибке.

Унарные операторы

Чтобы позволить клиентам получить модуль экземпляра Complex , мы добавим новый метод, который его возвращает. Поскольку модуль является очень распространенной операцией, было бы неплохо иметь возможность снова вызывать его как оператор. Однако на этот раз это должен быть унарный оператор. К счастью, Scala помогает нам определить и этот тип операторов:

1
2
3
4
5
6
class Complex(val re: Double, val im: Double) {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))
  ...
  def unary_! = modulus
  ...
}

Методы, начинающиеся с unary_ могут быть вызваны как унарные операторы:

1
2
3
4
5
scala> val y = new Complex(3, 4)
y: Complex = 3.0+4.0*i
 
scala> !y
res2: Double = 5.0

В финальной версии нашего класса Complex мы добавим унарные операторы для знаков + и - и для комплексного сопряжения.

Сопутствующие объекты

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

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

1
2
3
4
5
6
object Complex {
  val i = new Complex(0, 1)
  def apply(re: Double, im: Double) = new Complex(re, im)
  def apply(re: Double) = new Complex(re)
  implicit def fromDouble(d: Double) = new Complex(d)
}

Наш сопутствующий объект имеет следующие члены:

  • i постоянная для мнимой единицы
  • Два apply метода — это фабричные методы, которые позволяют создавать экземпляры Complex , вызывая Complex(...) вместо менее удобного new Complex(...) .
  • Неявное преобразование из fromDouble является введенным выше.

Имея объект-компаньон, мы можем теперь написать такие выражения, как:

1
2
scala> 2 + i + Complex(1, 2)
res3: Complex = 3.0+3.0*i

Черты

Строго говоря, комплексные числа не сопоставимы друг с другом. Тем не менее, для практических целей было бы полезно ввести естественное упорядочение на основе их модуля. Конечно, мы хотели бы иметь возможность сравнивать комплексные числа с теми же операторами <, <=,> и> =, которые используются для сравнения других числовых типов.

Один из способов добиться этого — определить все эти 4 метода. Тем не менее, это приведет к некоторому шаблону, так как методы <=,> и> = будут, конечно, вызывать метод <. В Scala этого можно избежать, используя мощную функцию, известную как черты .

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

В нашем примере мы Ordered черту Ordered в наш класс Complex . Эта черта обеспечивает реализации всех 4 операторов сравнения, которые все вызывают абстрактный метод compare . Поэтому, чтобы получить все операции сравнения «бесплатно», все, что нам нужно сделать, — это предоставить конкретную реализацию этого метода.

1
2
3
4
5
6
class Complex(val re: Double, val im: Double)
  extends Ordered[Complex] {
  ...
  def compare(that: Complex) = !this compare !that
  ...
}

Теперь мы можем сравнивать комплексные числа по желанию:

1
2
3
4
5
scala> Complex(1, 2) > Complex(3, 4)
res4: Boolean = false
 
scala> Complex(1, 2) < Complex(3, 4)
res5: Boolean = true

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

Интересно, что сравнение Complex экземпляров на равенство не работает должным образом:

1
2
scala> Complex(1, 2) == Complex(1, 2)
res6: Boolean = false

Это связано с тем, что метод == вызывает метод equals , который по умолчанию реализует равенство ссылок. Один из способов исправить это — переопределить метод equals для нашего класса. Конечно, переопределение equals означает также переопределение hashCode . Хотя это было бы довольно тривиально, это добавило бы нежелательный образец.

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

  • адекватные реализации equals и hashCode
  • сопутствующий объект с методом apply factory
  • параметры класса неявно определены как val
1
2
3
case class Complex(re: Double, im: Double)
  ...
}

Теперь сравнение на равенство работает, как и ожидалось:

1
2
scala> i == Complex(0, 1)
res6: Boolean = true

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

1
2
3
4
5
6
7
8
9
override def toString() =
    this match {
      case Complex.i => "i"
      case Complex(re, 0) => re.toString
      case Complex(0, im) => im.toString + "*i"
      case _ => asString
    }
  private def asString =
    re + (if (im < 0) "-" + -im else "+" + im) + "*i"

Приведенный выше код сопоставляет this с несколькими шаблонами, представляющими константу i , действительное число, чисто мнимое число и все остальное. Хотя это может быть написано и без сопоставления с образцом, этот способ короче и проще для понимания. Сопоставление с образцом становится действительно неоценимым, если вам нужно обрабатывать сложные деревья объектов, поскольку оно предоставляет гораздо более элегантную и лаконичную альтернативу шаблону проектирования Visitor, обычно используемому в таких случаях.

Заворачивать

Окончательная версия нашего класса Complex выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import scala.math._
 
case class Complex(re: Double, im: Double) extends Ordered[Complex] {
  private val modulus = sqrt(pow(re, 2) + pow(im, 2))
 
  // Constructors
  def this(re: Double) = this(re, 0)
 
  // Unary operators
  def unary_+ = this
  def unary_- = new Complex(-re, -im)
  def unary_~ = new Complex(re, -im) // conjugate
  def unary_! = modulus
 
  // Comparison
  def compare(that: Complex) = !this compare !that
 
  // Arithmetic operations
  def +(c: Complex) = new Complex(re + c.re, im + c.im)
  def -(c: Complex) = this + -c
  def *(c: Complex) =
    new Complex(re * c.re - im * c.im, im * c.re + re * c.im)
  def /(c: Complex) = {
    require(c.re != 0 || c.im != 0)
    val d = pow(c.re, 2) + pow(c.im, 2)
    new Complex((re * c.re + im * c.im) / d, (im * c.re - re * c.im) / d)
  }
 
  // String representation
  override def toString() =
    this match {
      case Complex.i => "i"
      case Complex(re, 0) => re.toString
      case Complex(0, im) => im.toString + "*i"
      case _ => asString
    }
  private def asString =
    re + (if (im < 0) "-" + -im else "+" + im) + "*i" 
}
 
object Complex {
  // Constants
  val i = new Complex(0, 1)
 
  // Factory methods
  def apply(re: Double) = new Complex(re)
 
  // Implicit conversions
  implicit def fromDouble(d: Double) = new Complex(d)
  implicit def fromFloat(f: Float) = new Complex(f)
  implicit def fromLong(l: Long) = new Complex(l)
  implicit def fromInt(i: Int) = new Complex(i)
  implicit def fromShort(s: Short) = new Complex(s)
}
 
import Complex._

С этой удивительно короткой и элегантной реализацией мы можем сделать все, что описано выше, и еще немного:

  • создавать экземпляры с помощью Complex(...)
  • получить модуль с !x и сопряженный с ~x
  • выполнять арифметические операции с обычными операторами +, -, * и /
  • свободно смешивать сложные, действительные и целые числа в арифметических выражениях
  • сравнить на равенство с == и! =
  • сравнить на основе модулей с <, <=,> и> =
  • получить наиболее естественное представление строки

Если вы склонны к некоторым экспериментам, я бы посоветовал вам вставить приведенный выше код в интерпретатор Scala (используя :paste сначала :paste ) и поэкспериментировать с этими возможностями, чтобы почувствовать себя лучше.

Вывод

Многие считают Скала довольно сложным языком. Возможно, именно поэтому он так подходит для комплексных чисел … Если не считать блин, где некоторые люди видят сложность, я вижу непревзойденную элегантность и мощь. Я надеюсь, что этот пост хорошо это проиллюстрировал. Я сам все еще изучаю Scala и далеко не эксперт. Знаете ли вы о лучших способах реализации вышеуказанных возможностей? Я хотел бы услышать об этом.

Ссылка: Комплексные числа в Scala от нашего партнера JCG Стояна Рачева в блоге Стояна Рачева .