Руководство пользователя
В этом руководстве описываются архитектура, макет и использование Spire. Сначала мы рассмотрим некоторые базовые структуры и шаблоны, используемые Spire. Затем рассмотрим многие конкретные типы, которые определяет Spire. Наконец, заглянем в некоторые сложные и хитрые стороны библиотеки.
Классы типов
Как и многие библиотеки Scala, Spire использует классы типов для определения общих операций.
Все следующие примеры кода предполагают следующий импорт:
import spire.algebra.* // определение всех классов типов
import spire.implicits.* // экземпляры и синтаксис всех классов типов
Например, Ring[A]
- это класс типов, определяющий множество базовых операций, таких как +
и *
над типом A
.
При использовании классов типов важно стараться различать следующее:
- Сам класс типов (
Ring[A]
). Часто этоtrait
. - Конкретные экземпляры класса типов, такие как
Ring[Int]
. - Неявные синтаксисы, которые используют класс типов для определения операторов.
Методам в этих классах типов всегда присваиваются текстовые имена (например, plus
).
В некоторых случаях эти имена соответствуют символьным операторам: в случае plus
он соответствует +
.
При использовании этих классов типов пользователи имеют возможность использовать символический синтаксис
непосредственно для значений или вызывать метод в экземпляре класса типа:
def usingSymbols[A: Ring](x: A, y: A): A = x + y
def usingNames[A](x: A, y: A)(using r: Ring[A]): A = r.plus(x, y)
Некоторые методы (например, sqrt
) не имеют соответствующих символов.
В этих случаях само имя метода может использоваться со значениями:
def sqrt[A: NRoot](x: A): A = x.sqrt
Уровень пакета
В случае Ring[A]
, сам класс типов находится в spire.algebra
.
За исключением нескольких особых случаев, все классы типов Spire можно найти в пакете spire.algebra
.
Экземпляры классов типов можно найти в двух разных местах.
Для типов, определенных в Spire, или кода, поддерживающего Spire,
экземпляры класса типов должны быть помещены в сопутствующий объект типа.
Например, UByte
(тип беззнакового байта) имеет экземпляр Rig[UByte]
, содержащийся в сопутствующем объекте.
Для типов, определенных где-либо еще, которые Spire поддерживает напрямую (например, встроенные числовые типы),
Spire определяет объекты spire.std
, в которых содержатся их экземпляры.
Итак, чтобы получить все экземпляры Int
, вам придется импортировать их из spire.std.int.*
.
Чтобы получить все эти "стандартные экземпляры" за один раз, импортируйте файлы spire.std.any.*
.
Этот шаблон также следует использовать при поддержке других числовых типов, не поддержанных Spire.
Наконец, неявные синтаксисы импортируются из объектов в spire.syntax
.
Чтобы получить синтаксис Ring[A]
, вы должны импортировать файлы spire.syntax.ring.*
.
Опять же, есть пакет ярлыков: вы можете импортировать spire.syntax.all.*
, чтобы получить весь синтаксис.
Такое импортирование может показаться немного запутанным, но оно очень полезно в ситуации,
когда типы или операторы Spire конфликтуют с другими библиотеками.
Существует еще более простой импорт (spire.implicits.*
), если нужны все экземпляры и все операторы.
Это удобно при работе в консоли или экспериментировании,
а также в тех случаях, когда вы уверены, что конфликта не возникнет.
Применение
В большинстве случаев классы типов используются в качестве границ контекста. Например:
object Demo:
import spire.algebra.*
import spire.std.any.*
import spire.syntax.ring.*
def double[A: Ring](x: A): A = x + x
def triple[A: Ring](x: A): A = x * 3
println((double(3), triple(4)))
Этот код в конечном итоге эквивалентен:
object Demo2:
def double[A](x: A)(using ev: Ring[A]): A = ev.plus(x, x)
def triple[A](x: A)(using ev: Ring[A]): A = ev.times(x, ev.fromInt(3))
println((double(3)(IntAlgebra), triple(4)(IntAlgebra)))
Тип IntAlgebra
расширяет Ring[Int]
и был импортирован через spire.std.any.*
.
Все неявные функции, предоставляющие бинарные операторы +
и *
(а также неявные функции преобразования целочисленного литерала в A
),
были импортированы в форме spire.syntax.ring.*
.
А привязка контекста Ring
на самом деле является просто "сахаром" для неявного параметра (экземпляра класса типов).
Специализация
Чтобы достичь скорости, сравнимой с прямым (неуниверсальным) кодом, вам потребуется использовать специализацию. Хорошей новостью является то, что большая часть кода Spire уже специализирована (и проверена на производительность). Плохая новость заключается в том, что вам придется аннотировать весь общий код следующим образом:
object Demo3:
import spire.algebra.*
import spire.std.any.*
import spire.syntax.ring.*
import scala.specialized as sp
def double[@sp A: Ring](x: A): A = x + x
def triple[@sp A: Ring](x: A): A = x * 3
println((double(3), triple(4)))
Слишком много ошибок со специализацией, чтобы перечислять их здесь. Но (очень) краткое руководство по специализации таково:
- Гораздо проще специализировать методы.
- Вызовы из общего кода в специализированный код не являются специализированными.
- Ограничьте специализацию типами, которые вы будете использовать через
@sp(Int, Double)
. - Специализация увеличит размер байт-кода в 2–10 раз.
Если у вас есть вопросы по специализации, смело задавайте их на #spire
канале Typelevel Discord.
Вы можете заметить, что некоторый код в Spire структурирован необычным образом,
и часто это делается для того, чтобы убедиться, что специализация работает правильно.
Вы можете обнаружить, что разработать общий код легко, не используя сначала специализацию (чтобы упростить задачу), а затем, при необходимости, возвращаясь к ней и добавляя аннотации позже. Это помогает упростить задачу, пока ваш код работает правильно, и это (относительно) незначительное изменение, позволяющее включить специализацию позже (при условии, что вы будете последовательны).
Конечно, если ваш код не является универсальным, вы можете вызвать специализированный код Spire, не беспокоясь об этом (и результат будет распакован и быстро).
Характеристики
Классы типов Spire часто описываются с точки зрения свойств (или "законов"). Эти свойства должны быть истинными независимо от того, какие значения используются.
Вот краткое описание некоторых наиболее распространенных свойств:
- ассоциативность:
|+|
является ассоциативным, если(a |+| b) |+| c = a |+| (b |+| c)
. - тождественность: для
|+|
существует такое значение идентификатораid
, чтоa |+| id = a = id |+| a
. - обратимость:
|+|
содержит операциюinverse
такую, чтоa |+| a.inverse = id = a.inverse |+| a
. - коммутативность:
|+|
коммутативна, еслиa |+| b = b |+| a
.
В некоторых случаях имена операторов различаются (например +
, *
), но сами свойства остаются прежними.
Типы
В этом разделе предпринята попытка описать существующие типы чисел с точки зрения их возможностей и проблем.
Byte, Short, Int и Long
Все эти встроенные целочисленные типы имеют знак и фиксированную ширину (8, 16, 32 и 64 бита соответственно). Деление с этими типами усекается, и переполнение может произойти незаметно, когда числа становятся слишком большими (или слишком маленькими). Деление на ноль вызовет исключение.
Стоит отметить, что JVM не поддерживает работу напрямую Byte
и Short
: операции над ними обычно возвращают Int
.
Это может вызвать путаницу при использовании вывода типа,
а также может привести к различиям между прямым кодом (где добавление байтов создает целое число)
и универсальным кодом (где добавление байтов создает байт).
Float и Double
Эти дробные типы соответствуют плавающей запятой IEEE-754 (32- и 64-битные соответственно).
Они содержат три сигнальных значения: положительную и отрицательную бесконечность и NaN
.
Большие положительные и отрицательные величины переполнятся до соответствующего значения бесконечности,
а деление на ноль незаметно уйдет в бесконечность.
Семантика сравнения и равенства для NaN
сложна (например, NaN == NaN
является ложным).
Это также означает, что для двойных значений не существует общего порядка, соответствующего сравнениям IEEE.
О полной альтернативе Ordering[Double]
см spire.optional.totalfloat
.
Поскольку значения с плавающей запятой являются аппроксимацией реальных значений,
при сложении значений разных величин может произойти потеря точности.
Таким образом, многие операции не всегда ассоциативны.
Spire предполагает, что пользователи, которые работают с Float
и Double
знают об этих проблемах,
и предоставляет экземпляры, например, Ring[Double]
, хотя в некоторых случаях операции не могут быть ассоциативным.
BigInt
Этот целочисленный тип не ограничен — он никогда не переполняется (хотя операции будут становиться все медленнее и медленнее по мере увеличения значения). Вероятно, это один из наименее сложных типов для правильного использования.
BigDecimal
Этот дробный тип отличается от предыдущих значений с плавающей запятой.
Он содержит объект MathContext
, который определяет конкретное количество десятичных цифр точности (по умолчанию 34).
Результаты будут округлены до этого уровня точности, что также делает этот тип неассоциативным в некоторых случаях
(хотя с заданной пользователем точностью легче избежать случаев, когда это имеет значение).
MathContext
также определяет, как следует округлять значения.
Поскольку этот тип является десятичным, он может точно представлять любое десятичное число
(в отличие от значения с плавающей запятой),
хотя для этого в его MathContext
потребуется достаточное количество цифр.
Как и в случае с плавающей запятой, Spire прилагает все усилия для поддержки этого типа, даже несмотря на то, что могут возникнуть проблемы, связанные с точностью и округлением. Spire также предоставляет возможности, которых нет в базовом типе, включая взятие корня, дробные степени и тригонометрические методы.
Rational
Этот дробный тип представляет рациональное число, дробь двух целых чисел (n/d
).
Это точный тип, хотя, как и следовало ожидать, он не может представлять иррациональные числа,
не аппроксимируя их как рациональные. Он не ограничен, хотя по мере того,
как дробь становится больше или сложнее, операции становятся медленнее.
Рациональные расчеты всегда сохраняются в простейшей форме, чтобы ускорить будущие вычисления.
Вероятно, это самый простой и правильный в использовании дробный тип.
SafeLong
Этот целочисленный тип также неограничен, как и BigInt
.
Однако он более эффективен для небольших значений, где вместо него будет использоваться Long
.
Обычно нет причин предпочитать использование BigInt
вместо SafeLong
,
за исключением случаев, когда ожидается, что большинство значений превысят емкость хранилища Long
.
Natural
Это простой неограниченный беззнаковый целочисленный тип.
Он моделирует натуральные числа как список цифр (каждая "цифра" представляет собой 32-битное целое число без знака).
Для относительно небольших значений (32–128 бит) он часто быстрее, чем SafeLong
или BigInt
.
Для больших значений он становится медленнее.
В настоящее время тип Natural
немного непривычный.
Однако тот факт, что он гарантированно неотрицательный, полезен.
UByte, UShort, UInt и ULong
Эти беззнаковые целочисленные типы предоставляются Spire. Они выполняют большинство тех же операций, что и их подписанные аналоги, хотя используют беззнаковое деление, что немного сложнее.
Это классы значений, поэтому в большинстве случаев не должно быть дополнительных накладных расходов
по сравнению с их примитивными аналогами. Единственное исключение — массивы.
Array[UInt]
будет упакован, тогда как Array[Int]
- нет.
Поскольку преобразования между UInt
и Int
завершаются только во время компиляции,
эту проблему легко обойти, сохранив UInt
экземпляры в Array[Int]
.
Написание литеральных беззнаковых значений немного более громоздко,
чем их знаковых аналогов (рассмотрим UInt(7)
в сравнении с 7
).
Spire предоставляет импорт синтаксиса, который упрощает написание:
import spire.syntax.literals.*
ui"7" // равнозначно UInt(7)
// res0: spire.math.UInt = 7
FixedPoint
Этот класс значений использует Long
с неявным знаменателем. Сам тип не содержит информации о знаменателе.
Вместо этого требуется неявный экземпляр FixedScale
для предоставления этого контекста,
когда необходимо (например, во время умножения).
Как и предыдущие значения без знака, значения с фиксированной точкой в большинстве случаев не будут упакованы.
Этот тип предназначен для решения определенного рода задач и его следует использовать только в ситуациях, когда необходимо большое количество рациональных чисел с одинаковым знаменателем и очень важна эффективность.
Complex[A] и Quaternion[A]
Эти универсальные типы представляют собой комплексные числа (x + yi
) и кватернионы (w + xi + xj + zk
) соответственно.
Их можно параметризовать любым дробным типом A
, реализующим Field[A]
, NRoot[A]
и Trig[A]
.
В целом эти значения столь же точны, как и их основные значения A
,
хотя в некоторых случаях обязательно возвращаются приблизительные результаты
(в случаях, когда используются корни или тригонометрические функции).
Эти типы являются специализированными, поэтому большинство операций должны выполняться достаточно быстро
и не вызывать ненужной упаковки. Однако эти типы используют больше памяти,
чем необобщенное комплексное число, основанное на Double
значениях, и работающее немного медленнее.
Number
Это упакованный тип числа, который аппроксимирует семантику чисел в динамически типизированной числовой башне
(например, Scheme или Python).
Существует четыре подтипа Number
, основанных на SafeLong
, Double
, BigDecimal
и Rational
.
Объединение двух чисел всегда будет возвращать число самой высокой точности.
Number
— хороший выбор для пользователей, которым нужны простые и правильные цифры.
Этот тип обеспечивает максимальную безопасность операций, обеспечивая при этом доступ ко всем операторам и методам.
Interval[A]
Интервал поддерживает арифметические операции в диапазоне возможных значений A
.
Это можно рассматривать как представление неопределенности относительно одного фактического значения
или как действие одновременно со всем набором значений.
В интервале можно использовать любой тип, имеющий Order[A]
,
хотя для большинства арифметических операций потребуются дополнительные классы типов
(от AdditiveSemigroup[A]
для +
до Field[A]
для /
).
Интервалы могут быть неограниченными с обеих сторон, а границы могут быть открытыми или закрытыми. (Интервал включает в себя закрытые границы, но не открытые границы). Вот несколько строковых представлений различных интервалов:
[3, 6]
- набор значений от 3 до 6 (включая оба).(2, 4)
- набор значений от 2 до 4 (исключая оба).[1, 2)
- полуоткрытый интервал, включающий 1, но не 2.(-∞, 5)
- набор значений меньше 5.
Интервалы моделируют непрерывные пространства, даже если тип A
дискретен.
Так, например, когда (3, 4)
есть Interval[Int]
, он не считается "пустым",
даже если между 3
и 4
нет значений Int
.
Это потому, что мы можем умножить интервал на 2
, чтобы получить (6, 8)
, который явно не пуст.
Базовый непрерывный интервал содержит значения,
которые при умножении на скаляр становятся действительными значениями Int
.
Polynomial[C]
В настоящее время Spire поддерживает одномерные полиномы. Это полиномы с одной переменной (например, \(x\)) следующей структуры: \(c_{0} + (c_{1} * x^{1}) + (c_{2} * x^{2}) + ... + (c_{n} * x^{n})\)
Коэффициенты (от \(c_{0}\) до \(c_{n}\)) являются значениями типа C
,
а показатели степени (от 1 до n) являются Int
значениями
(это означает, что реализация Spire поддерживает только полиномы, показатели степени которых меньше 2147483648
).
Как и в случае с интервалами, арифметика над полиномами выполняется с использованием классов типов для C
,
таких как Semiring[C]
. При наличии правильных классов типов полиномы могут поддерживать все арифметические операции,
охватываемые евклидовыми кольцами, но не поля.
Операции деления и обратные операции невозможны,
поскольку многочлены не поддерживают дробные или отрицательные показатели.
Полиномы также поддерживают interval
, derivative
и другие операции.
Spire поддерживает удобный синтаксис для литеральных полиномов.
Импортируя spire.syntax.literals.*
(или просто spire.implicits.*
),
вы можете использовать строковый интерполятор poly
для создания экземпляров Polynomial[Rational]
:
import spire.syntax.literals.*
poly"3x^2 - 5x + 1"
poly"5/4x^6 - 7x - 2"
poly"1.2x^3 - 6.1x^2 + 9x - 3.33"
Spire фактически поддерживает два типа полиномов: плотные и разреженные. Для большинства простых полиномов, используемых в этих примерах, вам, вероятно, понадобятся плотные полиномы. Однако в случаях, когда ваши полиномы имеют несколько членов с очень большими показателями степени, разреженная реализация будет более эффективной. В любом случае базовое представление является деталью реализации, и оба типа поддерживают одни и те же операции (и могут взаимодействовать).
Algebraic
Тип Algebraic
представляет собой реализацию числа для "Точных геометрических вычислений".
Он представляет алгебраические числа, используя AST операции, выполненные над ним.
Числа Algebraic
можно сравнивать чисто и точно.
Это означает, что если у нас есть два числа a
и b
, то a compare b
всегда корректно,
независимо от того, иррациональны они или невероятно близки друг к другу.
Они подходят для использования в алгоритмах, которые используют квадратные или n-корни
и для правильной работы полагаются на проверку знаков и числовое сравнение.
Помимо точных сравнений/проверок знаков, Algebraic
может аппроксимировать себя с любой желаемой точностью постфактум.
Это работает как для абсолютных приближений, таких как x +/- 0.00001
,
так и для относительных приближений, таких как x.toBigDecimal(new MathContext(10000))
.
Поскольку Algebraic
может представлять алгебраические числа
(примечание: Spire добавляет поддержку полиномиальных корней, а не только n-корней),
они имеют более широкий диапазон, чем Rational
.
Однако, хотя Rational
точно представляет числа, Algebraic
может только точно сравнивать их.
Для достижения этой цели Algebraic
также жертвует производительностью,
поэтому не подходят для использования там, где вам нужна производительность,
и допускают определенное количество ошибок.
Real
Real
означает "вычислимое действительное".
Реализация Spire Real
основана на ERA, написанном на Haskell Дэвидом Лестером (David Lester).
Вычислимые действительные числа — это числа, которые можно вычислить (т.е. аппроксимировать) с любой желаемой точностью.
В отличие от Double
и BigDecimal
, Real
значения сохраняются не как приближения,
а как функция от желаемой точности до ближайшего приближенного значения.
Если у нас есть Real
экземпляр x, который приближается к действительному числу r,
это означает, что для любой точности p (в битах) наш экземпляр выдаст x такой,
что \(\frac{x}{2^{p}}\) является ближайшим рациональным значением к r.
В переводе на Scala это означает, что x.apply(p)
возвращает SafeLong
значение x
,
являющееся Rational(x, SafeLong(2).pow(p))
- лучшим приближением для r.
Spire представляет два типа Real
значений: Exact
и Inexact
.
Первые представляют собой рациональные значения, для которых есть существующий экземпляр Rational
,
и работать с ними "недорого".
Последние представляют собой функции для аппроксимации (потенциально) иррациональных значений,
лениво оцениваются и запоминаются, и их вычисление потенциально может быть очень дорогим.
Как и в случае с Rational
значениями, операции над Real
значениями
могут подчиняться соответствующим алгебраическим тождествам.
Но в отличие от Rational
, Real
поддерживает корни и тригонометрические функции.
Кроме того, сохраняются важные тригонометрические тождества:
import spire.math.Real.{cos, sin}
import spire.math.{Real, sqrt}
// вернет Real(1) вне зависимости от переданного значения
def circle(a: Real): Real = sqrt(cos(a).pow(2) + sin(a).pow(2))
Имейте в виду, что вычисление корней не является абсолютно точным. Дополнительные сведения см. в разделе "Классы иррациональных и трансцендентальных типов".
Одним из интересных последствий построения вычислимых действительных чисел является то,
что прерывистые операции (такие как проверка знаков, сравнение и равенство) не могут быть выполнены точно.
Если x.apply(p)
возвращает 0
, невозможно узнать, равно ли значение на самом деле нулю
или это просто очень маленькое значение (положительное или отрицательное!), которое при этой точности примерно равно нулю.
Точно так же невозможно сказать, что x
равно y
, а только то, что они эквивалентны (или нет) с заданной точностью.
В настоящее время Spire вычисляет с точностью "по умолчанию", чтобы использовать его с такими методами.
Более того, эти методы всегда будут работать с Exact
значениями:
проблемы возникают только при использовании Inexact
значений.
Учитывая, что альтернативой использованию Real
является использование другого приближенного типа,
обеспечение приближенных сравнений и равенства кажется разумным компромиссом.
Какие типы чисел следует использовать?
Spire предоставляет множество типов чисел, и не всегда очевидно, каковы их относительные преимущества. В этом разделе объясняются различия между ними, что может помочь решить, какое числовое представление(-я) использовать.
Обычно существует противоречие между числами, которые имеют ограничения по корректности (например, возможные проблемы с переполнением или точностью), и числами, которые имеют ограничения по производительности (например, дополнительные выделения и/или более медленный код). Spire предоставляет широкий спектр числовых типов, отвечающих большинству потребностей.
Натуральные числа (беззнаковые, целые числа)
Для неотрицательных чисел безопасным типом является Natural
.
Он довольно быстр при представлении небольших чисел (128 бит и меньше),
но не имеет верхней границы значений, которые может представлять.
Однако его уникальная структура означает, что для очень больших значений BigInt
и SafeLong
могут работать быстрее.
Поскольку Natural
поддерживает только неотрицательные значения, вычитание не является полным (и может вызвать исключение).
Если ваши значения гарантированно будут небольшими (или вы готовы определять усечение),
то можете использовать UByte
(8-битный), UShort
(16-битный), UInt
(32-битный) или ULong
(64-битный),
в зависимости от того, какая размерность вам нужна.
Эти типы имеют ту же беззнаковую семантику, что и беззнаковые типы в таких языках, как C.
Эти типы не упакованы, хотя с массивами следует проявлять осторожность (как и с любым классом значений).
Целые числа (знаковые, целые числа)
Существует два безопасных типа, которые можно использовать с целочисленными значениями: SafeLong
и BigInt
.
Оба поддерживают произвольно большие значения, а также обычную семантику для таких вещей, как целочисленное деление (quot
).
Первый вариант (SafeLong
) работает намного лучше для значений, которые могут быть представлены с помощью Long
(например, 64-битного или менее), и является хорошим выбором по умолчанию.
При работе со значениями, которые по большей части или полностью очень велики, BigInt
может быть немного быстрее.
Как и в беззнаковом случае, вы можете использовать Byte
(8-битный), Short
(16-битный), Int
(32-битный)
или Long
(64-битный) для обработки случаев, когда ваши значения малы
или когда вы хотите избежать выделения памяти и решаете проблемы с усечением самостоятельно.
Эти типы предоставляются Scala (и, в конечном итоге, JVM) и не вызывают выделения объектов.
Дробные числа (числа, которые можно разделить)
Существует множество различных вариантов дробных чисел, которые поддерживают различные компромиссы между выразительностью, точностью и производительностью.
Дробные типы бывают двух основных видов: точные и неточные.
Неточные типы (например, Double
) будут накапливать ошибки и в некоторых случаях не являются ассоциативными
(это означает, что (x + y) + z
может дать результат, отличный от x + (y + z)
).
Эти типы часто работают быстрее, чем точные типы, но их использование может быть рискованным.
Точные числа дают более надежную гарантию точности, но за счет производительности или выразительности. Зачастую они немного медленнее и могут ограничивать поддерживаемые операции (чтобы сохранить гарантии точности).
Точные типы
Самый мощный точный тип — Real
. Он представляет вычислимые действительные числа
и поддерживает все ожидаемые операции, включая корни и тригонометрию.
Однако иррациональные значения (например, Real(2).sqrt
или Real.pi
)
представляются с помощью функций от точности до приближений.
Это означает, что в некоторых ситуациях этот тип может работать слишком медленно
или использовать слишком много памяти.
Кроме того, операции, требующие проверки сравнения или равенства,
могут быть вычислены только приблизительно.
Однако этот тип никогда не должен накапливать ошибки,
поэтому ваши результаты всегда будут правильно аппроксимированы с необходимой вам точностью.
Следующий точный тип — Algebraic
. Этот тип поддерживает все рациональные значения, а также корни.
Однако он не может представлять трансцендентальные значения, такие как "число Пи",
что делает его значения подмножеством Real
-а.
В отличие от Real
, этот тип способен выполнять точные проверки знаков
(и, следовательно, проверки на равенство и сравнения).
Поскольку AST Algebraic
использует для представления выражений,
выполнение может быть медленным и включать большое количество вычислений.
Наконец есть Rational
.
Этот тип представляет значения в виде несократимых дробей (например, n/d
).
Rational
не может представлять иррациональные значения (например, корни),
но эффективно реализует все операции с рациональными значениями.
Этот тип имеет наименьшее количество ошибок в производительности, хотя очевидно,
что обработка дробей с большими числителями или знаменателями займет больше времени.
Неточные типы
Эти типы более эффективны, чем точные типы, но требуют внимательности и анализа, чтобы гарантировать правильность и достаточную точность результатов.
Неточный тип с наибольшей потенциальной точностью - BigDecimal
- предоставляется Scala.
Это число аппроксимирует реальные значения до десятичных цифр (по основанию 10) (по умолчанию 34).
В отличие от значений с плавающей запятой, этот тип имеет точное представление таких значений, как 0.111110
,
и пользователь может использовать java.math.MathContext
для настройки используемой точности.
Несмотря на это, тип по-прежнему подвержен накопленной ошибке округления
и, следовательно, не является по-настоящему ассоциативным.
Далее идут встроенные Float
и Double
, 32- и 64-битные реализации операций с плавающей запятой в JVM.
Подводные камни использования значений с плавающей запятой хорошо известны (и документированы в других источниках),
но эти типы работают очень быстро.
Наконец, Spire поддерживает экспериментальный класс FixedPoint
.
Этот класс значений использует неупакованные Long
значения для представления дробей
в терминах знаменателя, указанного пользователем (предоставляется через неявный экземпляр FixedScale
).
Это очень специализированный тип, который можно использовать только в тех случаях,
когда аппроксимации с плавающей запятой вызывают проблемы и требуются распакованные значения.
Следует избегать этого типа, если только вашим приложениям не известна
конкретная потребность в арифметике с фиксированной запятой.
Другие типы
Остальные числовые и псевдочисловые типы (например Polynomial
, Interval
, Complex
и Quaternion
)
реализуют определенные функции, поэтому должно быть меньше путаницы в отношении того, какой тип использовать.
В разделах, описывающих эти типы, объясняются их свойства и компромиссы.
Дизайн
Этот раздел предназначен для описания целей проектирования, соглашений и замечаний по реализации для различных типов.
SafeLong
Целью разработки SafeLong
является предоставление надежных целых чисел произвольной точности,
которые, тем не менее, имеют производительность, приближающуюся к упакованным Long
,
для вычислений, которые остаются в диапазоне 64-битного целого числа со знаком.
Это достигается за счет наличия двух вариантов: SafeLongLong(x: Long)
используется для чисел в диапазоне Long
.
SafeLongBigInt(x: BigInt)
используется для чисел выходящих за пределы Long
.
Соглашения
Число в Long
диапазоне всегда должно быть представлено как SafeLongLong
.
Это используется как в операциях SafeLong
, так и в других классах spire.math
.
Следствия этого соглашения:
SafeLongBigInt
никогда не может равняться0
- А
SafeLongBigInt
никогда не может быть равноSafeLongLong
Замечания по реализации
Операции SafeLongBigInt
используют базовый BigInt
и создают соответствующий тип результата
в зависимости от того, является ли результат допустимым Long
.
Операции на SafeLongLong
используют макрос Checked для выполнения операций с использованием целых чисел в диапазоне Long
и возвращаются к использованию только BigInt
в случае числового переполнения.
Советы по производительности
- сравнение с нулем более эффективно при использовании
Signum
, чем при использованииcompare
- метод
isValidLong
можно использовать для проверки того, можно лиSafeLong
без потерь преобразовать вlong
, используяtoLong
Rational
Цель разработки Rational
— обеспечить хорошую производительность и низкие затраты памяти
в очень частых случаях рациональных чисел небольшой величины,
сохраняя при этом возможность представлять рациональные числа произвольного размера.
Для достижения этой цели есть два разных случая.
LongRational(n: Long, d: Long)
используется для представления малых рациональных чисел.
BigRational(n: BigInt, d: BigInt)
используется всякий раз, когда числитель или знаменатель слишком велики
для хранения в формате LongRational
.
Соглашения
Рациональные числа всегда хранятся в нормализованной форме, поэтому для каждого числа существует уникальное представление:
- Общие множители числителя и знаменателя сокращаются, поэтому, например,
3/9
сохраняется какn = 1, d = 3
. - Отрицательные рациональные числа всегда сохраняются с отрицательным числителем. Поэтому знаменатель всегда положителен.
- Рациональное число с нулевым знаменателем недопустимо.
- Ноль всегда будет храниться как
n = 0, d = 1
.
Помимо этих соглашений, которые довольно часто встречаются во многих реализациях рациональных чисел,
существуют дополнительные соглашения, связанные с использованием LongRational
или BigRational
:
-
Каждое рациональное число, которое может быть сохранено в
LongRational
с учетом приведенных выше соглашений, всегда будет храниться вLongRational
. Последствия этого:- Допустимый диапазон для числителя —
[Long.MinValue, Long.MaxValue]
и для знаменателя -[1, Long.MaxValue]
. - Поскольку знаменатель всегда сохраняется как положительное число, например,
Rational(1, Long.MinValue)
должен храниться какBigRational
.
- Допустимый диапазон для числителя —
-
BigRational
никогда не может быть равенLongRational
.- Числитель
BigRational
никогда не может быть нулевым, поскольку ноль всегда представляется какLongRational(0, 1)
- При реализации операций для
Rational
важно убедиться, что соглашения не нарушаются. Например, при выполнении операции с двумяBigRational
, в результате которой получается небольшое число, которое можно представить в видеLongRational
, результат должен бытьLongRational
.
- Числитель
Замечания по реализации
Реализация BigRational
довольно проста. Обычно операции выполняются с использованием SafeLong
,
а результат создается с помощью метода построения, который принимает SafeLong
в качестве числителя и знаменателя
и выдает правильный тип рационального числа с учетом соглашений.
Операции LongRational
немного сложнее: базовые операции используют макрос Checked,
чтобы попытаться выполнить операцию только с целыми числами long
,
и возвращаются к использованию только SafeLong
в случае числового переполнения.
Преимущество этого подхода заключается в том, что в том самом распространенном случае,
когда нет переполнения, не происходит ненужного выделения объектов.
Советы по производительности
Характеристики производительности операций над рациональными числами немного отличаются от обычных целочисленных операций или операций с плавающей запятой.
- базовые операции
+
,-
,*
,/
имеют примерно одинаковую стоимость - сравнение дешевле, чем основные операции, и не выделяет объекты для небольших рациональных чисел.
- сравнение с нулем более эффективно при использовании
Signum
, чем при использованииcompare
- сравнение с единицей лучше всего выполнять с помощью метода
CompareToOne
вместо использованияcompare
Ссылки: