Pipeable
缘起
Pipeable
或许是相当有争议的一种 C++ 编程方式。
在 Boost 中有 pipeable : pipable — Boost.HigherOrderFunctions 0.6 documentation - master 。它是 hof 库 的一个部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <boost/hof.hpp>
#include <cassert>
using namespace boost::hof;
struct sum
{
template<class T, class U>
T operator()(T x, U y) const
{
return x+y;
}
};
int main() {
assert(3 == (1 | pipable(sum())(2)));
assert(3 == pipable(sum())(1, 2));
}
HOF 库是 Higher-order functions for C++ 的意思,其作者 Paul Fultz II 也是名人。在他的博客中有两篇 Posts 和本文主题相关:
同样地,Boost 中也有 Extension methods 与之相对应。
基本机理
我们注意到,Pipeable 编程的起源来自于 OS Shell 中的管道操作,例如:
1
cat 1.txt | grep 'best of' | uniq -c | head -10
特别是当 C++ 的运算符 ‘ | ‘(逻辑或)也能被重载时,一切似乎就变得顺理成章了。 |
它的 C++ 关键思路是做 ‘ | ’ 操作符重载以及 ‘()’ 操作符重载: |
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class F>
struct pipe_closure : F
{
template<class... Xs>
pipe_closure(Xs&&... xs) : F(std::forward<Xs>(xs)...)
{ }
};
template<class T, class F>
decltype(auto) operator|(T&& x, const pipe_closure<F>& p)
{
return p(std::forward<T>(x));
}
于是用户类 F 就能够在 pipe_closure
1
2
3
4
5
6
7
8
struct add_one_f
{
template<class T>
auto operator()(T x) const // `T const &x` is better
{
return x + 1;
}
};
那么 pipeable 串联操作就像这样:
1
2
3
const pipe_closure<add_one_f> add_one = { };
int number_3 = 1 | add_one | add_one;
std::cout << number_3 << std::endl;
要注意 ‘()’ 操作符接受泛型类 T 为入参,这样就能够收到 pip_closure<F>::operator | 转发的 lhs 要件了,也即 “ |
” 操作符的左手操作数。这样一来,lhs 表达式的结果就被 pipe 到了 rhs 操作数到 operator ()<T>() 中,从而完成了这次管道操作。 |
略加改进的
显然这个思路还可以进一步美化。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
namespace hicc::pipeable {
template<class F>
struct pipeable {
private:
F f;
public:
pipeable(F &&f)
: f(std::forward<F>(f)) { }
template<class... Xs>
auto operator()(Xs &&...xs) -> decltype(std::bind(f, std::placeholders::_1, std::forward<Xs>(xs)...)) const {
return std::bind(f, std::placeholders::_1, std::forward<Xs>(xs)...);
}
};
template<class F>
pipeable<F> piped(F &&f) { return pipeable<F>{std::forward<F>(f)}; }
template<class T, class F>
auto operator|(T &&x, const F &f) -> decltype(f(std::forward<T>(x))) {
return f(std::forward<T>(x));
}
} // namespace hicc::pipeable
然后,能够有更雅致的呈现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void test_piped() {
using namespace hicc::pipeable;
{
auto add = piped([](int x, int y) { return x + y; });
auto mul = piped([](int x, int y) { return x * y; });
int y = 5 | add(2) | mul(5) | add(1);
hicc_print(" y = %d", y);
int y2 = 5 | add(2) | piped([](int x, int y) { return x * y; })(5) | piped([](int x) { return x + 1; })();
// Output: 36
hicc_print(" y2 = %d", y2);
}
}
上面的 pipeable class 以及 piped(…) 源于 Functional pipeline in C++11 - Victor Laskin’s Blog,他的实现更 meaningful,所以我们更喜欢一些。
ACK:下图来自他的博客
Paul Fultz II 的动机更优先,只是我从来不会真正使用 boost,它太庞大了,太完整了,根本不适合在工程中使用。我更喜欢相对轻量级的,但却又相对完整的。
如果你对 Paul Fultz II 的实现感兴趣,除了 boost::hof::pipeable 之外,他还制作了开源的 Linq: pfultz2/Linq: Linq for list comprehension in C++ 。这是 C# Linq 技术的 C++ 实现,很 amazing,也很疯狂。
后继
以下是延伸阅读时间。
ranges
当然,也需要提到的是 std::ranges 范围库,它是 C++20 带来的改变之一,不过目前 C++20 规范也没发布多久,工程应用并不现实。
ranges 也有同名的孵化项目 ericniebler/range-v3: Range library for C++14/17/20, basis for C++20’s std::ranges ,如果不想立即启用 c++20,那么可以使用这个项目,它需要 clang 3.6+ 或者 gcc 4.9.1,所以旧工程表示无压力。
总的来说,ranges 提供一种 filter 手段,在管道过滤的基础上你可以做筛选、排序、mapreduce 等等操作。
不过 range v3 也是褒贬不一了。
怎么理解范围库?
实际上这个家伙和其它一些术语,例如 protocol,concept 等等同样地莫名其妙、不知所云。
你知道什么是概念吗?iykyk,我特么一真真的中国人还会对概念没概念吗。
不过,C++20 的 std::concept 就是个怪怪的东西。
它们的实际含义要去考究英文词根的词源,所以对于中国人来说很难直观理解。
在这里,我们不去管 ranges 词源问题,而是借助于 std::ranges::view 来理解它。范围库换个人话来说,就是对一个集合做一个截面,获得一个可观察的视图(view),然后对该 view 进行各种可能的操作,可以倍乘、加一,也可以计算均值、做其他聚集操作,还可以排序,更可以 map reduce。
能做什么的问题,不是 ranges 的问题,而是你准备如何在 ranges 提供的 view 上做操作的问题。所以想要运用好 ranges 库,你应该对函数式编程(functional programming)有足够的理解。对于 C# Linq 技术,RxJava 有理解的朋友能够很容易理解 std::ranges 的核心内涵。
使用
至于使用 ranges,参考它们提供的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <string>
#include <vector>
#include <range/v3/view/filter.hpp>
#include <range/v3/view/transform.hpp>
using std::cout;
int main()
{
std::vector<int> const vi{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
using namespace ranges;
auto rng = vi | views::filter([](int i) { return i % 2 == 0; }) |
views::transform([](int i) { return std::to_string(i); });
// prints: [2,4,6,8,10]
cout << rng << '\n';
}
很难说这是不是进步。
别人家的孩子
康康 Kotlin 的:
1
2
3
4
5
6
7
8
val animals = listOf("raccoon", "reindeer", "cow", "camel", "giraffe", "goat")
// grouping by first char and collect only max of contains vowels
val compareByVowelCount = compareBy { s: String -> s.count { it in "aeiou" } }
val maxVowels = animals.groupingBy { it.first() }.reduce { _, a, b -> maxOf(a, b, compareByVowelCount) }
println(maxVowels) // {r=reindeer, c=camel, g=giraffe}
如果是 RxJava 的 Kotlin 更好看:
1
2
3
4
5
6
Observable.just("Apple", "Orange", "Banana")
.subscribe(
{ value -> println("Received: $value") }, // onNext
{ error -> println("Error: $error") }, // onError
{ println("Completed!") } // onComplete
)
要不是 Kotlin 总是带着 JVM,我就完整彻底地慕了。
当然 Rx 还有 c++ 版本 RxCpp,用起来是这个味道:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include "rxcpp/rx.hpp"
namespace Rx {
using namespace rxcpp;
using namespace rxcpp::sources;
using namespace rxcpp::operators;
using namespace rxcpp::util;
}
using namespace Rx;
#include <regex>
#include <random>
using namespace std;
using namespace std::chrono;
int main()
{
random_device rd; // non-deterministic generator
mt19937 gen(rd());
uniform_int_distribution<> dist(4, 18);
// for testing purposes, produce byte stream that from lines of text
auto bytes = range(0, 10) |
flat_map([&](int i){
auto body = from((uint8_t)('A' + i)) |
repeat(dist(gen)) |
as_dynamic();
auto delim = from((uint8_t)'\r');
return from(body, delim) | concat();
}) |
window(17) |
flat_map([](observable<uint8_t> w){
return w |
reduce(
vector<uint8_t>(),
[](vector<uint8_t> v, uint8_t b){
v.push_back(b);
return v;
}) |
as_dynamic();
}) |
tap([](vector<uint8_t>& v){
// print input packet of bytes
copy(v.begin(), v.end(), ostream_iterator<long>(cout, " "));
cout << endl;
});
// ...
// full codes at: https://github.com/ReactiveX/RxCpp
return 0;
}
只能说什么,坏就坏在 [...](...){ ... }
这样的 lambda 函数体太离谱了,相当不好看。它和 kotlin 的 lambda 一比较的话就只能退散了。
其实 C++11 以来的各种改变一直如此,造就了大量奇奇怪怪不符合直觉的东西。若非也有新造一些好东西,加上历史沿革,恐怕它也死了吧。
关于我提到的争议
首先一方面,C++ 的匿名函数形状太糟糕。请参考 C++0x lambdas suck,C++ lambda ugly 等等。
另一方面,我们不得不看到,这也是历史的原因,不提 C++ 的函数式原本有它自己的风格,而 ranges 之类的东西标准来的太迟,太多各种各样的实现造成了混乱,而标准嘛、总是姗姗来迟。
有了 C# 闭包,Java 8 闭包,Kotlin 闭包的珠玉在前,先入为主之下,C++ 的这一战役是胜不了了。
关于 Extension Method
这个概念,是协议化编程(Protocol-oriented Progamming)的一部分。
Swift
在 Swift 语言中,你可以通过 Protocol 的方式对任意一个类进行扩充(Extensions)。它表现的有些像这样:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let pythons = ["Eric", "Graham", "John", "Michael", "Terry", "Terry"]
let beatles = Set(["John", "Paul", "George", "Ringo"])
extension Collection {
func summarize() {
print("There are \(count) of us:")
for name in self {
print(name)
}
}
}
// Both Array and Set will now have that method, so we can try it out
pythons.summarize()
beatles.summarize()
另一个例子,在 String 类上增加一个 reverse() 方法(注:String 有原生方法 reversed()):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Foundation
extension String {
func reverse() -> String {
var word = [Character]()
for char in self {
word.insert(char, at: 0)
}
return String(word)
}
}
var bobobo = "reversing"
print(bobobo.reverse())
// gnisrever
上面的例子们都没有利用 Protocol 进行约束,所以只展示了对已有的类做 Extension 的能力。你可以对符合特定 protocol 约束条件的泛型类进行 extension,从而让这组泛型类额外地具备一个新的方法。这些内容太庞大了,所以不再在这里继续展开了。
Kotlin
Kotlin 提供了 Extension Function 的手段,这对 Java 可以算是颠覆性的创新设计。
这次的例子稍微变一变:
1
2
3
4
5
6
7
8
9
10
11
12
fun String.reverseCaseOfString(): String {
val inputCharArr = toCharArray()
var output = ""
for (i in 0 until inputCharArr.size) {
output += if (inputCharArr[i].isUpperCase()) {
inputCharArr[i].toLowerCase()
} else {
inputCharArr[i].toUpperCase()
}
}
return output
}
这就为 String 类增加了 reverseCaseOfString() 方法,用起来和 String.length() 没有区别:
1
print("CaseSensitive".reverseCaseOfString())
做手机端开发的朋友对 Swift 和 Kotlin 的这个能力应该是很熟悉的。
在 C++ 中
我已经研究、梦想了很久了(数年之久,跨了十年了),想要在 C++ 中能够做同样的事情。不过即使到 c++23 这也仍旧是不可能滴。
至于创作一个等效的 C++ 类库,屡败屡战之下,结论是仍旧不可能。
不那么完美地通过 c++ 来达到扩展已有类功能,可以通过 Decorator Pattern,这是最规范的方案,但形状不好、累赘太多:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
#include <string>
struct Shape {
virtual ~Shape() = default;
virtual std::string GetName() const = 0;
};
struct Circle : Shape {
void Resize(float factor) { radius *= factor; }
std::string GetName() const override {
return std::string("A circle of radius ") + std::to_string(radius);
}
float radius = 10.0f;
};
struct ColoredShape : Shape {
ColoredShape(const std::string& color, Shape* shape)
: color(color), shape(shape) {}
std::string GetName() const override {
return shape->GetName() + " which is colored " + color;
}
std::string color;
Shape* shape;
};
int main() {
Circle circle;
ColoredShape colored_shape("red", &circle);
std::cout << colored_shape.GetName() << std::endl;
}
使用装饰模式,既可以装饰已有方法的实现,也可以新增方法(CIrcle.Resize()),但你需要显示地构造到装饰器类才行(ColoredShape colored_shape("red", &circle)
)。改进的方法是利用模板类进行 mixin,但那也改善的有限。
小结
以上,个人观点,你随便看看就好——Dont come for me pls。
Retried
留下评论