基于C++11模板元编程实现Scheme中的list及相关函数式编程接口

Posted 飘飘白云

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于C++11模板元编程实现Scheme中的list及相关函数式编程接口相关的知识,希望对你有一定的参考价值。

前言

本文将介绍如何使用C++11模板元编程实现Scheme中的list及相关函数式编程接口,如listconscarcdrlengthis_emptyreverseappend,maptransformenumeratelambda等。

预备知识

Scheme简介

Scheme语言是lisp>语言的一个方言(或说成变种),它诞生于1975年的MIT,对于这个有近三十年历史的编程语言来说,它并没有象 C++,java,C#那样受到商业领域的青睐,在国内更是鲜为人知。但它在国外的计算机教育领域内却是有着广泛应用的,有很多人学的第一门计算机语言就 是Scheme语言(SICP就是以Scheme为教学语言)。

它是一个小巧而又强大的语言,作为一个多用途的编程语言,它可以作为脚本语言使用,也可以作为应用软件的扩展语言来使用,它具有元语言特性,还有很多独到的特色,以致于它被称为编程语言中的”皇后”。

如果你对Scheme感兴趣,推荐使用drracket这个GUI解释器,入门教程有:How to Design Programs,高级教程有:SICP

Scheme中的list及相关操作

list可以说是Lisp系语言的根基,其名就得自于**LIS**t **P**rocessor,其重要性就像文件概念之于unix。

list示例:

> (list "red" "green" "blue")
'("red" "green" "blue")
> (list 1 2 3)
'(1 2 3)

上面的语法糖list其实是通过递归调用点对cons实现的,因此上面的语法等价于:

> (cons "red" (cons "green" (cons "blue" empty)))
'("red" "green" "blue")
> (cons 1 (cons 2 (cons 3 empty)))
'(1 2 3)

另外两个重要的点对操作是carcdr,名字有点奇怪但是是有历史的:**C**urrent **A**ddress **R**egister and **C**urrent **D**ecrement **R**egister,其实就相当于firstsecond的意思。

(car (cons 1 2))  ==> 1
(cdr (cons 1 2))  ==> 2

模板元编程

C++中的Meta Programming,即模板元编程,是图灵完备的,而且是编译期间完成的。模板元编程通常用于编写工具库,如STL、Boost等。

比如通常我们使用递归来实现实现阶乘:

#include <iostream>

unsigned int factorial(unsigned int n) 
    return n == 0 ? 1 : n * factorial(n - 1);


int main() 
    std::cout << factorial(5) << std::endl;
    return 0;

我们也可以通过模板元编程来实现:

#include <iostream>

template <unsigned int n>
struct factorial 
    static constexpr unsigned int value = n * factorial<n - 1>::value;
;

template <>
struct factorial<0> 
    static constexpr unsigned int value = 1;
;

int main() 
    std::cout << factorial<5>::value << std::endl; // 120
    return 0;

实现

本文完整代码可以在这里查看:点击查看代码

基本数据结构

为了用模板元编程来模拟Scheme中list即相关操作,我们需要先定义一些模板数据结构。这些数据结构非常简单,即重新定义基本数据类型,为了简化,在这里我只特化了必须的intuintbool以及empty的实现。empty是递归实现list的最后一个元素,其作用相当于'\\0'之于字符串。

// type_
//
template <typename T, T N>
struct type_ 
    using type = type_<T, N>;
    using value_type = T;
    static constexpr T value = N;
;

// int_
//
template <int N>
struct int_ 
    using type = int_<N>;
    using value_type = int;
    static constexpr int value = N;
;

// uint_
//
template <unsigned int N>
struct uint_ 
    using type = uint_<N>;
    using value_type = unsigned int;
    static constexpr unsigned int value = N;
;

template <>
struct uint_<0> 
    using type = uint_<0>;
    using value_type = unsigned int;
    static constexpr unsigned int value = 0;
;

// bool_
template <bool N>
struct bool_ 
    using type = bool_<N>;
    using value_type = bool;
    static constexpr bool value = N;
;

// empty
//
struct empty 
    using type = empty;
    using value = empty;
;

下面我们先来个小示例,看看怎么使用这些模板数据结构。这个示例的作用是将仅仅用0和1表示的十进制数字当成二进制看,转换为十进制数值。如:101 转换为十进制数值为 5.

template <unsigned int N>
struct binary : uint_ < binary < N / 10 >::type::value * 2 + (N % 10) > ;

template <>
struct binary<0> : uint_<0> ;

测试示例:

std::cout << binary<101>::value << std::endl;               // 5

cons & car & cdr实现

cons的实现原理很简单:就是能够递归调用自己结合成点对pair。在Scheme中示例如下:

(cons 1 (cons 2 (cons 3 '())))

其中'()表示空的点对pair,在我们的实现里面就是empty

因此cons用C++元编程实现就是:

template <typename h, typename t>
struct cons 
    using type = cons<h, t>;
    using head = h;
    using tail = t;
;

使用示例:

std::cout << cons<int_<1>, int_<2>>::head::value << std::endl;  // 1

同样,我们可以实现用于获取headcar与获取tailcdr操作:

struct car_t 
    template <typename cons>
    struct apply 
        using type = typename cons::type::head;
    ;
;

template <typename cons>
struct car : car_t::template apply<cons>::type ;

struct cdr_t 
    template <typename cons>
    struct apply 
        using type = typename cons::type::tail::type;
    ;
;

template <typename cons>
struct cdr : cdr_t::template apply<cons>::type ;

使用示例:

    using c1 = cons<int_<1>, cons<int_<2>, int_<3>>>;
    std::cout << car<c1>::value << ", " << cdr<c1>::head::value << std::endl; // 1, 2
    std::cout << car<c1>::value << ", " << car<cdr<c1>>::value << std::endl; // 1, 2

对于上面的实现,稍微解释一下:car是对car_t的封装,这样使用起来更为方便,对比如下用法就能明了,后面这样的封装手法还会用到:

car<cons<int_<1>, int_<2>>::value                   // == 1
car_f::template apply<cons<int_<1>, int_<2>>::value // == 1

list的实现

list其实一种特殊的cons,其实现如下:

template <typename first = empty, typename ...rest>
struct list_t : std::conditional <
    sizeof...(rest) == 0,
    cons<first, empty>,
    cons<first, typename list_t<rest...>::type>>::type
;

template <>
struct list_t<empty> : empty ;

template <typename T, T ...elements>
struct list : list_t<type_<T, elements>...> ;

这里用到了C++11中的变长模板参数,std::conditional以及对empty的特化处理。
- 变长模板参数:list接收变长模板参数elements,然后封装类型为成type_的变长模板参数forward给list_t
- std::conditional:相当于if ... else ...,如果第一参数为真,则返回第二参数,否则返回第三参数;
- <第一参数中的code>sizeof…(rest):sizeof是C++11的新用法,用于获取变长参数的个数;
- 第二参数的作用是终止递归;
- 第三参数的作用是递归调用list_t构造点对。
- 因为empty比较特殊,所以需要特化处理

使用示例:

    using l1 = list<int, 1, 2, 3>;
    using l2 = list<int, 4, 5, 6, 7>;
    using l3 = list_t<int_<1>, int_<2>, int_<3>>;

    std::cout << "\\n>list" << std::endl;
    print<l1>();    // 1, 2, 3
    print<l3>();    // 1, 2, 3
    std::cout << car<l1>::value << ", " << cdr<l1>::head::value << std::endl;   // 1, 2

length & is_empty的实现

先来看看length的实现,其思路与list的实现一样:递归调用自身,并针对empty特化处理。

template <typename list>
struct length_t

    static constexpr unsigned int value =
        1 + length_t<typename cdr<list>::type>::value;
;

template <>
struct length_t<empty>

    static constexpr unsigned int value = 0;
;

template <typename list>
struct length

    static constexpr unsigned int value = length_t<typename list::type>::value;
;

is_empty可以简单实现为判断length为0:

template <typename list>
struct is_empty

    static constexpr bool value = (0 == length<list>::value);
;

当然这样的实现效率并不高,因此可以通过对list以及empty的特化处理来高效实现:

template <typename list>
struct is_empty_t

    static constexpr bool value = false;
;

template <>
struct is_empty_t<empty>

    static constexpr bool value = true;
;

template <typename list>
struct is_empty

    static constexpr bool value = is_empty_t<typename list::type>::value;
;

使用示例:

    std::cout << "is_empty<empty> : " << is_empty<empty>::value << std::endl;            // 1
    std::cout << "is_empty<list<int>> : " << is_empty<list<int>>::value << std::endl;    // 1
    std::cout << "is_empty<list<int, 1, 2, 3>> : " << is_empty<l1>::value << std::endl;  // 0
    std::cout << "length<empty> : " << length<empty>::value << std::endl;                // 0
    std::cout << "length<list<int>> : " << length<list<int>>::value << std::endl;        // 0
    std::cout << "length<list<int, 1, 2, 3>> : " << length<l1>::value << std::endl;      // 3

append & reverse的实现

append是将一个列表list2追加到已有列表list1的后面,其实现思路是递归地将car当做head,然后将cdr作为新的list1递归调用append。不要忘记特化empty的情况。

struct append_t 
    template <typename list1, typename list2>
    struct apply : cons<
        typename car<list1>::type,
        typename append_t::template apply<typename cdr<list1>::type, list2>::type>
    ;

    template<typename list2>
    struct apply <empty, list2>: list2
    ;
;

template <typename list1, typename list2>
struct append : std::conditional <
    is_empty<list1>::value,
    list2,
    append_t::template apply<list1, list2>
    >::type
;

reverse的实现思路与append类似,只不过是要逆序罢了:

struct reverse_t 
    template <typename reset, typename ready>
    struct apply : reverse_t::template apply<
            typename cdr<reset>::type,
            cons<typename car<reset>::type, ready>>
    ;

    template<typename ready>
    struct apply <empty, ready> : ready
    ;
;

template <typename list>
struct reverse : std::conditional <
    is_empty<list>::value,
    list,
    reverse_t::template apply<typename list::type, empty>
    >::type
;

使用示例:

    // reverse
    using r1 = reverse<l1>;
    using r2 = reverse<list<int>>;
    print<r1>();    // 3, 2, 1
    print<r2>();

    // append
    using a1 = append<l1, l2>;
    using a2 = append<l1, list<int>>;
    using a3 = append<list<int>, l1>;
    print<a1>();    // 1, 2, 3, 4, 5, 6, 7
    print<a2>();    // 1, 2, 3
    print<a3>();    // 1, 2, 3

函数式编程

lisp系语言的最大特性就是支持函数式编程,它能够把无差别地对待数据与函数,实现了对数据与代码的同等抽象。下面我们来添加对函数式编程的支持:enumerate, mapapply, lambda 以及 transform

map的实现

map的语义是迭代地将某个方法作用于列表中的每个元素,然后得到结果list。先来定义一些辅助的方法:

template <typename T, typename N>
struct plus : int_ < T::value + N::value > ;

template <typename T, typename N>
struct minus : int_ < T::value - N::value > ;

struct inc_t 
    template <typename n>
    struct apply : int_ < n::value + 1 > ;
;

template <typename n>
struct inc : int_ < n::value + 1 > ;

下面来看map的实现

struct map_t 
    template <typename fn, typename list>
    struct apply : cons <
        typename fn::template apply<typename car<list>::type>,
        map_t::template apply<fn, typename cdr<list>::type>
    >;

    template <typename fn>
    struct apply <fn, empty>: empty;
;

template <typename fn, typename list>
struct map : std::conditional <
    is_empty<list>::value,
    list,
    map_t::template apply<fn, list>
    >::type
;

使用示例:

    using m1 = map<inc_t, list<int, 1, 2, 3>>;
    using m2 = map<inc_t, list<int>>;
    print<m1>();    // 2, 3, 4
    print<m2>();

为了让map支持形如inc这样的模板类,而不仅仅是形如inc_t,我们需要定义一个转换器:lambda

struct apply_t 
    template <template <typename...> class F, typename ...args>
    struct apply : F<typename args::type...> ;

    template <template <typename...> class F>
    struct apply <F, empty> : empty ;
;

template <template <typename...> class F, typename ...args>
struct apply : apply_t::template apply<F, args...> ;

template <template <typename...> class F>
struct lambda 
    template <typename ...args>
    struct apply : apply_t::template apply<F, args...> ;
;

使用示例:

    std::cout << lambda<inc>::template apply<int_<0>>::value << std::endl;  // 1
    using ml1 = map<lambda<inc>, list<int, 1, 2, 3>>;
    print<ml1>();  // 2, 3, 4

transform的实现

transform的语义是对迭代地将某个方法作用于两个列表上的元素,然后得到结果list

struct transform_t 
    template <typename list1, typename list2, typename fn>
    struct apply : cons <
        typename fn::template apply<
            typename car<list1>::type, typename car<list2>::type>::type,
        typename transform_t::template apply <
            typename cdr<list1>::type, typename cdr<list2>::type, fn>::type
    > ;

    template <typename list1, typename fn>
    struct apply<list1, empty, fn> : cons <
        typename fn::template apply<typename car<list1>::type, empty>,
        typename transform_t::template apply <typename cdr<list1>::type, empty, fn>::type
    > ;

    template <typename list2, typename fn>
    struct apply<empty, list2, fn> : cons <
        typename fn::template apply<empty, typename car<list2>::type>::type,
        typename transform_t::template apply <empty, typename cdr<list2>::type, fn>::type
    > ;

    template <typename fn>
    struct apply<empty, empty, fn> : empty ;
;

template <typename list1, typename list2, typename fn>
struct transform : std::conditional <
    is_empty<list1>::value,
    list1,
    transform_t::template apply<list1, list2, fn>
    >::type

    static_assert(length<list1>::value == length<list2>::value, "transform: length of lists mismatch!");
;

其实现是最为复杂的,我们先来看使用示例,再来讲解实现细节。

    using t1 = list<int, 1, 2, 3>;
    using t2 = list<int, 3, 2, 1>;
    using ml = transform<t1, t2, lambda<minus>>;
    using pl = transform<t1, t2, lambda<plus>>;
    using te = transform<list<int>, list<int>, lambda<plus>>;
    using el = transform<t1, list<int>, lambda<plus>>;
    print<ml>();    // -2, 0, 2
    print<pl>();    // 4, 4, 4
    print<te>();
    // print<el>(); // assertion: length mismatch!

实现细节:

  • 使用C++11新特性static_assert对两个列表的长度相等做断言;
  • 使用std::conditional处理空列表,如果非空forward给transform_t
  • transform_t特化处理空列表的情况;
  • 如果list1list2均非空,那么通过car取出两个列表的head作用于方法,然后递归调用transform_t作用于两个列表的tail

enumerate的实现

enumerate的语义是迭代将某个方法作用于列表元素。

template <typename fn, typename list, bool is_empty>
struct enumerate_t;

template <typename fn, typename list>
void enumerate(fn f)

    enumerate_t<fn, list, is_empty<list>::value> impl;
    impl(f);


template <typename fn, typename list, bool is_empty = false>
struct enumerate_t

    void operator()(fn f) 
        f(car<list>::value);
        enumerate<fn, typename cdr<list>::type>(f);
    
;

template <typename fn, typename list>
struct enumerate_t<fn, list, true>

    void operator()(fn f) 
        // nothing for empty
    
;

enumerate的实现与之前的map的实现很不一样,它是通过模板方法与函数子来实现的。模板方法内部调用函数子来做事情,函数子又是一个模板类,并对空列表做了特化处理。

使用示例:

    using value_type = typename car<e1>::value_type;
    auto sqr_print = [](value_type val)  std::cout << val * val << " "; ;
    enumerate<decltype(sqr_print), e1>(sqr_print);      // 1 4 9

equal的实现

equal用于判断两个列表是否等价。

struct equal_t 
    // both lists are not empty
    template <typename list1, typename list2, int empty_value = 0,
        typename pred = lambda<std::is_same>>
    struct apply : std::conditional <
        !pred::template apply <
            typename car<list1>::type,
            typename car<list2>::type
            >::type::value,
        bool_<false>,
        typename equal_t::template apply <
            typename cdr<list1>::type,
            typename cdr<list2>::type,
            (is_empty<typename cdr<list1>::type>::value
                + is_empty<typename cdr<list2>::type>::value),
            pred
        >::type
    > ;

    // one of the list is empty.
    template <typename list1, typename list2, typename pred>
    struct apply<list1, list2, 1, pred>: bool_<false>
    ;

    // both lists are empty.
    template <typename list1, typename list2, typename pred>
    struct apply<list1, list2, 2, pred>: bool_<true>
    ;
;

template <typename list1, typename list2, typename pred = lambda<std::is_same>>
struct equal : equal_t::template apply<list1, list2,
    (is_empty<list1>::value + is_empty<list2>::value), pred>::type
;

equal的实现也有点复杂。

  • pred是等价比较谓词,默认是使用std::is_same来做比较;
  • 关键部分依然是通过std::conditional来实现的;
  • 第一参数是判断两个列表的head是否相等;
  • 如果不等就返回第二参数;
  • 如果相等就递归比较两个列表的剩余元素;
  • 这里使用了一个小小的技巧来简化模板类特化的情况:如果其中一个列表为空,那么empty_value为1;如果两个列表均为空,那么empty_value为2,这两种情况都会调用特化版本。

使用示例:

    using e1 = list<int, 1, 2, 3>;
    using e2 = list<int, 1, 2, 3>;
    using e3 = list<int, 1, 2, 1>;
    std::cout << "equal<e1, e2> : " << equal<e1, e2>::value << std::endl;   // 1
    std::cout << "equal<e1, e3> : " << equal<e1, e3>::value << std::endl;   // 0
    std::cout << "equal<e1, list<int>> : " << equal<e1, list<int>>::value << std::endl; // 0
    std::cout << "equal<list<int>, e1> : " << equal<list<int>, e1>::value << std::endl; // 0
    std::cout << "equal<list<int>, list<int>> : " << equal<list<int>, list<int>>::value << std::endl;   // 1

print的实现

print是依次打印列表元素,也可以使用enumerate来实现:

template <typename list, bool is_empty>
struct print_t;

template <typename list>
void print()

    print_t<list, is_empty<list>::value> impl;
    impl();


template <typename list, bool is_empty = true>
struct print_t

    void operator()() 
        std::cout << std::endl;
    
;

template <typename list>
struct print_t<list, false>

    void operator()() 
        std::cout << car<list>::value;
        using rest = typename cdr<list>::type;
        if (false == is_empty<rest>::value) 
            std::cout << ", ";
        
        print<rest>();
    
;

print的实现思路与enumerate,是通过模板方法与函数子来实现的。模板方法内部调用函数子来做事情,函数子又是一个模板类,并对空列表做了特化处理。

总结

C++尤其是C++11,14,17等新特性使得这把实用的瑞士军刀越发锋利与实用,虽然实现的形式上不如SchemePython等优雅,但它确实能够,而且无需获得语言层面上的支持。纸上得来终觉浅,绝知此事要躬行。看过本文的读者不妨自己实现一番本文中的提到的相关概念。

参考阅读

《计算机程序的构造与解释》

以上是关于基于C++11模板元编程实现Scheme中的list及相关函数式编程接口的主要内容,如果未能解决你的问题,请参考以下文章

模版元编程:C++11中type traits的部分实现

C++模板元编程深度解析:探索编译时计算的神奇之旅

什么样的C++模板编程可以称为“元编程”? [关闭]

Qt笔记——元对象系统

Item 48:了解模板元编程

Item 48:了解模板元编程