c++ - 就功能编程而言,C++可以提供什么?

标签 c++ functional-programming

在C++中,以下事情对于FP而言是固有的吗?

  • 高阶函数
  • lambdas(关闭/匿名函数)
  • 函数签名,类型为
  • 类型多态(泛型)
  • 不可变数据结构
  • 代数数据类型(变体)
  • adhock数据结构(元组)
  • 部分功能应用程序
  • 类型推断
  • 尾递归
  • 模式匹配
  • 垃圾回收
  • 最佳答案

    让我首先指出,这些都不是“内在的”,或者我们说“是必需的”。其中的许多功能是著名的功能语言所缺少的,从理论上讲,这些功能中的许多功能都可以用于实现其他功能(例如,无类型lambda演算中的高阶功能)。

    但是,让我们看一下这些:

    闭包

    闭包不是必需的,并且是语法上的糖:通过Lambda Lifting的过程,您可以将任何闭包转换为函数对象(甚至只是自由函数)。

    命名函子(C++ 03)

    只是为了证明这不是问题,这是在C++ 03中没有lambda的简单方法:

    没问题:

    struct named_functor 
    {
        void operator()( int val ) { std::cout << val; }
    };
    vector<int> v;
    for_each( v.begin(), v.end(), named_functor());
    

    匿名函数(C++ 11)

    但是,C++ 11中的匿名函数(也称为lambda函数,它们是从LISP历史中派生的)(以非别名命名的函数对象实现)可以提供相同的可用性(实际上称为闭包,所以是的,C++ 11确实有闭包):

    没问题:
    vector<int> v;
    for_each( v.begin(), v.end(), [] (int val)
    {
        std::cout << val;
    } );
    

    多态匿名函数(C++ 14)

    甚至没有什么问题,我们不再需要关心C++ 14中的参数类型:

    问题更少:
    auto lammy = [] (auto val) { std::cout << val; };
    
    vector<int> v;
    for_each( v.begin(), v.end(), lammy);
    
    forward_list<double> w;
    for_each( w.begin(), w.end(), lammy);
    

    我应该注意到,它完全支持闭包语义,例如通过引用和按值从作用域中获取变量,以及能够获取所有变量,而不仅仅是指定变量。 Lambda被隐式地定义为函数对象,为它们工作提供了必要的上下文。通常这是通过lambda提升完成的。

    高阶函数
    没问题:
    std::function foo_returns_fun( void );
    

    这还不够吗?这是lambda工厂:
    std::function foo_lambda( int foo ) { [=] () { std::cout << foo; } };
    

    您不能创建函数,但是可以创建函数对象,这些对象可以作为std::function与普通函数一样传递。所以所有功能都在那里,这取决于您如何组合在一起。我可能会补充说,许多STL都是围绕给您可重用的组件而设计的,这些组件可用来形成即席函数对象,从而近似地用整个功能来创建函数。

    部分函数应用程序
    没问题

    std::bind完全支持此功能,并且非常擅长将函数转换为任意不同的函数:
    void f(int n1, int n2, int n3, const int& n4, int n5)
    {
        std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << '\n';
    }
    
    int n = 7;
    // (_1 and _2 are from std::placeholders, and represent future
    // arguments that will be passed to f1)
    auto f1 = std::bind(f, _2, _1, 42, std::cref(n), n);
    

    对于备注和其他局部函数专门化技术,您必须使用包装器自己对其进行编码:
    template <typename ReturnType, typename... Args>
    std::function<ReturnType (Args...)>
    memoize(ReturnType (*func) (Args...))
    {
        auto cache = std::make_shared<std::map<std::tuple<Args...>, ReturnType>>();
        return ([=](Args... args) mutable  
        {
            std::tuple<Args...> t(args...);
            if (cache->find(t) == cache->end())
                (*cache)[t] = func(args...);
    
            return (*cache)[t];
        });
    }
    

    它可以完成,并且实际上可以相对自动地完成,但是还没有人为您完成。
    }

    组合器
    没问题:

    让我们从经典开始: map ,过滤器,折叠。
    vector<int> startvec(100,5);
    vector<int> endvec(100,1);
    
    // map startvec through negate
    std::transform(startvec.begin(), startvec.end(), endvec.begin(), std::negate<int>())
    
    // fold startvec through add
    int sum =  std::accumulate(startvec.begin(), startvec.end(), 0, std::plus<int>());
    
    // fold startvec through a filter to remove 0's
    std::copy_if (startvec.begin(), startvec.end(), endvec.begin(), [](int i){return !(i==0);} );
    

    这些很简单,但是头<functional><algorithm><numerical>提供了数十个函子(可调用为函数的对象),这些函子可放入这些通用算法以及其他通用算法中。它们共同构成了组合功能和行为的强大功能。

    但是,让我们尝试一些更实用的功能:SKI可以很容易地实现,并且功能非常强大,源于未类型化的lambda演算:
    template < typename T >
    T I(T arg)
    {
        return arg;
    }
    
    template < typename T >
    std::function<T(void*)> K(T arg)
    {
    return [=](void*) -> T { return arg; };
    }
    
    template < typename T >
    T S(T arg1, T arg2, T arg3)
    {
    return arg1(arg3)(arg2(arg1));
    }
    

    这些非常脆弱;实际上,这些类型必须是返回其自身类型并接受其自身类型的单个参数的类型;这样的约束将允许将SKI系统的所有功能推理安全地应用于这些组成。只需做一些工作,再进行一些模板元编程,甚至可以在编译时通过表达式模板的神奇力量来完成大部分工作,从而形成高度优化的代码。

    顺便说一句,表达式模板是一种技术,其中通常以一系列操作或代码的顺序形式的表达式作为模板的参数。因此,表达模板是编译时间组合器。它们非常高效,类型安全,并且有效地允许将特定于 Realm 的语言直接嵌入到C++中。虽然这些是高级主题,但它们在标准库和boost::spirit中得到了很好的利用,如下所示。

    精神分析器组合器
    template <typename Iterator>
    bool parse_numbers(Iterator first, Iterator last)
    {
        using qi::double_;
        using qi::phrase_parse;
        using ascii::space;
    
        bool r = phrase_parse(
        first,                          
        last,                           
        double_ >> (char_(',') >> double_),   
        space                           
        );
    
        if (first != last) // fail if we did not get a full match
            return false;
        return r;
    }
    

    这标识逗号分隔的数字列表。 double_和char_是单独的解析器,分别标识单个double或单个char。使用>>运算符,每个运算符将自身传递到下一个运算符,从而形成单个大型组合解析器。他们通过模板传递自己,从而形成了联合行动的“表达”。这与传统的组合器完全类似,并且经过了全面的编译时检查。

    Valarray

    valarray是C++ 11标准的一部分,允许使用表达模板(但出于某些奇怪的原因不是必需的),以提高转换效率。从理论上讲,任何数量的操作都可以串在一起,这将形成一个很大的困惑表达,然后可以积极地内联速度。这是组合器的另一种形式。

    如果您想进一步了解表达模板,建议使用this resource。它们绝对可以完成您希望完成的所有编译时间检查,并提高代码的可重用性。但是,它们很难编程,这就是为什么我建议您找到一个包含所需成语而不是自己编写成语的库的原因。

    函数签名作为类型
    没问题
    void my_int_func(int x)
    {
        printf( "%d\n", x );
    }
    
    void (*foo)(int) = &my_int_func;
    

    或者,在C++中,我们将使用std::function:
    std::function<void(int)> func_ptr = &my_int_func;
    

    类型推断
    没问题

    通过推论输入的简单变量:
    // var is int, inferred via constant
    auto var = 10;
    
    // y is int, inferred via var
    decltype(var) y = var;
    

    模板中的通用类型推断:
    template < typename T, typename S >
    auto multiply (const T, const S) -> decltype( T * S )
    {
        return T * S;
    }
    

    此外,这可以在lambda,函数对象中使用,基本上任何编译时表达式都可以使用decltype进行编译时类型推断。

    但这不是您真正想要的,不是吗?您需要类型推导和类型限制,还需要类型重构和类型推导。所有这些都可以通过概念来完成,但是它们还不是语言的一部分。

    那么,为什么我们不只是实现它们呢? boost::conceptsboost::typeerasuretype traits(boost::tti和boost::typetraits的后裔)可以完成所有这些工作。

    是否要限制基于某种类型的功能? std::enable_if来营救!

    啊,那是临时的,对吗?这意味着对于您要构造的任何新类型,您都需要做样板等。好吧,不,但是这是一种更好的方法!
    template<typename RanIter>
    BOOST_CONCEPT_REQUIRES(
        ((Mutable_RandomAccessIterator<RanIter>))
        ((LessThanComparable<typename Mutable_RandomAccessIterator<RanIter>::value_type>)),
        (void)) // return type
    stable_sort(RanIter,RanIter);
    

    现在,您的stable_sort只能在符合严格要求的类型上使用。 boost::concept有很多预制的,您只需要将它们放在正确的位置即可。

    如果您想调用不同的函数或对类型执行不同的操作,或者禁止类型,请使用类型特征,它现在是标准功能。是否需要根据部分类型而不是全部类型进行选择?还是允许具有共同接口(interface)的许多不同类型只是具有相同接口(interface)的单一类型?那么,您需要擦除类型,如下所示:

    类型多态性
    没问题

    用于编译时类型多态性的模板:
    std::vector<int> intvector;
    std::vector<float> floatvector;
    ...
    

    类型擦除,用于运行时和基于适配器的类型多态性:
    boost::any can_contain_any_type;
    std::function can_call_any_function;
    any_iterator can_iterator_any_container;
    ...
    

    在任何OO语言中,类型擦除都是可能的,并且涉及设置从通用接口(interface)派生的小型函数对象,并将内部对象转换为该对象。只需一点点升压MPL样板,即可快速,轻松且有效。希望很快看到它成为真正的流行。

    不可变数据结构
    不是显式构造的语法,但可能:

    可以通过不使用更改器(mutator)或模板元编程来完成。由于这是很多代码(完整的ADT可能非常大),因此我将链接here,以显示如何制作不可变的单链接列表。

    要在编译时执行此操作,需要大量模板魔术,但是使用constexpr可以更轻松地完成。这是读者的练习;我什至不知道有什么编译时间库。

    但是,从STL创建不可变的数据结构非常容易:
    const vector<int> myvector;
    

    你在这;无法更改的数据结构!认真地说,手指树实现do exist可能是您对关联数组功能的最佳选择。默认情况下,这只是为您完成的。

    代数数据类型
    没问题:

    令人惊叹的boost::mpl允许您限制类型的使用,它与boost::fusionboost::functional一起在编译时执行有关ADT的任何操作。实际上,大多数事情都是为您完成的:
    #include <boost/mpl/void.hpp>
    //A := 1
    typedef boost::mpl::void_ A;
    

    如前所述,许多工作不是一次完成的。例如,您需要使用boost::optional获取可选类型,并使用mpl获取单元类型,如上所示。但是,使用相对简单的编译时模板机制,您可以执行递归ADT类型,这意味着您可以实现通用ADT。模板系统即将完成时,您将可以使用图完成完整类型检查器和ADT生成器。

    它只是在等待您将各个部分放在一起。

    基于变体的ADT的

    boost::variant除了提供语言中的原始联合外,还提供了类型检查的联合。这些可以毫不费力地使用,请放入:
    boost::variant< int, std::string > v;
    

    此变体可以是int或字符串,可以通过检查的任何一种方式进行分配,您甚至可以执行基于运行时变体的访问:
    class times_two_visitor
        : public boost::static_visitor<>
    {
    public:
        void operator()(int & i) const
        {
            i *= 2;
        }
        void operator()(std::string & str) const
        {
            str += str;
        }
    };
    

    匿名/临时数据结构
    没问题:

    当然,我们有元组!如果愿意,可以使用结构,或者:
    std::tuple<int,char> foo (10,'x');
    

    您还可以对元组执行很多操作:
    // Make them
    auto mytuple = std::make_tuple(3.14,"pi");
    std::pair<int,char> mypair (10,'a');
    
    // Concatenate them
    auto mycat = std::tuple_cat ( mytuple, std::tuple<int,char>(mypair) );
    
    // Unpack them
    int a, b;
    std::tie (a, std::ignore, b, std::ignore) = mycat; 
    

    尾递归
    没有明确的支持,迭代就足够了

    尽管在Scheme中,它在Common LISP中不受支持或强制要求,所以我不知道您是否可以说它是必需的。但是,您可以轻松地在C++中执行尾递归:
    std::size_t get_a_zero(vector<int>& myints, std::size_t a ) {
       if ( myints.at(a) == 0 ) {
          return a;
       }
       if(a == 0) return myints.size() + 1;
    
       return f(myints, a - 1 );   // tail recursion
    }
    

    哦,GCC会将其编译为一个迭代循环,没有危害也没有犯规。虽然这种行为不是强制性的,但它是允许的,并且在我所知的至少一种情况下(也可能是Clang)可以做到。
    但是我们不需要尾递归:C++完全可以用于突变:
    std::size_t get_a_zero(vector<int>& myints, std::size_t a ) {
       for(std::size_t i = 0; i <= myints.size(); ++i){
           if(myints.at(i) == 0) return i;
        }
        return myints.size() + 1;
    }
    

    尾递归已优化到迭代中,因此您拥有的功能正好一样。
    此外,通过使用boost::coroutine,可以轻松地为用户定义的堆栈提供用法,并允许无限制的递归,从而不需要尾递归。该语言既不主动反对递归,也不反对尾随递归。它仅要求您自己提供安全。

    模式匹配
    没问题:

    这可以通过boost::variant轻松完成,如本文其他地方所述,通过访问者模式:
    class Match : public boost::static_visitor<> {
    public:
        Match();//I'm leaving this part out for brevity!
        void operator()(const int& _value) const {
           std::map<int,boost::function<void(void)>::const_iterator operand 
               = m_IntMatch.find(_value);
           if(operand != m_IntMatch.end()){
               (*operand)();
            }
            else{
                defaultCase();
            }
        }
    private:
        void defaultCause() const { std::cout << "Hey, what the..." << std::endl; }
        boost::unordered_map<int,boost::function<void(void)> > m_IntMatch;
    };
    

    这个来自this very charming website的示例演示了如何仅使用boost::variant获得所有Scala模式匹配的功能。还有更多样板,但是有了一个不错的模板和宏库,其中的大部分都将消失。

    In fact, here is a library that has done all that for you:
    #include <utility>
    #include "match.hpp"                // Support for Match statement
    
    typedef std::pair<double,double> loc;
    
    // An Algebraic Data Type implemented through inheritance
    struct Shape
    {
        virtual ~Shape() {}
    };
    
    struct Circle : Shape
    {
        Circle(const loc& c, const double& r) : center(c), radius(r) {}
        loc    center;
        double radius;
    };
    
    struct Square : Shape
    {
        Square(const loc& c, const double& s) : upper_left(c), side(s) {}
        loc    upper_left;
        double side;
    };
    
    struct Triangle : Shape
    {
        Triangle(const loc& a, const loc& b, const loc& c) : first(a), second(b), third(c) {}
        loc first;
        loc second;
        loc third;
    };
    
    loc point_within(const Shape* shape)
    {
        Match(shape)
        {
           Case(Circle)   return matched->center;
           Case(Square)   return matched->upper_left;
           Case(Triangle) return matched->first;
           Otherwise()    return loc(0,0);
        }
        EndMatch
    }
    
    int main()
    {
        point_within(new Triangle(loc(0,0),loc(1,0),loc(0,1)));
        point_within(new Square(loc(1,0),1));
        point_within(new Circle(loc(0,0),1));
    }
    

    this lovely stackoverflow answer提供
    如您所见,这不仅可能,而且还很漂亮。

    垃圾收集
    将来的标准,分配器,RAII和shared_ptr就足够了

    虽然C++没有GC,但有a proposal for one that was voted down in C++11, but may be included in C++1y。您可以使用多种用户定义的变量,但是C++不需要垃圾回收。

    C++有一个惯用语RAII来处理资源和内存。因此,C++不需要GC,因为它不会产生垃圾。默认情况下,所有内容都会立即按照正确的顺序进行清理。这确实引入了谁拥有什么的问题,但是在C++ 11中,这通过共享指针,弱指针和唯一指针在很大程度上得以解决:
    // One shared pointer to some shared resource
    std::shared_ptr<int> my_int (new int);
    
    // Now we both own it!
    std::shared_ptr<int> shared_int(my_int);
    
    // I can use this int, but I cannot prevent it's destruction
    std::weak_ptr<int> weak_int (shared_int);
    
    // Only I can ever own this int
    std::unique_ptr<int> unique_int (new int);
    

    这些使您能够提供更具确定性和用户控制的垃圾收集形式,而不会调用任何停止世界的行为。

    那对你来说还不容易?使用自定义分配器,例如boost::pool或自己滚动;使用基于池或竞技场的分配器来同时利用两个方面的优势相对容易:您可以随意随意分配,然后在完成后简单地删除池或竞技场。没有大惊小怪,没有头脑,也没有停止世界。

    但是,在现代C++ 11设计中,除非分配给* _ptr,否则您几乎永远不会使用new,因此无论如何都不需要GC。

    总结

    C++具有许多功能语言功能,并且您可以使用Haskell或Lisp的相同功能和表达能力来完成您列出的所有功能。但是,默认情况下,大多数这些功能不是内置的。随着引入lambda(填充STL的功能部分)以及将boost吸收到标准语言中,这种情况正在改变。

    并非所有这些习语都是最可口的,但是它们对我来说都不是特别繁重,或者对一些宏都无法修改,以使它们更容易被吞下。但是,任何说不可能的人都没有做过研究,在我看来,他们对实际C++编程的经验有限。

    关于c++ - 就功能编程而言,C++可以提供什么?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/21471836/

    相关文章:

    c++ - 如何在 C++ 头文件中初始化和返回数组?

    c++ - 'i' 的可能结果数

    c++ - 私有(private)成员黑客行为是否已定义?

    java - 为什么@FunctionalInterface没有在合格的JDK的所有接口(interface)上使用?

    java - 使用 jna 加载 lib.so

    .net - F# 的简单包装器,用于执行矩阵运算

    functional-programming - 如何从列表理解而不是嵌套列表中获得平坦的结果?

    Haskell - 在数据声明中指定种类

    c# - 在不使用接口(interface)的情况下访问 F# 记录基本属性

    c++ - 在命名空间的类中,我可以**仅**类命名空间退出吗?