您的位置:首页 > 编程语言 > C语言/C++

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

2017-03-15 16:39 495 查看

前言

本文将介绍如何使用C++11模板元编程实现Scheme中的list及相关函数式编程接口,如
list
cons
car
cdr
length
is_empty
reverse
append
,
map
transform
enumerate
lambda
等。

预备知识

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)


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

(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
即相关操作,我们需要先定义一些模板数据结构。这些数据结构非常简单,即重新定义基本数据类型,为了简化,在这里我只特化了必须的
int
uint
bool
以及
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


同样,我们可以实现用于获取
head
car
与获取
tail
cdr
操作:

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
,
map
apply
,
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
特化处理空列表的情况;

如果
list1
list2
均非空,那么通过
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等新特性使得这把实用的瑞士军刀越发锋利与实用,虽然实现的形式上不如
Scheme
Python
等优雅,但它确实能够,而且无需获得语言层面上的支持。纸上得来终觉浅,绝知此事要躬行。看过本文的读者不妨自己实现一番本文中的提到的相关概念。

参考阅读

《计算机程序的构造与解释》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息