【问题标题】:What is the type of this self-applying factorial function?这个自应用阶乘函数的类型是什么?
【发布时间】:2015-03-05 01:37:29
【问题描述】:

我用 C++ 编写了一个匿名阶乘函数,并用 g++4.9.2 编译了我的代码。 它运作良好。但是,我不知道我的函数的类型。

#include<iostream>
#include<functional>
using std::function;
int main()
{
    //tested at g++ 4.9.2
    //g++ -std=c++1y -o anony anony.cpp
    auto fac = [](auto self,auto n)->auto{
        if(n < 1)
            return 1;
        else 
            return n * self(self,n-1);
    };
    std::cout<<fac(fac,3)<<std::endl;//6
    return 0;
}

那么,我想知道:facself 的类型是什么? 如果我只是将 C++ 代码翻译成 Haskell,它将无法编译,因为 它涉及无限类型:

fac2 self 0 = 1
fac2 self n = n * (self self $ n-1)

我必须围绕它定义一些递归类型的工作:

data Y a = Y ((Y a)->a->a)
fac2 self 0 = 1
fac2 self n = n * ((applY self self) (n-1))
    where applY (Y f1) f2 = f1 f2
fact2 = fac2 $ Y fac2

那么,为什么 g++ 能得到完全正确的 fac 函数类型,而 g++ 认为 fac 函数是什么类型呢?

【问题讨论】:

  • 当您将 auto 替换为某种类型时,例如int 编译器应该告诉你它不能推断类型并给它们命名。但我没有测试过
  • fac 这是一个通用的 lambda,它的作用就像一个带有模板化 operator() 的函子。请注意,这些对于 c++14 来说是新的。
  • 附带说明,编写 Haskell 版本的更好方法是将其拆分为递归位(即一般情况下,fix :: (a -&gt; a) -&gt; afix f = f (fix f))和非递归位。非递归位是fact1 recur n = if n == 0 then 1 else n * recur (n-1),您会注意到(a)不是递归的,(b)具有有限且可推断的类型fact1 :: (Int -&gt; Int) -&gt; (Int -&gt; Int),并且(c)实现了一个阶乘的单个“步骤”。然后我们通过组合递归和非递归位来“完成”它:fact = fix fact1.
  • 好吧,我不会称 fix 为 Y 组合子。它具有相同的效果,但有许多定点组合器,其中Yfix 只是两个示例。
  • 哦,是的,我现在记起来了:Data.Function 中的定义使fix (1:) 成为循环链表而不是无限链表,对于其他一些类似的事情也是如此。它可以让您将数据结构打结成结,而更明显的fix f = f (fix f) 则不会。

标签: c++ haskell types c++14 generic-lambda


【解决方案1】:

C++ fac 并不是真正的函数,而是具有成员函数的结构体。

struct aaaa // Not its real name.
{
    template<typename a, typename b>
    auto operator()(a self, b n) const
    { 
    }
};

重载的调用运算符隐藏了 C++ 为实现“lambda 函数”而执行的一些技巧

当你“呼叫”fac 时,会发生什么

fac.operator() (fac, 3);

所以函数的参数不是函数本身,而是一个将它作为成员的对象。
这样做的一个影响是函数的类型(即operator() 的类型)不会出现在operator() 函数本身的类型中。
self的类型是定义函数的结构体。)

模板部分不是这个工作所必需的;这是fac“函数”的非通用版本:

struct F
{
    int operator()(const F& self, int n) const
    { 
        // ...
    }
};

F fac;
fac(fac, 3);

如果我们保留模板并将operator()重命名为applY

// The Y type
template<typename a>
struct Y
{
    // The wrapped function has type (Y<a>, a) -> a
    a applY(const Y<a>& self, a n) const
    { 
        if(n < 1)
            return 1;
        else 
            return n * self.applY(self, n-1);
    }
};

template<typename a>
a fac(a n)
{
    Y<a> y;
    return y.applY(y, n);
}

我们看到您的 Haskell 程序和您的 C++ 程序非常相似 - 主要区别在于标点符号。

相比之下,在 Haskell 中

fac2 self 0 = 1
fac2 self n = n * (self self $ n-1)

self 一个函数,fac2 的类型必须是

X -> Int -> Int

对于一些X
由于self是一个函数,而self self $ n-1是一个Int,所以self的类型也是X -&gt; Int -&gt; Int

但是X 会是什么呢?
它必须与self本身的类型相同,即X -&gt; Int -&gt; Int
但这意味着self的类型是(代替X):

(X -> Int -> Int) -> Int -> Int  

所以X 类型也必须是

(X -> Int -> Int) -> Int -> Int  

所以self的类型必须是

((X -> Int -> Int) -> Int -> Int) -> Int -> Int

等等,无限循环。
也就是说,在 Haskell 中,类型是无限的。

您的 Haskell 解决方案本质上明确地引入了 C++ 通过其具有成员函数的结构生成的必要间接。

【讨论】:

    【解决方案2】:

    正如其他人指出的那样,lambda 充当涉及模板的结构。那么问题就变成了:为什么 Haskell 不能键入自应用程序,而 C++ 可以?

    答案在于 C++ 模板和 Haskell 多态函数之间的区别。比较这些:

    -- valid Haskell
    foo :: forall a b. a -> b -> a
    foo x y = x
    
    // valid C++
    template <typename a, typename b>
    a foo(a x, b y) { return x; }
    

    虽然它们看起来几乎相同,但实际上并非如此。

    当 Haskell 类型检查上述声明时,它会检查该定义对于任何类型 a,b 是类型安全的。也就是说,如果我们将a,b 替换为任意两种类型,则函数必须是良好定义的。

    C++ 遵循另一种方法。在模板定义中,不会检查a,b 的任何替换是否正确。此检查延迟到模板的使用点,即在实例化时。为了强调这一点,让我们在代码中添加一个+1

    -- INVALID Haskell
    foo :: forall a b. a -> b -> a
    foo x y = x+1
    
    // valid C++
    template <typename a, typename b>
    a foo(a x, b y) { return x+1; }
    

    Haskell 定义不会进行类型检查:当x 是任意类型时,不能保证您可以执行x+1。相反,C++ 代码很好。 a 的某些替换导致错误代码的事实现在无关紧要。

    推迟这个检查会导致一些“无限类型的值”被允许,粗略地说。 Python 或 Scheme 等动态语言进一步将这些类型错误推迟到运行时,当然也可以很好地处理自应用程序。

    【讨论】:

      【解决方案3】:

      auto fac = 后面的表达式是一个 lambda 表达式,编译器会自动生成一个闭包对象。该对象的类型是唯一的,只有编译器知道。

      从 N4296,§5.1.2/3 [expr.prim.lambda]

      lambda-expression 的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型——称为 闭包类型 — 其属性如下所述。此类类型既不是聚合(8.5.1)也不是文字类型(3.9)。闭包类型在包含相应 lambda-expression 的最小块作用域、类作用域或命名空间作用域中声明。

      请注意,正因为如此,即使是两个相同的 lambda 表达式也会有不同的类型。例如,

      auto l1 = []{};
      auto l2 = []{}; // l1 and l2 are of different types
      

      您的 lambda 表达式是 C++14 通用 lambda,编译器会将其转换为类似于以下内容的类:

      struct __unique_name
      {
          template<typename Arg1, typename Arg2>
          auto operator()(Arg1 self, Arg2 n) const
          { 
              // body of your lambda
          }
      };
      

      我无法评论 Haskell 部分,但递归表达式在 C++ 中起作用的原因是因为您只是在每次调用中传递了闭包对象实例 (fac) 的副本。 operator() 作为模板能够推断出 lambda 的类型,即使它不是你可以用其他方式命名的类型。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2015-04-21
        • 1970-01-01
        • 2018-09-21
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-01-29
        • 2016-09-19
        相关资源
        最近更新 更多