Вопросы по конвертации на Haskell -> C # - PullRequest
14 голосов
/ 21 мая 2011

Предыстория:

Меня «перетянули», когда я увидел этот вопрос: Выражение закрытой формы Фибоначчи в 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.Но что это значит и как это на самом деле применяется к этому типу?Есть ли неявное число «поле», которое я пропускаю?Применяет ли оно возведение в степень к каждому соответствующему числу, которое оно содержит?Я предполагаю, что он делает последнее, и в итоге получается код C #:

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);
    }
}

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

Ответы [ 3 ]

9 голосов
/ 21 мая 2011

[...], первая часть объявляет тип 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).

8 голосов
/ 21 мая 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 также есть оператор /, но он указывает на деление действительного числа.

4 голосов
/ 21 мая 2011

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

Интересно сравнить этот тип, имеющий форму a+b*Sqrt(5), с комплексными числами, которые принимают форму a+b*Sqrt(-1). Сложение и вычитание работают точно так же. Умножение немного отличается, потому что я ^ 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();
}
...