Вопросы по Haskell -> конвертация C#

Задний план:

Меня «затащили» на этот вопрос: Выражение закрытой формы Фибоначчи в Haskell
когда автор сначала использовал теги многих других языков, но позже сосредоточился на вопросе Haskell. К сожалению, у меня нет опыта работы с Haskell, поэтому я не мог участвовать в этом вопросе. Однако один из ответов привлек мое внимание, куда повернулся ответчик это в чисто целочисленную математическую задачу. Это звучало для меня потрясающе, поэтому мне пришлось выяснить, как это работает, и сравнить это с рекурсивной реализацией Фибоначчи, чтобы увидеть, насколько она точна. У меня есть ощущение, что если бы я просто вспомнил соответствующую математику, связанную с иррациональными числами, я мог бы решить все сам (но я этого не делаю). Так что первым шагом для меня было портировать его на язык, с которым я знаком. В данном случае я делаю C#.

К счастью, я не совсем в темноте. У меня большой опыт работы с другим функциональным языком (OCaml), поэтому многое из этого показалось мне несколько знакомым. Начав с преобразования, все казалось простым, так как он фактически определял новый числовой тип для облегчения вычислений. Однако я столкнулся с парой препятствий в переводе, и у меня возникли проблемы с его завершением. Я получаю совершенно неправильные результаты.

Анализ:

Вот код, который я перевожу:

data Ext = Ext !Integer !Integer
    deriving (Eq, Show)

instance Num Ext where
    fromInteger a = Ext a 0
    negate (Ext a b) = Ext (-a) (-b)
    (Ext a b) + (Ext c d) = Ext (a+c) (b+d)
    (Ext a b) * (Ext c d) = Ext (a*c + 5*b*d) (a*d + b*c) -- easy to work out on paper
    -- remaining instance methods are not needed

fib n = divide $ twoPhi^n - (2-twoPhi)^n
  where twoPhi = Ext 1 1
        divide (Ext 0 b) = b `div` 2^n -- effectively divides by 2^n * sqrt 5

Итак, основываясь на моем исследовании и том, что я могу вывести (поправьте меня, если я где-то ошибаюсь), первая часть объявляет тип Ext с конструктором, который будет иметь два параметра Integer (и, я думаю, унаследует типы/модули Eq и Show) .

Далее следует реализация Ext, которая «происходит» от Num. fromInteger выполняет преобразование из Integer. negate — это унарное отрицание, а затем есть двоичные операторы сложения и умножения.

Последняя часть — это фактическая реализация Фибоначчи.

Вопросы:

В ответе Хаммар (ответчик) упоминает, что возведение в степень обрабатывается реализацией по умолчанию в Num. Но что это значит и как это на самом деле применяется к этому типу? Есть ли неявное числовое «поле», которое мне не хватает? Применяет ли он просто возведение в степень к каждому соответствующему числу, которое оно содержит? Я предполагаю, что это делает последнее и в итоге получается этот код С#:

public static Ext operator ^(Ext x, int p) // "exponent"
{
    // just apply across both parts of Ext?
    return new Ext(BigInt.Pow(x.a, p), BigInt.Pow(x.b, p));
    //     Ext     (a^p)               (b^p)
}

Однако это противоречит тому, как я понимаю, зачем нужен negate, он не понадобится, если это действительно произойдет.


Теперь суть кода. Я прочитал первую часть divide $ twoPhi^n - (2-twoPhi)^n как:

разделить результат следующего выражения: twoPhi^n - (2-twoPhi)^n.

Довольно просто. Возведите twoPhi в n степень. Вычтите из этого результат остальных. Здесь мы делаем двоичное вычитание, но реализовали только унарное отрицание. Или мы не сделали? Или можно ли подразумевать двоичное вычитание, потому что его можно составить, комбинируя сложение и отрицание (что у нас есть)? Я предполагаю последнее. И это облегчает мою неуверенность в отрицании.


Последняя часть — это фактическое деление: divide (Ext 0 b) = b `div` 2^n. Здесь две проблемы. Из того, что я нашел, нет оператора деления, только функция `div`. Так что мне просто нужно разделить числа здесь. Это правильно? Или есть оператор деления, но отдельная функция `div`, которая делает что-то особенное?

Я не уверен, как интерпретировать начало строки. Это просто совпадение с образцом? Другими словами, будет ли это применяться только в том случае, если первым параметром будет 0? Каким был бы результат, если бы он не совпадал (первое было ненулевым)? Или я должен интерпретировать это, поскольку мы не заботимся о первом параметре и применяем функцию безоговорочно? Это кажется самым большим препятствием, и использование любой интерпретации по-прежнему дает неверные результаты.

Я сделал какие-либо неправильные предположения в любом месте? Или все в порядке и я просто неправильно реализовал C#?

Код:

Вот (нерабочий) перевод и полный исходный код (включая тесты), поэтому далеко, если кому интересно.

// code removed to keep post size down
// full source still available through link above

Прогресс:

Итак, глядя на ответы и комментарии до сих пор, я думаю, что знаю, куда идти дальше и почему.

Возведение в степень просто нужно было сделать, как обычно, умножить p раз, учитывая, что мы реализовали операцию умножения. Мне никогда не приходило в голову, что мы должны делать то, что нам всегда говорили на уроках математики. Подразумеваемое вычитание из сложения и отрицания также является довольно удобной функцией.

Также заметил опечатку в моей реализации. Я добавил, когда я должен был умножить.

// (Ext a b) * (Ext c d) = Ext (a*c + 5*b*d) (a*d + b*c)
public static Ext operator *(Ext x, Ext y)
{
    return new Ext(x.a * y.a + 5*x.b*y.b, x.a*y.b + x.b*y.a);
    //                 ^ oops!
}

Вывод:

Итак, теперь это завершено. Я реализовал только основные операторы и немного переименовал их. Названы аналогично комплексным числам. Пока что соответствует рекурсивной реализации даже при очень больших входных данных. Вот окончательный код.

static readonly Complicated TWO_PHI = new Complicated(1, 1);
static BigInt Fib_x(int n)
{
    var x = Complicated.Pow(TWO_PHI, n) - Complicated.Pow(2 - TWO_PHI, n);
    System.Diagnostics.Debug.Assert(x.Real == 0);
    return x.Bogus / BigInt.Pow(2, n);
}

struct Complicated
{
    private BigInt real;
    private BigInt bogus;

    public Complicated(BigInt real, BigInt bogus)
    {
        this.real = real;
        this.bogus = bogus;
    }
    public BigInt Real { get { return real; } }
    public BigInt Bogus { get { return bogus; } }

    public static Complicated Pow(Complicated value, int exponent)
    {
        if (exponent < 0)
            throw new ArgumentException(
                "only non-negative exponents supported",
                "exponent");

        Complicated result = 1;
        Complicated factor = value;
        for (int mask = exponent; mask != 0; mask >>= 1)
        {
            if ((mask & 0x1) != 0)
                result *= factor;
            factor *= factor;
        }
        return result;
    }

    public static implicit operator Complicated(int real)
    {
        return new Complicated(real, 0);
    }

    public static Complicated operator -(Complicated l, Complicated r)
    {
        var real = l.real - r.real;
        var bogus = l.bogus - r.bogus;
        return new Complicated(real, bogus);
    }

    public static Complicated operator *(Complicated l, Complicated r)
    {
        var real = l.real * r.real + 5 * l.bogus * r.bogus;
        var bogus = l.real * r.bogus + l.bogus * r.real;
        return new Complicated(real, bogus);
    }
}

А вот и полностью обновленный источник.


person Jeff Mercado    schedule 21.05.2011    source источник
comment
Оператор ^ означает «побитовое XOR» в C#. Вы можете перегрузить его, чтобы он означал возведение в степень, но в культуре C# это будет считаться запутанным и нежелательным, поскольку это неожиданное несоответствие. Вместо этого рассмотрите возможность объявления метода Pow.   -  person Timwi    schedule 21.05.2011
comment
@Timwi: Конечно. Прежде всего, моей главной целью было получить работающую реализацию, максимально приближенную к исходному коду. Как только я это получу, я буду C#erize еще больше. ;)   -  person Jeff Mercado    schedule 22.05.2011


Ответы (3)


[...], первая часть объявляет тип Ext с конструктором, который будет иметь два целочисленных параметра (и я предполагаю, что он унаследует типы/модули Eq и Show).

Eq и Show являются классами типов. Вы можете думать о них как о интерфейсах в C#, только более мощных. deriving — это конструкция, которую можно использовать для автоматической генерации реализаций нескольких стандартных классов типов, включая Eq, Show, Ord и другие. Это уменьшает количество шаблонов, которые вам нужно написать.

Часть instance Num Ext обеспечивает явную реализацию класса типов Num. Вы правильно поняли большую часть этой части.

[ответчик] упоминает, что возведение в степень обрабатывается реализацией по умолчанию в Num. Но что это значит и как это на самом деле применяется к этому типу? Есть ли неявное числовое «поле», которое мне не хватает? Применяет ли он просто возведение в степень к каждому соответствующему числу, которое оно содержит?

Это было немного непонятно с моей стороны. ^ не относится к классу типов Num, но является вспомогательной функцией, полностью определенной в терминах методов Num, вроде метода расширения. Он реализует возведение в степень положительных целых степеней посредством бинарного возведения в степень. Это главная «фишка» кода.

[...] мы делаем двоичное вычитание, но реализовали только унарное отрицание. Или мы не сделали? Или можно ли подразумевать двоичное вычитание, потому что его можно составить, комбинируя сложение и отрицание (что у нас есть)?

Правильный. Реализация двоичного минуса по умолчанию — x - y = x + (negate y).

Последняя часть — это фактическое деление: divide (Ext 0 b) = b `div` 2^n. Здесь две проблемы. Из того, что я нашел, нет оператора деления, только функция div. Так что мне просто нужно разделить числа здесь. Это правильно? Или есть оператор деления, но отдельная функция div, которая делает что-то особенное?

Между операторами и функциями в Haskell есть только синтаксическая разница. Можно рассматривать оператор как функцию, написав его в скобках (+), или рассматривать функцию как бинарный оператор, написав его в `backticks`.

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

Я не уверен, как интерпретировать начало строки. Это просто совпадение с образцом? Другими словами, будет ли это применяться только в том случае, если первый параметр равен 0? Каким был бы результат, если бы он не совпадал (первое было ненулевым)? Или я должен интерпретировать это, поскольку мы не заботимся о первом параметре и применяем функцию безоговорочно?

Это действительно простое сопоставление шаблона для извлечения коэффициента √5. Целая часть сопоставляется с нулем, чтобы показать читателям, что мы действительно ожидаем, что она всегда будет равна нулю, и чтобы программа вылетала, если какая-то ошибка в коде приводила к тому, что она не была равна нулю.


Небольшое улучшение

Заменив Integer на Rational в исходном коде, вы можете записать fib n еще ближе к формуле Бине:

fib n = divSq5 $ phi^n - (1-phi)^n
  where divSq5 (Ext 0 b) = numerator b
        phi = Ext (1/2) (1/2)

Это выполняет деление на протяжении всего вычисления, а не сохраняет все на конец. Это приводит к меньшим промежуточным числам и ускорению примерно на 20% при вычислении fib (10^6).

person hammar    schedule 21.05.2011
comment
О да. Спасибо, что ответили и особенно ответили на мои вопросы напрямую. Трюк с рациональными числами тоже звучит неплохо. Должно быть легко реализовать. Это будет в списке дел. - person Jeff Mercado; 22.05.2011
comment
Быстрый вопрос по вычитанию. Не было бы проще реализовать вычитание напрямую, чем сложение и отрицание одновременно? Или это ограничение Haskell для реализации вычитания таким образом? Насколько я мог судить, эти две операции не использовались напрямую в реализации или, возможно, косвенно в используемых здесь операциях по умолчанию. - person Jeff Mercado; 22.05.2011
comment
@Jeff: Это был просто стилистический выбор. Я мог бы так же легко реализовать вычитание напрямую. - person hammar; 22.05.2011
comment
@Jeff: Num имеет реализацию по умолчанию - с точки зрения negate и наоборот. Остальные операции должны быть реализованы напрямую. В данном случае я пропустил abs и signum, так как они не имеют значения. Компилятор выдаст предупреждение об этих отсутствующих методах, и попытка вызвать один из них вызовет исключение во время выполнения. - person hammar; 22.05.2011
comment
Я бы не назвал классы типов более мощными, чем интерфейсы. Во всяком случае, они менее мощные. - person Rotsor; 22.05.2011
comment
@Rotsor: Одним из примеров является то, что классы типов позволяют вам абстрагироваться не только от типов, но и от конструкторов типов. - person hammar; 22.05.2011
comment
@hammar, правда, у них масса достоинств. Но статическая диспетчеризация кажется серьезным ограничением, которое может легко сделать классы типов непригодными для использования в некоторых ситуациях. Однако, подумав об этом еще немного, я вспомнил какое-то расширение GHC, позволяющее делать своего рода динамическую диспетчеризацию (с помощью кванторов существования). Имея это в виду, я поднимаю свой вызов. - person Rotsor; 22.05.2011
comment
@Rotsor: классы типов явно поддерживают динамическую диспетчеризацию. Если вы напишете такой метод, как sort :: Ord a => [a] -> [a], компилятор не сможет знать, какой экземпляр использовать во время компиляции. Таким образом, динамическая отправка (в GHC с использованием словарей методов) необходима для разрешения используемой функции. Разница в том, что система типов Haskell настолько продвинута, что вся информация о типе может быть удалена во время выполнения - если вы явно не требуете, чтобы код (через контекст Typeable) знал о его типе, нет тип хранится в памяти. - person fuz; 22.05.2011
comment
@FUZxxl, компилятор знает, какой экземпляр использовать во время компиляции при компиляции модулей с использованием sort, поэтому внутреннее использование динамической диспетчеризации является скорее удобством компилятора (позволяющим компилировать модули отдельно от их использования), чем необходимостью. В любом случае, внутренности компилятора здесь не имеют большого значения. Важно то, что вы не можете использовать sort, который использует диспетчеризацию на основе классов, для реализации sortBy, который действительно использует динамическую диспетчеризацию с помощью функций первого класса. - person Rotsor; 23.05.2011

Во-первых, Num, Show, Eq — это классы типов, а не типы и не модули. Они немного похожи на интерфейсы в C#, но разрешаются статически, а не динамически.

Во-вторых, возведение в степень выполняется посредством умножения с реализацией ^, которая не является членом класса типов Num, а является отдельной функцией.

реализация заключается в следующем:

(^) :: (Num a, Integral b) => a -> b -> a
x0 ^ y0 | y0 < 0    = error "Negative exponent"
        | y0 == 0   = 1
        | otherwise = f x0 y0
    where -- f : x0 ^ y0 = x ^ y
          f x y | even y    = f (x * x) (y `quot` 2)
                | y == 1    = x
                | otherwise = g (x * x) ((y - 1) `quot` 2) x
          -- g : x0 ^ y0 = (x ^ y) * z
          g x y z | even y = g (x * x) (y `quot` 2) z
                  | y == 1 = x * z
                  | otherwise = g (x * x) ((y - 1) `quot` 2) (x * z)

Кажется, это недостающая часть решения.

Вы правы насчет вычитания. Реализуется через сложение и отрицание.

Теперь функция divide делится только в том случае, если a равно 0. В противном случае мы получаем ошибку сопоставления с образцом, что указывает на ошибку в программе.

Функция div — это простое целочисленное деление, эквивалентное /, применяемому к целочисленным типам в C#. В Haskell также есть оператор /, но он указывает на деление действительных чисел.

person Rotsor    schedule 21.05.2011
comment
Класс типа подобен интерфейсу в Java, если это вам поможет. - person fuz; 21.05.2011
comment
@FUZ: Вот о чем я думал, хотя я не знал названия термина в Haskell, поэтому я придерживался этого. :) - person Jeff Mercado; 21.05.2011

Быстрая реализация на C#. Я реализовал возведение в степень, используя алгоритм возведения в квадрат и умножения.

Полезно сравнить этот тип, имеющий форму a+b*Sqrt(5), с комплексными числами, имеющими форму a+b*Sqrt(-1). Сложение и вычитание работают одинаково. Умножение немного отличается, потому что i^2 здесь не -1, а +5. Разделение немного сложнее, но и не должно быть слишком сложным.

Возведение в степень определяется как умножение числа на себя n раз. Но конечно это медленно. Поэтому мы используем тот факт, что ((a*a)*a)*a идентично (a*a)*(a*a), и переписываем, используя алгоритм возведения в квадрат и умножения. Так что нам просто нужно log(n) умножений вместо n умножений.

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

struct MyNumber
{
    public readonly BigInteger Real;
    public readonly BigInteger Sqrt5;

    public MyNumber(BigInteger real,BigInteger sqrt5)
    {
        Real=real;
        Sqrt5=sqrt5;
    }

    public static MyNumber operator -(MyNumber left,MyNumber right)
    {
        return new MyNumber(left.Real-right.Real, left.Sqrt5-right.Sqrt5);
    }

    public static MyNumber operator*(MyNumber left,MyNumber right)
    {
        BigInteger real=left.Real*right.Real + left.Sqrt5*right.Sqrt5*5;
        BigInteger sqrt5=left.Real*right.Sqrt5 + right.Real*left.Sqrt5;
        return new MyNumber(real,sqrt5);
    }

    public static MyNumber Power(MyNumber b,int exponent)
    {
        if(!(exponent>=0))
            throw new ArgumentException();
        MyNumber result=new MyNumber(1,0);
        MyNumber multiplier=b;
        while(exponent!=0)
        {
            if((exponent&1)==1)//exponent is odd
                result*=multiplier;
            multiplier=multiplier*multiplier;
            exponent/=2;
        }
        return result;
    }

    public override string ToString()
    {
        return Real.ToString()+"+"+Sqrt5.ToString()+"*Sqrt(5)";
    }
}

BigInteger Fibo(int n)
{
    MyNumber num = MyNumber.Power(new MyNumber(1,1),n)-MyNumber.Power(new MyNumber(1,-1),n);
    num.Dump();
    if(num.Real!=0)
      throw new Exception("Asser failed");
    return num.Sqrt5/BigInteger.Pow(2,n);
}

void Main()
{
  MyNumber num=new MyNumber(1,2);
  MyNumber.Power(num,2).Dump();
  Fibo(5).Dump();
}
person CodesInChaos    schedule 21.05.2011
comment
Ах, это то, что мне нужно было увидеть, сравнение с комплексными числами. Теперь я могу лучше представить, что представляет собой этот тип. - person Jeff Mercado; 21.05.2011
comment
Почему «*Sqrt(5)» в ToString, когда можно написать гораздо более читаемую «√5»? Вряд ли кто-то попытается разобрать и выполнить вывод ToString() :) - person Timwi; 21.05.2011