有关 Haskell -> C# 转换的问题

本文关键字:转换 问题 Haskell 有关 | 更新日期: 2023-09-27 17:57:20

>Background:

我被"拖"进了这个问题:哈斯克尔
中的斐波那契闭式表达式当作者最初用许多其他语言标记,但后来专注于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

因此,根据我的研究和我可以推断的内容(如果我在任何地方都错了,请纠正我),第一部分使用具有两个Integer参数的构造函数声明类型 Ext(我猜将继承EqShow类型/模块)。

接下来是从Num"派生"的Ext的实现。 fromIntegerInteger执行转换。 negate是一元否定,然后是二进制加法和乘法运算符。

最后一部分是实际的斐波那契实现。

问题:

在答案中,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。

很简单。 将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);
    }
}

这是完全更新的来源。

有关 Haskell -> C# 转换的问题

[...],第一部分声明了带有两个整数参数的构造函数的 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 .这里有两个问题。据我发现,没有除法运算符,只有一个div 函数。所以我只需要在这里划分数字。这是对的吗?或者是否有一个除法运算符,但有一个单独的div 函数可以做其他特殊的事情?

在 Haskell 中,运算符和函数之间只有语法上的区别。可以通过将运算符写成括号(+)将函数视为函数,或者通过将其写成`backticks`将函数视为二元运算符。

div是整数除法,属于类型类Integral,因此它是为所有类似整数的类型定义的,包括Int(机器大小的整数)和Integer(任意大小的整数)。

我不知道如何解释行的开头。这只是一个简单的模式匹配吗?换句话说,这仅适用于第一个参数为 0 的情况吗?如果不匹配(第一个是非零的)会是什么结果?还是我应该解释它,因为我们不关心第一个参数并无条件地应用该函数?

提取

系数 √5 确实只是一个简单的模式匹配。整数部分与零匹配,以向读者表达我们确实希望它始终为零,并在代码中的某些错误导致它不为零时使程序崩溃。


一个小改进

将原始代码中的Integer替换为Rational,您可以编写更接近 Binet 公式fib n

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

这将在整个计算过程中执行除法,而不是将其全部保存到最后。这导致较小的中间数和计算fib (10^6)时大约 20% 的加速。

首先,NumShowEq是类型类,而不是类型或模块。它们有点类似于 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)

这似乎是解决方案中缺少的部分。

你对减法是对的。它是通过加法和否定来实现的。

现在,仅当 a 等于 0 时,divide函数才会除法。否则,我们将得到模式匹配失败,表明程序中存在错误。

div函数是一个简单的整数除法,等效于 C# 中应用于整型类型的/。在 Haskell 中还有一个运算符/,但它表示实数除法。

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