Многие, в школе или университете, сталкивались с понятием комплексных чисел, и наверняка большинству принятие математической истины, сокрытой в этих необычных числах, обобщающих действительные числа, показалось действительно тяжелым: согласитесь, как можно принять, что квадратный корень из -1 существует, да еще и не принадлежит к действительным числам, к которым мы так привыкли?!
Комплексные числа, как известно, родились из попытки решить уравнение:
x ^^ 2 = -1
и соответственно, из предположения, что такое число действительно существует (подумайте, основная теорема алгебры гласит «для любого алгебраического уравнения n-ой степени существует n корней» — и теперь, после осмысления этой давно доказанной теоремы, предположение кажется не только достойным, но и вполне законным), но что если комплексные числа — это еще не финальное обобщение для действительных чисел, а помимо них существуют и другие схожие по свойствам типы чисел?
Математики, работающие с числами наподобие комплексных, знают, что комплексные числа — это не единственный тип чисел, которые состоят из действительной и мнимой части, сами комплексные числа можно обобщить (если интересно, можете посмотреть про гиперкомплексные числа) и соответственно, комплексные числа — это еще не все…
Рассмотрим уравнение:
x ^^ 2 = 0
на первый взгляд, простейшее квадратное уравнение и его решение очень даже очевидно (x = 0, и точка), но что если, следуя логике, предположить, что таким образом (извлекая квадратный корень) мы нашли лишь одно из возможных решений ? Что если, у уравнения существует еще один корень, и он не равен нулю?
Если сделать такое предположение, то следующей очевидной гипотезой будет утверждение, что среди действительных чисел, не существует такого числа, отличного от нуля, которое бы при возведении во вторую степень дало бы ноль.
Так давайте же выдвинем гипотезу, что корень приведенного выше уравнения действительно существует, но во множестве каких-то других чисел, и тогда исходя из этого, введем обозначение для такого числа, а также для остальных чисел принадлежащих такому множеству.
Математики, столкнувшиеся впервые с такой же проблемой при рассмотрении уже упомянутого квадратного уравнения, придумали специальное обозначение для второго его корня: d или Ε (Ε — греческая буква эпсилон).
Что дальше? Мы знаем, что d отлично от нуля, а это значит, что умножая d на некоторое действительное число, мы получаем число, которое принадлежит к множеству некоторых чисел, которые не принадлежат к множеству действительных чисел. Более того, взяв некоторые числа a и b, мы можем получить некоторое число c, которое равно:
c = a + bd
такие числа получили название дуальных чисел, слагаемое a в них — называют действительной частью, а слагаемое bd — мнимой или дуальной частью (но все же, устоялось понятие «мнимая часть»). (Действительно, очень похоже на комплексные числа. К тому же, дуальные числа, это по сути дела, те же «комплексные числа», только «мнимая единица» другая.)
Ни в одном языке программирования не существует встроенного типа данных или встроенного класса для работы с дуальными числами, да и сами дуальные числа не слишком известны среди программистов, однако, можно легко реализовать тип данных для дуальных чисел, а также целый ряд базовых операций над ними. Конечно, соблазнительно было бы сделать подобное с помощью класса, однако в этом нет нужды: D позволяет использовать структуры данных, определяя для них методы, а также (что самое любопытное) разрешает перегружать операторы (для тех кто не в теме: перегрузка операторов — это прием в программировании, позволяющий изменять привычное поведение встроенных операторов языка, настраивая тем самым работу типовых операторов под нужные типы данных. Например, оператор «+» (сложение, бинарный оператор) для чисел и для матриц имеет одинаковый смысл, но при попытке сложения двух матриц этим оператором, вы скорее всего получите ошибку. Определив оператор «+» для матриц /т.е. перегрузив его, добавив к нему новую смысловую нагрузку/, вы можете складывать матрицы без опасений).
Определим структуру для дуальных чисел следующим образом:
struct DualNumber { private: float _re; float _im; // чисто действительное число this(float re) { _re = re; _im = 0.0; } // дуальное число this(float re, float im) { _re = re; _im = im; } }
где, _re и _im — действительная и мнимая части числа, соответственно. Эти переменные были сделаны закрытыми (private) для полной инкапсуляции данных, а также для исключения случайного повреждения самого дуального числа в ходе неосторожных манипуляций со структурой.
D запрещает создавать множество различных конструкторов, а потому вместо создания двух разных конструкторов, использована техника делегирования: сначала создаем конструктор с меньшим числом параметров внутри которого, выставляем один из параметров в значение по умолчанию; а затем создаем главный конструктор с нужным количеством параметров — происходит как бы преобразование вызова первого конструктора в вызов второго, но в котором один из параметров уже был зафиксирован ранее (т.е. вызов this(x) превращается в вызов this(x, 0.0), надеюсь, так понятнее будет).
Конструктор с одним параметром, по сути дела, создает обычное действительное число (если мнимая часть дуального числа равна нулю, то очевидно перед нами действительное число), а конструктор с двумя параметрами — дуальное число (если хотите создать число с нулевой действительной частью, то придется вызвать this(0.0, x)).
Поскольку, внутренние переменные _re и _im закрыты, то соответственно, просмотреть их будет невозможно, а знание чему равны эти переменные может однажды очень потребоваться, поэтому проще всего реализовать их в виде свойств, к которым можно легко получить доступ, а помимо этого добавить еще ряд полезных свойств:
// действительная часть @property float re() { return _re; } // мнимая часть @property float im() { return _im; } // модуль @property float abs() { return sqrt((_re ^^ 2) + (_im ^^ 2)); } // сопряженное число @property DualNumber conj() { return DualNumber(_re, -_im); }
свойства re и im выдают действительную и мнимую часть дуального числа, свойство abs — модуль числа (вычисляется как сумма квадратов действительной и мнимой части), а свойство conj — сопряженное дуальное число (превращает дуальное число a + bd в число a — bd).
Однако, этого мало: необходимо еще реализовать хотя бы базовые арифметические операции над новым классом чисел, что делается сравнительно легко с помощью перегрузки арифметических операторов.
Чтобы перегрузить бинарный (т.е манипулирующий двумя величинами) оператор <оператор> необходимо перегрузить метод opBinary, для чего необходимо воспользоваться примерно вот таким шаблоном кода:
<имя структуры или класса> opBinary(string op : "<оператор>")(<имя структуры или класса> rhs) { }
где rhs — это значение, которое будет справа от <оператор> (а сама структура или класс с перегруженным оператором будет в выражении слева от <оператор>).
Простые действия с дуальными числами выглядят так:
(a + be) + (c + de) = (a + c) + (b + d)e (a + be) - (c + de) = (a - c) + (b - d)e (a + be) * (c + de) = (a * c) + (b * c + a * d)e (a + be) / (c + de) = (a / c) + ((b * c + a * d) / (c ^^ 2))e (a + be) ^^ (c + de) = (a ^^ c) + (b * c * (a ^^ (c - 1.0)) + d * (a ^^ c) * log(a))e;
(где a,b,c,d — некоторые действительные числа, а e — это та «самая дуальная единица»)
а их реализации в D достаточно проста:
// возможные операции с дуальными числами auto opBinary(string op)(DualNumber rhs) { DualNumber tmp; switch (op) { case "+": tmp = DualNumber(_re + rhs.re, _im + rhs.im); break; case "-": tmp = DualNumber(_re - rhs.re, _im - rhs.im); break; case "*": tmp = DualNumber(_re * rhs.re, _im * rhs.re + _re * rhs.im); break; case "/": tmp = DualNumber(_re / rhs.re, (_im * rhs.re - _re * rhs.im) / (rhs.re ^^ 2)); break; case "^^": float r = _re ^^ rhs.re; float i = _im * rhs.re * (_re ^^ (rhs.re - 1.0f)) + rhs.im * r * log(_re); tmp = DualNumber(r, i); break; default: throw new Exception("Operator " ~ op ~ "is not implemented"); break; } return tmp; }
что является обобщенной формой уже описанного шаблона перегрузки бинарных операторов (код для действий над дуальными числами написан таким образом, что бы было меньше разбросанных методов, описывающих бинарные операторы).
Если после перегрузки операторов попытаться произвести элементарные математические операции над дуальными числами, то все прекрасно получиться, но … дуальные числа будут отображаться также как и обычные структуры, а не так как будто это числа. Что делать?
Для любой структуры или любого класса допускается перегрузка метода toString(), который отвечает за преобразование в строку некоторого объекта. Таким образом, необходимо просто перегрузить этот метод для нашей структуры дуальных чисел:
// перевод в строку public string toString() { string tmp; if (_im >= 0) { tmp ~= to!string(_re) ~ "+" ~ to!string(_im) ~ "e"; } else { tmp ~= to!string(_re) ~ "-" ~ to!string(fabs(_im)) ~ "e"; } return tmp; }
работает все элементарно: если мнимая часть отрицательная, то очевидно в представлении числа a + bi, у b будет знак «-«, что мы и учитываем при склейке через этот знак преобразованной в строку действительной части и преобразованного в строку модуля мнимой части (помимо этого, получившую строку склеиваем с символом «e», обозначающий, как вам уже известно, квадратный корень из нуля, неравный нулю). То же самое действие производится и в случае положительности мнимой части, но в склейке фигурирует знак «+».
Давайте теперь определим какую-нибудь простую функцию над дуальными числами, к примеру функцию:
f(x) = (x ^^ 2) + 3 * x;
которая в коде выглядит так:
DualNumber testFunc(DualNumber x) { return (x ^^ (DualNumber(2.0))) + DualNumber(3.0) * x; }
здесь все как обычно, функция для дуальных чисел определяется точно также как и для любых других чисел.
Как видите — все просто.
А теперь маленькая хитрость: если в testFunc передать аргумент вида DualNumber(x), где x — некоторое число, то, если от полученного результата взять действительную часть, то получим значение testFunc в точке x (т.е как будто мы посчитали эту функцию для действительного числа), что очевидно, если посмотреть определение дуального числа.
Казалось бы, определение функции в дуальных числах абсолютно бесполезно, однако это далеко не так: если в функцию, работающую с дуальными числами, передать аргумент вида DualNumber(x, 1), а потом взять только мнимую часть результата функции, то мы получим производную функции в точке x!
Теперь пользуясь этим приемом, который называется «автоматическое дифференцирование» можно без лишнего напряжения численно вычислять производные практически любых функций:
float funcDerivable(DualNumber function(DualNumber) f, float x) { return f(DualNumber(x, 1.0)).im; }
полученная функция довольно необычна — она принимает в качестве аргумента функциональный литерал (т.е некоторую функцию или указатель на нее) и некоторый числовой аргумент, а выдает число с плавающей точкой; таким образом получается независимость от каких-либо функций — функция для которой вычисляется производная в некоторой точке не является фиксированной, а может быть абсолютно любой допустимой в данном контексте и в языке программирования D.
Попробуем применить функцию, воспользовавшись вот таким кодом:
writeln(funcDerivable(x => testFunc(x), 0.5));
ничего сложного, однако, конструкция вида x => testFunc(x) внушает подозрения…
На самом деле ничего подозрительного нет, и надпись x => testFunc(x) обозначает анонимную функцию (т.е функцию без имени или лямбда-функция, которая существует внутри некоторого выражения или в связанном с переменной виде), которая использована для конструирования на лету корректного функционального литерала, передаваемого в функцию вычисления производной. Анонимная функция передается в качестве аргумента в производную и существует только внутри круглых скобок этой функции, прямым следствием из этого является тот факт, что переменная x не требует объявления и недоступна нигде, кроме определения лямбда-функции.
Запустив этот пример, на выводе получим цифру 4 (для тех, кто не понял откуда она взялась, поясняю: производной функции x ^^ 2 — 3 * x является функция 2 * x + 3, которая для аргумента 0.5 дает значение 4), чего и следовало ожидать.
Весь код приложения:
import std.stdio; import std.math; import std.conv : to; void main() { writeln(funcDerivable(x => testFunc(x), 0.5)); } struct DualNumber { private: float _re; float _im; // чисто действительное число this(float re) { _re = re; _im = 0.0; } // дуальное число this(float re, float im) { _re = re; _im = im; } // действительная часть @property float re() { return _re; } // мнимая часть @property float im() { return _im; } // модуль @property float abs() { return sqrt((_re ^^ 2) + (_im ^^ 2)); } // сопряженное число @property DualNumber conj() { return DualNumber(_re, -_im); } // возможные операции с дуальными числами auto opBinary(string op)(DualNumber rhs) { DualNumber tmp; switch (op) { case "+": tmp = DualNumber(_re + rhs.re, _im + rhs.im); break; case "-": tmp = DualNumber(_re - rhs.re, _im - rhs.im); break; case "*": tmp = DualNumber(_re * rhs.re, _im * rhs.re + _re * rhs.im); break; case "/": tmp = DualNumber(_re / rhs.re, (_im * rhs.re - _re * rhs.im) / (rhs.re ^^ 2)); break; case "^^": float r = _re ^^ rhs.re; float i = _im * rhs.re * (_re ^^ (rhs.re - 1.0f)) + rhs.im * r * log(_re); tmp = DualNumber(r, i); break; default: throw new Exception("Operator " ~ op ~ "is not implemented"); break; } return tmp; } // перевод в строку public string toString() { string tmp; if (_im >= 0) { tmp ~= to!string(_re) ~ "+" ~ to!string(_im) ~ "e"; } else { tmp ~= to!string(_re) ~ "-" ~ to!string(fabs(_im)) ~ "e"; } return tmp; } } DualNumber testFunc(DualNumber x) { return (x ^^ (DualNumber(2.0))) + DualNumber(3.0) * x; } float funcDerivable(DualNumber function(DualNumber) f, float x) { return f(DualNumber(x, 1.0)).im; }
Наверняка автоматическим дифференцированием применение дуальных чисел не ограничивается, однако, наиболее известно именно оно: в заключение, мне хочется сказать, что далеко не все операции над дуальными числами были реализованы, а все подробности об этом доступны здесь (жаль, что на английском, однако не принципиально).