Руководство пользователя

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

Классы типов

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

Все следующие примеры кода предполагают следующий импорт:

import spire.algebra.*   // определение всех классов типов
import spire.implicits.* // экземпляры и синтаксис всех классов типов

Например, Ring[A] - это класс типов, определяющий множество базовых операций, таких как + и * над типом A. При использовании классов типов важно стараться различать следующее:

Методам в этих классах типов всегда присваиваются текстовые имена (например, 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)))

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

Если у вас есть вопросы по специализации, смело задавайте их на #spire канале Typelevel Discord. Вы можете заметить, что некоторый код в Spire структурирован необычным образом, и часто это делается для того, чтобы убедиться, что специализация работает правильно.

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

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

Характеристики

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

Вот краткое описание некоторых наиболее распространенных свойств:

В некоторых случаях имена операторов различаются (например +, *), но сами свойства остаются прежними.

Типы

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

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] для /).

Интервалы могут быть неограниченными с обеих сторон, а границы могут быть открытыми или закрытыми. (Интервал включает в себя закрытые границы, но не открытые границы). Вот несколько строковых представлений различных интервалов:

Интервалы моделируют непрерывные пространства, даже если тип 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 используют базовый BigInt и создают соответствующий тип результата в зависимости от того, является ли результат допустимым Long.

Операции на SafeLongLong используют макрос Checked для выполнения операций с использованием целых чисел в диапазоне Long и возвращаются к использованию только BigInt в случае числового переполнения.

Советы по производительности

Rational

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

Для достижения этой цели есть два разных случая. LongRational(n: Long, d: Long) используется для представления малых рациональных чисел. BigRational(n: BigInt, d: BigInt) используется всякий раз, когда числитель или знаменатель слишком велики для хранения в формате LongRational.

Соглашения

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

Помимо этих соглашений, которые довольно часто встречаются во многих реализациях рациональных чисел, существуют дополнительные соглашения, связанные с использованием LongRational или BigRational:

Замечания по реализации

Реализация BigRational довольно проста. Обычно операции выполняются с использованием SafeLong, а результат создается с помощью метода построения, который принимает SafeLong в качестве числителя и знаменателя и выдает правильный тип рационального числа с учетом соглашений.

Операции LongRational немного сложнее: базовые операции используют макрос Checked, чтобы попытаться выполнить операцию только с целыми числами long, и возвращаются к использованию только SafeLong в случае числового переполнения. Преимущество этого подхода заключается в том, что в том самом распространенном случае, когда нет переполнения, не происходит ненужного выделения объектов.

Советы по производительности

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


Ссылки: