Эта часть продолжает неформальный рассказ о p-адических числах и она посвящена практическим аспектам работы с этой числовой системой и, в частности, некоторым деталям реализации p-адической алгебры на языке Haskell. О том, что это за система и зачем она может понадобиться, читайте в предыдущей части. Мы поговорим об эффективном внутреннем представлении p-адических чисел, о базовых алгоритмах и методах работы с ними, а также о двух классных инструментах в системе типов языка Haskell: о типах-литералах (type literals) и семействах типов (type families). Чаще всего, мы имеем дело с p-адическими числами в их каноническом разложении, то есть, в виде последовательности цифр, потенциально, бесконечной. Естественным образом возникает искушение и реализовать их программно, как последовательности (массивы, векторы или списки) цифр, а всю арифметику выполнять поразрядно. Это может быть поучительно, но не эффективно.  Мы уже достаточно подкованы теоретически, чтобы понять, что p-адические числа — это, всё-таки, не цепочка цифр, а числовая система. Эффективнее всего компьютер работает с целыми числами, и в роли системы, представляющей кольцо целых p-адических чисел с ограниченной точностью в  разрядов, может выступать кольцо , содержащее обычные целые числа с обычной модулярной арифметикой. Например, вместо "честных" 2-адических чисел можно использовать 64-разрядное число типа Word64, с которым можно обращаться, как с представителем кольца . Также 7-адические числа можно представить как целые числа, вычисляемые по модулю , которые тоже все помещаются в Word64. По существу, понижая  до , мы ограничиваемся лишь первой цифрой канонического разложения в кольце , в котором очень много элементов и они могут быть очень большими. Существование изоморфизмов между числовыми системами, упомянутых в первой части статьи, приводит к тому, что сложение, вычитание, умножение и даже деление для этой первой цифры будет в полной мере соответствовать p-адическим операциям. А поскольку это просто большое целое число, то арифметика с ним может быть реализована очень эффективно, например, с использованием GMP или подобных ей библиотек. Таким образом, на практике стоит рассматривать p-адические числа (и рациональные и трансцендентные) как большие целые, вся арифметика с которыми выполняется по модулю . Ограничение разрядности требует осознанной работы с точностью чисел. И нам нужно научиться переходить от вещественных чисел, выраженных с конечной точностью, в  и возвращаться обратно так, чтобы по-возможности, не потерять необходимой информации при этом переходе.  В нашем контексте точность вещественных чисел определяется не числом значащих цифр десятичного разложения, а максимальными абсолютными значениями для числителя и знаменателя дроби, приближающей вещественные числа. Например, для каких-то вычислений мы можем ограничиться дробями, в которых числитель и знаменатель не превышают , а для более точных расчетов повысить верхнюю границу используемых числителей и знаменателей, скажем, до максимального значения допускаемого типом Word64 — огромного числа . Приведу два базовых алгоритма для перевода чисел из  в  и наоборот. Для перевода числа  из множества  в  достаточно найти обратный элемент для  в , то есть элемент , такой, что . Это сравнение эффективно решается расширенным алгоритмом Евклида или через цепные дроби, еcли .  Вот как выглядит алгоритм для нахождения округлённого p-адического образа числа , как элемента : Выделяем из числителя или знаменателя множитель , то есть, представляем входное число как произведение       Найдём образ числа  в  так, чтобы можно было без потери точности восстанавливать числа с числителем и знаменателем, имеющими тип Word16. Найдём необходимую для обеспечения точности степень основания:    Выделим из числа степени основания: , значит, число будет иметь порядок . Найдём обратный элемент для знаменателя: .
Получаем обратимый элемент p-адического образа:     Найдём образ числа  в , с той же степенью округления. Найдём необходимую для обеспечения точности степень основания:    Выделим из числа степени основания: . Основание не простое и имеет со знаменателем общий делитель . Значит, дробь нужно преобразовать, избавившись от него.        Как мы уже говорили, из-за того, что множество p-адических чисел существенно больше множества рациональных, корректная реконструкция рационального числа возможна не всегда. При этом особенности p-адической топологии приводят к тому, что постепенное увеличение точности для нерационального p-адического числа не порождает последовательности приближений, сходящейся к какому-то рациональному числу, а работает скорее, как генератор псевдослучайных рациональных чисел. Однако, как в 1981 году показал Пол Ванг, если рациональная реконструкция существует, то она может быть однозначно получена алгоритмом, основанным на расширенном алгоритме Евклида, более известном, как метод Лагранжа для выделения базиса целочисленной решётки.  Вот как выглядит алгоритм восстановления рационального числа  из p-адического числа, округлённого до  разрядов, представленного числом . Необходимое (но не достаточное) условие корректной работы алгоритма: . Инициализируем два вектора . Пока  выполняем итерации     Восстановим рациональное число  по его округлённому представлению  с точностью  (максимальное значение для числителя и и знаменателя) Применение алгоритма породит цепочку итераций для векторов  и :   Один из основных инструментов при работе в p-адическом мире был описан самим основателем этой теории Куртом Гензелем. Процесс построения проективного предела из начального кольца вычетов, о котором шла речь в первой части статьи, можно применять ко всей числовой системе сразу, а можно сосредоточиться только на некоторой её части: на множествах корней алгебраических уравнений. Он позволяет для уравнения , которое очень легко решить в самом маленьком кольце вычетов , повысить эти элементарные решения до любого кольца , в котором такие решения существуют. Алгоритм поднятия решений алгебраического уравнения  с целыми (или p-адическими целыми) коэффициентами с помощью леммы Гензеля состоит в следующем: Находим решение  в кольце , например, перебором, если кольцо имеет небольшой порядок. На этом этапе надо убедиться в том, что в этом кольце . Для , имея решение  в  находим решение  в , следующим образом: , где . Решим уравнение  в  с точностью до 10 разрядов. Простым перебором обнаруживаем, что это уравнение имеет два решения:  в кольце . Мы можем поднять это решение до  за четыре шага: .   Если перевести результат в каноническое разложение, то получим десять цифр решения:   По существу, это метод Ньютона, хорошо известный в вещественном мире. На практике, если p-адические числа реализованы, как кольцо , проще вычислять корни уравнений напрямую методом Ньютона в этом большом кольце, без возни с промежуточными кольцами. Главное при этом, начинать итерации с корней, существующих в базовом кольце .  Самое замечательное, что количество итераций метода Ньютона будет таким же, как и при ручном применении леммы Гензеля, потому что корявая фрактальная p-адическая топология обеспечивает тотальную сходимость метода Ньютона, в отличие от во всех отношениях приятной топологии вещественной прямой, в которой как известно, область притяжения метода Ньютона даже для простых полиномов второго или третьего порядка уже представляет собой фрактальное множество. Вот такой дуализм. Коль скоро в нашем арсенале появился метод Ньютона, мы можем вычислять p-адические квадратные корни (если они существуют), при этом шаг итераций не будет отличаться от традиционного . Однако, он точно не сможет работать в , поскольку производная целевой функции  тождественно равна нулю на всём . Для этого случая можно использовать оригинальный рекурсивный метод:   Каждый новый множитель добавляет две новых верные цифры в каноническом разложении результата. Инструменты для работы с p-адическими числами есть во многих математических пакетах, таких как Sage, Maple, Magma и им подобных, на GitHub выложены библиотеки для многих языков, а теперь такая библиотека есть и в Hackage.  Для Haskell Андреем Лелеченко написан отличный пакет модулярной арифметики mod, с очень удобным интерфейсом и эффективной реализацией. Он и послужил основой для моей библиотеки. Каноническое разложение в список цифр используется только для текстового представления чисел, а вся арифметика производится в целочисленной модулярной арифметике. Простейшая работа с библиотекой выглядит так: Всё достаточно удобно и интуитивно. Продемонстрируем накопление ошибки округления при работе с IEEE и её отсутствие при работе с 10-адическими числами: В пакете определено достаточно большое количество функций, алгебраических и трансцендентных, реализован метод Ньютона. Так, например, можно решать алгебраические уравнения:  Производную надо задавать явно, но можно воспользоваться автоматическим дифференцированием, которое я не захотел тащить в библиотеку, чтобы не утяжелять её зависимости. Можно найти 10-адические автоморфные числа и убедиться в том, что в какую бы степень мы их не возвели, они останутся неизменными: А вот ещё числа, которых не встретишь в вещественном мире:  различных корней -ной степени из единицы, как в комплексных числах (правда, они существуют не для всех ). Методом Гензеля можно решать и трансцендентные уравнения тоже. Например, вот 7-адическое число, синус которого равен : Из-за особенностей округления в р-адических числах в них можно работать с невероятно большими числами, правда, только с младшими разрядами, наименее значащими в вещественном мире, но в модулярных арифметиках и они способны нести полезную информацию. Например, нам не составит особого труда вычислить младшие разряды знаменитого числа Грэма. Оно определяется через т.н. гипероперации — обобщения арифметических операций. Определим одну из них — тетрацию, то есть башню возведений в степень a ^^^ b = a ^ (a ^ (a ... a ^ a)) состоящую из b этажей. Здесь функция zPow — это p-адическое возведение в степень для кольца целых p-адических чисел. Значение этой функции быстро становятся такими большими, что в обычных вещественных числах с ними уже не поработаешь, тогда как в p-адической арифметике вычисления не занимают сколь-нибудь заметного времени.  Число Грэма строится на основе тетрации тройки. Нетрудно убедиться, что последовательность  сходится в  к некоторому предельному числу , которое решает уравнение : Существование этого предела означает, что какую бы огромную башню степеней тройки мы бы ни построили (а для числа Грэма она больше чем число планковских расстояний, укладывающихся в диаметр видимой Вселенной), мы получим всего лишь конечное округление числа ! В русскоязычной статье из Википедии приводится 50 младших разрядов числа Грэма. Работая в p-адической арифметике, мы легко можем получить хоть 50, хоть 100 последних цифр этого монстра: Работа над библиотекой padic доставила мне массу удовольствия!  Особенно приятно, что при реализации этой библиотеки удалось задействовать элементы зависимых типов, обеспечив типобезопасность и непредставимость некорректных данных, а также переведя часть вычислений на этап компиляции. На уровне типов определяются как модуль в котором мы работаем, так и задаваемая точность. Если точность не указана, она подбирается такой, чтобы можно было корректно реконструировать рациональные числа с числителями и знаменателями в пределах типа Int32 из p-адического представления. На уровне типов так же происходит и согласование модуля p-адического числа  и модуля его внутреннего представления  c требуемой точностью. Для этого используются зависимые типы, предоставляемые типами-литералами (type literals) и семействами типов (type families). Типы-литералы, представленные библиотекой GHC.TypeLits позволяют оперировать с натуральными числами и со строками, как с фантомными типами, параметризующими обычные типы данных, и к которым есть доступ в рантайме (лишь на чтение, изменять фантомы можно только на уровне типов). В качестве простого примера их использования создадим минимальный работающий прототип для целочисленной p-адической арифметики. Начнём с типа Mod p, который добавляет к натуральному числу информацию о том, к какому кольцу вычетов оно относится: Тип-фантом p присутствует только в левой части определения типа-обёртки над типом Integer. Его роль только помечать на уровне типов порядок кольца вычетов. Доступ к типу-параметру можно получить с помощью функции natVal: В результате мы получаем "нормальное" натуральное число, с которым можно проводить все возможные вычисления. Например, мы можем определить толковый способ вывода чисел в кольце вычетов на печать: Класс KnownNat содержит определение для функции natVal, поэтому его необходимо указывать в качестве условия (constraint). Получить доступ к фантомному типу-параметру можно и у ещё не созданного объекта. Вот, например, как можно реализовать базовый арифметический функционал: Этот нехитрый приём позволяет нам создавать параметризованные типы из чисел, которые "узнают" о том, в какую модулярную арифметику их погрузили от вызывающей стороны. Корректное представление p-адических чисел подразумевает ряд условий. Во-первых, основание  должно быть числом больше единицы. Во-вторых, внутреннее представление в виде кольца вычетов должно иметь модуль , где  должно вычисляться исходя и заданной точности округления p-адических чисел. Всё это можно сделать на уровне типов, используя семейства типов.  Эти условия и ограничения позволяют нам определить тип для целых p-адических чисел Чехарда с двумя вложенными типами Z и R понадобилась для того, чтобы обеспечить доступ к обоим фантомным типам, при том, что функция natVal способна "считать" только внешний тип-фантом. На этапе компиляции все эти типы-обертки исчезнут так что в результирующем коде останутся только операции над числами типа Integer заточенные для работы с p-адическими числами.
Осталось только сделать так, чтобы p-адические числа выводились в своей системе счисления. Для этого напишем примитивную реализацию метода show: По большому счёту, того, что мы сделали, уже достаточно для реализации простейшей p-адической арифметики. Для полноценной работы нужно ещё реализовать корректные переходы от рациональных чисел к p-адическим и обратно, а также многие другие функции. Те, кому интересны, эти детали, могут посмотреть код библиотеки на GitHub. Руководитель, научный сотрудник, преподаватель