14

バックグラウンド:

著者が最初に他の多くの言語でタグ付けしたが、後にHaskellの質問に焦点を合わせたとき、私はこの質問を見ることに「引きずり込まれ」 ました。
残念ながら、私はHaskellの経験がまったくないので、質問に実際に参加することができませんでした。しかし、答えの1つが私の目に留まり、答え者はそれを純粋な整数の数学の問題に変えました。それは素晴らしかっ私にとっては、それがどのように機能するかを理解し、これを再帰的なフィボナッチの実装と比較して、それがどれほど正確であるかを確認する必要がありました。無理数を含む関連する数学を思い出せば、自分ですべてを解決できるかもしれないと感じています(しかし、私はそうしません)。ですから、私にとっての最初のステップは、それを私が精通している言語に移植することでした。この場合、私は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は2つのパラメーターを持つコンストラクターで型を宣言します(そして型/モジュールIntegerを継承すると思います)。EqShow

次は、Extから「派生」する実装ですNumfromIntegerからの変換を実行しIntegerます。 negateは単項否定であり、次に2進加算および乗算演算子があります。

最後の部分は、実際のフィボナッチの実装です。

質問:

回答の中で、hammar(回答者)は、べき乗はのデフォルトの実装によって処理されると述べています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。

ものすごく単純。3乗twoPhinます。それから残りの結果を引きます。ここではバイナリ減算を行っていますが、単項否定のみを実装しました。それとも私たちはしませんでしたか?または、加算と否定(私たちが持っている)を組み合わせて構成することができるので、バイナリ減算を暗示することができますか?私は後者を想定しています。そして、これは否定についての私の不確実性を和らげます。


最後の部分は実際の分割です:divide (Ext 0 b) = b `div` 2^n。ここで2つの懸念。私が見つけたところによると、除算演算子はなく、`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);
    }
}

そして、これが完全に更新されたソースです。

4

3 に答える 3

9

[...]、最初の部分は、2つの整数パラメーターを持つコンストラクターを使用してタイプExtを宣言します(そして、EqおよびShowタイプ/モジュールを継承すると思います)。

EqShow型クラスです。それらはC#のインターフェイスに似ていると考えることができますが、より強力です。は、、、などderivingを含む少数の標準型クラスの実装を自動的に生成するために使用できる構造です。これにより、作成する必要のある定型文の量が減ります。EqShowOrd

このinstance Num Ext部分は、Num型クラスの明示的な実装を提供します。あなたはこの部分のほとんどを正しく理解しました。

[回答者]は、べき乗はNumのデフォルトの実装によって処理されると述べています。しかし、それはどういう意味で、実際にこのタイプにどのように適用されるのでしょうか。私が見逃している暗黙の数体「フィールド」はありますか?含まれている対応する各数値にべき乗を適用するだけですか?

これは私の側では少し不明確でした。^型クラスにはありませんが、拡張メソッドのようにNum、メソッドに関して完全に定義された補助関数です。バイナリべき乗Numを介して正の整数乗へのべき乗を実装します。これがコードの主な「トリック」です。

[...]バイナリ減算を実行していますが、単項否定のみを実装しました。それとも私たちはしませんでしたか?または、バイナリ減算は、(私たちが持っている)結合加算と否定で構成されている可能性があるため、暗示することができますか?

正しい。バイナリマイナスのデフォルトの実装はですx - y = x + (negate y)

最後の部分は実際の除算です:divide (Ext 0 b) = b `div` 2^n。ここで2つの懸念。私が見つけたところによると、除算演算子はなく、div関数だけです。だから私はここで数字を割る必要があります。これは正しいです?または、除算演算子がありますが、何か特別なことをする別のdiv関数がありますか?

Haskellの演算子と関数の間には構文上の違いしかありません。演算子を括弧(+)で囲むことで関数として扱うことも、関数をで書くことで二項演算子として扱うこともでき`backticks`ます。

divは整数除算であり、型クラスに属しているため、 (マシンサイズの整数)や(任意のサイズの整数)Integralを含むすべての整数のような型に対して定義されます。IntInteger

行頭の解釈方法がわかりません。単純なパターンマッチですか?言い換えると、これは最初のパラメーターが0の場合にのみ適用されますか?一致しなかった場合(最初はゼロ以外)、結果はどうなりますか?または、最初のパラメーターを気にせず、関数を無条件に適用するので、それを解釈する必要がありますか?

√5の係数を抽出するのは確かに単純なパターンマッチです。整数部分はゼロと照合され、読者に常にゼロであると実際に期待していることを示し、コードのバグが原因でプログラムがクラッシュしないようにします。


小さな改善

元のコードをで置き換えるIntegerと、 Binetの式にさらに近づく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)

于 2011-05-21T19:23:05.447 に答える
8

まずNum、、、は型クラスであり、型やモジュールではありませんShowEqこれらは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関数はa0に等しい場合にのみ分割されます。それ以外の場合は、プログラムのバグを示すパターン一致の失敗が発生します。

このdiv関数は単純な整数除算で/あり、C#の整数型に適用されるのと同じです。Haskellにも演算子がありますが/、これは実数の除算を示しています。

于 2011-05-21T15:26:29.887 に答える
4

C#での簡単な実装。二乗と乗算のアルゴリズムを使用してべき乗を実装しました。

a+b*Sqrt(5)形をとるこのタイプを、形をとる複素数と比較することは啓発的a+b*Sqrt(-1)です。足し算と引き算はまったく同じように機能します。ここではi^2は-1ではなく+5であるため、乗算は少し異なります。分割は少し複雑ですが、それほど難しくないはずです。

べき乗は、数値にそれ自体をn回乗算することとして定義されます。しかしもちろんそれは遅いです。したがって、2乗と乗算のアルゴリズムと((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();
}
于 2011-05-21T15:44:10.017 に答える