0%

雖然這東西沒什麼技術含量,不過流程也是要自己跑過一遍才會有記憶點,如果只看過沒跑過很快就忘
現代的Javascript Develop實在麻煩,需要很多工具配合使用

先決條件

沒什麼好講的

1
2
$ mkdir modern_javascript_library && cd modern_javascript_library
$ npm init -y

Typescript

身為不專業的C++開發者,我比較能接受Typescript,所以就用上了

1
$ npm install typescript --save-dev

接著編寫tsconfig.json,依需求自行修改

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
{
"compilerOptions": {
"allowUnreachableCode": true,
"strictNullChecks": true,
"strict": true,
"esModuleInterop": true,
"target": "es5",
"noImplicitAny": true,
"removeComments": true,
"preserveConstEnums": false,
"sourceMap": true,
"outDir": "./lib",
"lib": [
"dom",
"esnext"
],
"downlevelIteration": true,
"moduleResolution": "node",
"baseUrl": ".",
"rootDir": ".",
"paths": {
"@/*":["src/*"],
"@/types":["src/types"]
}
},
"include": [
"src/main.ts"
]
}

可以看到我們的進入點是src/main.ts
接著就開始寫src/main.ts

1
2
import print from './print'
print()

看到我們import了print,所以要補上這個檔案

1
2
3
4
5
6
function print()
{
console.log('Hello World!')
}

export default print

接著編譯它

1
$ npx tsc

驗證結果

1
2
$ node lib/src/main.js
Hello World!

第一階段完成,我們可以看到產生了main.js和print.js兩個檔案,接著我們要打包在一起了

Rollup

Rollup是個打包工具,跟Webpack比起來更適合打包Library,在這個地方我們使用它

1
$ npm install rollup @rollup/plugin-typescript --save-dev

編寫rollup.config.js

1
2
3
4
5
6
7
8
9
10
import typescript from '@rollup/plugin-typescript';

export default {
input: 'src/main.ts',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [typescript()]
};

執行它

1
$ npx rollup -c

可以看到產生了output/main.js一個檔案而已

1
2
$ node output/main.js
Hello World!

結果正確

ESLint

ESLint也是現代JS開發不可或缺的玩意,一併用上

1
$ npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin --save-dev

編寫.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:@typescript-eslint/recommended" // Uses the recommended rules from the @typescript-eslint/eslint-plugin
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
},
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
}
};

修改package.json,新增lint Rule

1
2
3
"scripts": {
"lint": "tsc --noEmit && eslint 'src/*.{js,ts,tsx}' --quiet --fix"
},

接著跑一次看看

1
2
3
$ npm run lint
> modern_javascript_library@1.0.0 lint /home/hm/modern_javascript_library
> tsc --noEmit && eslint 'src/*.{js,ts,tsx}' --quiet --fix

因為程式碼不複雜,自然也不會有什麼問題產生

Prettier

對Javascript Code做Formatter的工具,類似clang-format或是gofmt,少不了又要配置一堆東西

1
$ npm install prettier eslint-config-prettier eslint-plugin-prettier --save-dev

新增.prettierrc.js

1
2
3
4
5
6
7
module.exports = {
semi: true,
trailingComma: "all",
singleQuote: true,
printWidth: 120,
tabWidth: 4
};

修改上一步驟的.eslintrc.js

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
"prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
"plugin:prettier/recommended" // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module" // Allows for the use of imports
}
};

同上一步,執行

1
$ npm run lint

如果沒有錯誤的話,ts檔案會auto formatter

Conclusion

這東西做個一次可能還很新鮮,重複幾次下來就會煩躁
因此一堆人開發了Scaffold來做這件事,畢竟我不是專業JS開發者,淺嘗則止
哪天轉職之後再來看也不遲,不過由於Javascript太弱了,才需要一堆工具,真是麻煩

契機

雖然知道Logdown是一個不再維護的產品,不過得過且過,畢竟搬家麻煩,不過當前幾天XDite跑路,Logdown打不開的時候,危機意識就產生了
總有一天這個地方會被關掉,趁著Logdown還活著的時候趕緊搬家

我的Blog寫作平台史

Wordpress

沒什麼好講的,用mysql存檔到沒什麼問題,問題在於存起來的格式,對於Programmer來說,Embedded code根本就是災難

Octopress

用Markdown寫Blog,當初也花了很多時間在建立環境,重點是維護環境很麻煩,Deploy也麻煩,更慘的是,Octopress停止開發,
Ruby我也看不太懂

Logdown

這是我用最久的組合,只負責寫作,Depoly就交給後台負責了,不過負責人跑了,不跟著跑才有鬼

Hexo

雖然有許多類似的玩意 (Hugo/Vuepress/Gatsby) ,不過我還是選擇最簡單的方案,我不是個合格的Frontned Developer,我只想寫作
加上Github action的功能,我終於可以把Deploy這件事交給別人做了

2020新的開始,就想辦法克服懶病,多寫一些廢文吧

Reference

通过 Github Actions 自动发布 Hexo 博客

由於是個Kubernetes初學者,一切從頭開始
基本上是趙個這篇Write a Kubernetes-ready service from zero step-by-step開始的,加上一些新玩意
– 把dep換成Golang原生的Module
– 利用 Docker Multistage Build,可以參考透過 Multi-Stage Builds 改善持續交付流程
– 將裡面的Minikube換成了microk8s
– 原本想自己架Private Docker Registry,K8S那邊搞不定,放棄,乖乖用Docker Hub

搞定環境的時間比寫程式的時間多十倍

成果就放在go-k8s-example

由於是k8s新手,所以希望先從Minikube這樣的小東西開始學習
microk8s現在只有Debian/Linux有,window或是MacOS還是乖乖用Minikube吧
首先先安裝snapd

1
$ sudo apt install -y snapd

接著透過snapd安裝 microk8s

1
$ sudo snap install microk8s --classic

將自己加入microk8s的group

1
$ sudo usermod -a -G microk8s `whoami`

登出Session之後重登入

1
2
$ type microk8s.kubectl
microk8s.kubectl is hashed (/snap/bin/microk8s.kubectl)

由於microk8s的指令都是microk8s開頭的,我們希望跟kubectl一樣

1
2
3
$ sudo snap alias microk8s.kubectl kubectl
$ type kubectl
kubectl is hashed (/snap/bin/kubectl)

從one_of說起

之前寫過這樣的 Code Snippet

1
2
3
4
5
6
7
8
9
10
#include <initializer_list>
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <typename ...Ts>
bool is_any_of(state_type s, const Ts& ...ts)
{
bool match = false;
(void)std::initializer_list<int>{(match |= (s == ts), 0)...};
return match;
}
is_any_of(s, IDLE, DISCONNECTING);

這程式碼適用於C++11,如果用C++17可以用fold expression更進一步簡化

1
2
3
4
5
6
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <typename ...Ts>
bool is_any_of(state_type s, const Ts& ...ts)
{
return ((s == ts) || ...);
}

能不能更進一步?

variadic non type template

1
2
3
4
5
6
7
enum state_type { IDLE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED };
template <state_type ... states>
bool is_any_of(state_type s)
{
return ((s == states) || ...);
}
is_any_of<IDLE, DISCONNECTING>(s);

可以看出,這個版本將比較值跟貝比教值分開,比原先的版本更容易看出語意,不過問題也很明顯,所以被比較值要在編譯期就決定了,靈活度反而比不上原先的方案,有沒有折衷的方案?

std::tuple

看到Modern Techniques for Keeping Your Code DRY這方案真是眼睛一亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts == t) | ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}
template <typename T>
friend bool operator==(const T& lh, const any_of& rh) { return rh == lh; }
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;
any_of(IDLE, DISCONNECTING) == s
s == any_of(IDLE, DISCONNECTING)

用CTAD,std::tuple,std::apply的組合技包裝成威力十足的武器

Add more operation

假設我們現在要加上<的比較

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts == t) || ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}

template <typename T>
bool operator<(const T& t) const {
return std::apply([&t](const auto& ...ts) { return ((ts < t) || ...); },
static_cast<const std::tuple<Ts...>&>(*this));
}
};

可以看出兩段程式碼大同小異,能否亙進一步?

Higher order functions

由於兩個operator都是由一系列or operation組成的
因此我們可以把or operation提出來

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename F, typename ...Ts>
bool or_elements(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) || ...); }, t);
}

template <typename ...Ts>
struct any_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return or_elements([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return or_elements([&t](const auto& v) { return v < t; }, *this);
}
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;

至此any_of的東西就差不多了,那麼來寫all_of吧

all_of implementation

大同小異

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
template <typename F, typename ...Ts>
bool and_elements(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) && ...); }, t);
}

template <typename ...Ts>
struct all_of : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return and_elements([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return and_elements([&t](const auto& v) { return v < t; }, *this);
}
};

template <typename ...Ts>
all_of(Ts...)->all_of<Ts...>;

一般來說做到此就行了,能否亙進一步?

Inheritance

再上去就是炫技了,實用度就不高了,編譯時間又拉長不少

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
struct or_elements {
template <typename F, typename ...Ts>
static bool apply(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) || ...); }, t);
}
};

struct and_elements {
template <typename F, typename ...Ts>
static bool apply(const F& f, const std::tuple<Ts...>& t) {
return std::apply([&f](const auto& ...ts) { return (f(ts) && ...); }, t);
}
};

template <typename Op, typename ...Ts>
struct op_t : private std::tuple<Ts...> {
using std::tuple<Ts...>::tuple;
template <typename T>
bool operator==(const T& t) const {
return Op::apply([&t](const auto& v) { return v == t; }, *this);
}

template <typename T>
bool operator<(const T& t) const {
return Op::apply([&t](const auto& v) { return v < t; }, *this);
}

template <typename T>
friend bool operator==(const T& lh, const op_t& rh) { return rh == lh; }
template <typename T>
friend bool operator>(const T& lh, const op_t& rh) { return rh < lh; }
};

template <typename ...Ts>
struct any_of : op_t<or_elements, Ts...> {
using op_t<or_elements, Ts...>::op_t;
};

template <typename ...Ts>
struct all_of : op_t<and_elements, Ts...> {
using op_t<and_elements, Ts...>::op_t;
};

template <typename ...Ts>
any_of(Ts...)->any_of<Ts...>;
template <typename ...Ts>
all_of(Ts...)->all_of<Ts...>;

Reference

Modern Techniques for Keeping Your Code DRY
Lambdas: From C++11 to C++20, Part 1
Lambdas: From C++11 to C++20, Part 2

從C# Extension Method說起

1
2
3
4
5
6
7
8
9
static class StringUtilities
{
public static int WordCount(this string text)
{
return text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
var text = "This is an example";
var count = text.WordCount();

將一個Method綁在已有的type之上
在C++有類似的提案,叫做UFCS
不過提案會不會過還不知道,但是可以用目前的技術模擬
以下從Reddit看來的兩種方式,寫下自己的看法

Ver 1

使用CRTP技巧來達成

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
// One way to emulate UFCS in standard C++.

// A CRTP class that forwards .call<F>(...) to F::function(*this, ...)
template<typename T>
struct uniform_call_syntax {
template <typename F, typename... Args>
constexpr auto call(Args... args) {
return F::function(static_cast<T&>(*this), args...);
}
};

// Usage:
struct my_type : public uniform_call_syntax<my_type> {
int value;
};

struct set_value {
static my_type& function(my_type& o, int i) {
o.value = i;
return o;
}
};

struct add_value {
static my_type& function(my_type& o, int i) {
o.value += i;
return o;
}
};

struct get_value {
static int function(const my_type& o) {
return o.value;
}
};

int test() {
my_type obj;

return obj
.call<set_value>(10)
.call<add_value>(20)
.call<get_value>(); // returns 30.

}

作法顯而易見,每個需要UFCS功能的都需要繼承uniform_call_syntax,然後每個Externsion method都需要有同樣的function name
缺點也顯而易見,要有這功能就需要繼承,有些type是不允許修改的

Ver 2

版本二需要C++14以上的標準

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<string>
#include<iostream>

// Same as Haskell's infix operator `&` or F#'s operator `|>`
template<typename Arg, typename F>
decltype(auto) operator % (Arg&& arg, F&& fn)
{
return std::forward<F>(fn)(std::forward<Arg>(arg));
}

namespace my
{
auto tolower = []
{
return [](auto&& s) -> decltype(auto)
{
for (auto& c : s) {
c = ('A' <= c && c <= 'Z') ? c - ('Z' - 'z') : c;
}
return decltype(s)(s);
};
};

auto append(const std::string& what)
{
return [&](auto&& s) -> decltype(auto)
{
s += what;
std::cerr << "is-lvalue-reference:" << std::is_lvalue_reference<decltype(s)>::value << ";" << s << "\n";
return std::forward<decltype(s)>(s);
};
};
}

int main()
{
// as rvalue-references
auto s1 = std::string{ "HELLO," }
% my::tolower()
% my::append(" world");

// as lvalue-references
auto s2 = std::string{ "GOODOBYE," };
s2% my::tolower()
% my::append(" cruel world.");

std::cerr << s1 << "\n" << s2 << "\n";

return 0;
}

看起來很美,很像Functional Programming的Pipe觀念
不過對Programmer的要求比較高,至少要有FP的基本觀念才比較容易理解

看了C++17的文章之後,還是搞不太清楚兩者的差異,於是自己寫程式驗證一下

Capture this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include <memory>
struct Obj {
Obj(int value) : v(std::make_unique<int>(value)) {}
~Obj() { printf("Obj have been destroyed\n"); }
Obj(const Obj& o) {
printf("Copy constructor invoked\n");
v = std::make_unique<int>(*o.v);
}
Obj(Obj&& o) {
printf("Move constructor invoke\n");
v = std::move(o.v);
}
auto test() { return [this] { printf("%d\n", *v); }; }
std::unique_ptr<int> v;

};
int main()
{
auto test = Obj(123).test();
test();
}

很不意外的,Crash掉了
如果我們把呼叫改成Obj(123).test()(),Obj的Lifetime就跟執行時間一樣長了,可以正常執行

Capture *this

1
2
3
struct Obj {
auto test() { return [*this] { printf("%d\n", *v); }; }
};

跟上面一樣,只是修改了capture this的方式

1
2
3
4
Copy constructor invoked
Obj have been destroyed
123
Obj have been destroyed

由此可見他會呼叫Copy construtor,然後摧毀了原先的Obj,lambda中的this指的就是Copy的Obj

Simulate *this without C++17

要這麼做也不是不行

1
2
3
struct Obj {
auto test() { return [self = *this] { printf("%d\n", *self.v); }; }
};

C++17的程式碼比較乾淨,不過這個版本就很清楚知道這個this是複製品了

最近實在是太頹廢了,強迫自己寫些新東西
蟒蛇有些新玩具,可以加入自己的工具組合之中

PySnooper

Github Project主業
首先先安裝pysnooper這個package

1
$ pip install pysnooper

然後來個最簡單的示範,寫個比官方範例更複雜一點

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@pysnooper.snoop()
def findPrimes(number):
num = 2
primes = []
while num <= number:
div = 2
flag = True
while div * div <= num and flag:
if num % div == 0:
flag = False
div = div + 1
if flag:
primes.insert(len(primes), num)
num = num + 1
return primes
print(findPrimes(10))

然後可以列出所有中間產物的過程

輸出到Log file

可以指定到不同的Log file

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pysnooper

@pysnooper.snoop("./debug1.log")
def isPrime(num):
div = 2
while div * div <= num:
if num % div == 0:
return False
div = div + 1
return True

@pysnooper.snoop("./debug.log")
def findPrimes(number):
num = 2
primes = []
while num <= number:
if isPrime(num):
primes.insert(len(primes), num)
num = num + 1
return primes
print(findPrimes(10))

用不同的Prefix區分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import pysnooper

@pysnooper.snoop("./debug.log", prefix="--isPrime--")
def isPrime(num):
div = 2
while div * div <= num:
if num % div == 0:
return False
div = div + 1
return True

@pysnooper.snoop("./debug.log", prefix="--findPrimes--")
def findPrimes(number):
num = 2
primes = []
while num <= number:
if isPrime(num):
primes.insert(len(primes), num)
num = num + 1
return primes
print(findPrimes(10))

stackprinter

Github Project主業
要做的還是先裝上去

1
$ pip install stackprinter

接著我們修改上面的範例,讓它Crash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def isPrime(num):
div = 0
while div * div <= num:
if num % div == 0:
return False
div = div + 1
return True

def findPrimes(number):
num = 2
primes = []
while num <= number:
if isPrime(num):
primes.insert(len(primes), num)
num = num + 1
return primes
print(findPrimes(10))

執行之後產生以下的 Crash info

1
2
3
4
5
6
7
8
Traceback (most recent call last):
File "a.py", line 20, in <module>
print(findPrimes(10))
File "a.py", line 16, in findPrimes
if isPrime(num):
File "a.py", line 7, in isPrime
if num % div == 0:
ZeroDivisionError: integer division or modulo by zero

如果我們加上了stachpointer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import stackprinter
stackprinter.set_excepthook(style='darkbg2')

def isPrime(num):
div = 0
while div * div <= num:
if num % div == 0:
return False
div = div + 1
return True

def findPrimes(number):
num = 2
primes = []
while num <= number:
if isPrime(num):
primes.insert(len(primes), num)
num = num + 1
return primes
print(findPrimes(10))

會變成這樣子

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
File a.py, line 20, in <module>
16 if isPrime(num):
17 primes.insert(len(primes), num)
18 num = num + 1
19 return primes
--> 20 print(findPrimes(10))

File a.py, line 16, in findPrimes
12 def findPrimes(number):
13 num = 2
14 primes = []
15 while num <= number:
--> 16 if isPrime(num):
17 primes.insert(len(primes), num)
..................................................
number = 10
num = 2
primes = []
..................................................

File a.py, line 7, in isPrime
4 def isPrime(num):
5 div = 0
6 while div * div <= num:
--> 7 if num % div == 0:
8 return False
..................................................
num = 2
div = 0
..................................................

ZeroDivisionError: integer division or modulo by zero

有顏色和箭頭比較容易分析哪裡出錯

既然C++20把Coroutine列為標準配備之後,必須試著了解玩法
從最簡單的範例開始

The simplest coroutine example

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 <iostream>
#include <experimental/coroutine>

#define CORETURN_ENABLE

struct generator {
struct promise_type {
int current_value;
auto initial_suspend() { return std::experimental::suspend_always{}; }
auto final_suspend() { return std::experimental::suspend_always{}; }
auto get_return_object() { return generator{handle_type::from_promise(*this)}; }
void unhandled_exception() { std::exit(1); }
auto yield_value(int v) {
current_value = v;
return std::experimental::suspend_always{};
}
#ifdef CORETURN_ENABLE
void return_value(int v) {
current_value = v;
}
#else
void return_void() {}
#endif
};
bool move_next() {
coro.resume();
return !coro.done();
}
int current_value() { return coro.promise().current_value; }
using handle_type = std::experimental::coroutine_handle<promise_type>;
handle_type coro;
};
generator count()
{
co_yield 42;
co_yield 56;
#ifdef CORETURN_ENABLE
co_return 999;
#endif
}

int main() {
auto g = count();
while (g.move_next())
std::cout << g.current_value() << std::endl;
#ifdef CORETURN_ENABLE
std::cout << g.current_value() << std::endl;
#endif
return 0;
}

分析

假設我們有一個Coroutine的程式碼片段,例如

1
2
3
4
5
template <typename TRet, typename … TArgs>
TRet func(TArgs args…)
{
body;
}

邊義氣會幫我們做類似這樣的處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
template <typename TRet, typename ... TArgs>
TRet func(TArgs args...)
{
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

promise_t promise;
auto __return__ = promise.get_return_object();

co_await promise.initial_suspend();

try
{ // co_return expr; => promise.return_value(expr); goto final_suspend;
body; // co_return; => promise.return_void(); goto final_suspend;
} // co_yield expr; => co_await promise.yield_value(expr);
catch (...)
{
promise.set_exception(std::current_exception());
}

final_suspend:
co_await promise.final_suspend();
}

對照Pseudo code func和上面範例的countˇ以及promise_tgenerator::promise_type明白了一些東西

– 任何包含co_yieldco_return或者co_await的程式碼都是corutine block,必須定義一個structure,而這個structure必須有個promise_type
– promeise_type需要六個函數, return_valuereturn_void不可同時存在
– 中間狀態必須保存在promise_type中
– 當co_yield交出控制權之後,必須使用coro.resume()恢復執行

以下討論另外一個關鍵字 co_await

co_await

先從古早時期的Callback說起,給出一個範例

From a callback start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <thread>
#include <iostream>
using call_back = std::function<void(int)>;
void Add100ByCallback(int init, call_back f)
{
std::thread t([init, f]() {
std::this_thread::sleep_for(std::chrono::seconds(1));
f(init + 100);
});
t.detach();
}
int main()
{
Add100ByCallback(10, [](int v) {
std::cout << v << "\n";
});
std::this_thread::sleep_for(std::chrono::seconds(5));
}

程式本身沒有什麼意義,只是模擬長時間處理的情形

Awaitable object

Coroutine版根據上面的版本修改,新增了一些東西

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
struct Task
{
struct promise_type {
auto get_return_object() { return Task{}; }
auto initial_suspend() { return std::experimental::suspend_never{}; }
auto final_suspend() { return std::experimental::suspend_never{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
};
};

struct Add100AWaitable
{
Add100AWaitable(int init) :init_(init) {}
bool await_ready() const { return false; }
int await_resume() { return result_; }
void await_suspend(std::experimental::coroutine_handle<> handle)
{
auto f = [handle, this](int value) mutable {
result_ = value;
handle.resume();
};
Add100ByCallback(init_, f);
}
int init_;
int result_;
};

Task Add100ByCoroutine(int init, call_back f)
{
std::cout << "Before co_await: " <<std::this_thread::get_id() << "\n";
int ret = co_await Add100AWaitable(init);
std::cout << "After co_await: " << std::this_thread::get_id() << "\n";
f(ret);
}

上面的Task不需要多做介紹,Add100ByCoroutine也不需要多說些什麼
直接看Add100AWaitable的部分
同樣的Add100AWaitable也有必須要注意的點
– 中間值一樣存在 Awaitable object
– 必須有await_readyawait_resumeawait_suspend的存在
– 以上面的例子來看,當Callback完成之後,我們手動讓Coroutine繼續執行上去
– 呼叫Add100ByCoroutine和corutime resume的Thread不一定是同一個,需要把狀態保留到awaitable object中

Reference

How C++ coroutines work
Coroutine Theory
From Algorithms to Coroutines in C++
Coroutines and Reference Parameters

Range-V3要進C++20 Standard,看了一下他給的範例

Pythagorean triple如何印出前十組Pythagorean triple

從最簡單的方案開始看

Direct Method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main() {
int i = 0;
for (int z = 1; true; ++z) {
for (int x = 1; x < z; ++x) {
for (int y = x; y < z; ++y) {
if (x * x + y * y == z * z) {
printf("(%d,%d,%d)\n", x, y, z);
if (++i == 10) goto done;
}
}
}
}
done:;
}

不會很難理解,必須迴圈外部維護一個狀態,當狀態滿足之後強行離開迴圈

Callback Solution

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
51
#include <iostream>
#include <tuple>
#include <type_traits>

template<class F>
auto boolify(F& f) {
return [&](auto&&... args) {
using R = decltype(f(std::forward<decltype(args)>(args)...));
if constexpr (std::is_void_v<R>) {
f(std::forward<decltype(args)>(args)...);
return false;
}
else {
return f(std::forward<decltype(args)>(args)...);
}
};
}

template<class F>
void generate_triples(F f) {
for (int z = 1; true; ++z) {
for (int x = 1; x < z; ++x) {
for (int y = x; y < z; ++y) {
if (x*x + y * y == z * z) {
bool stop = boolify(f)(std::make_tuple(x, y, z));
if (stop) return;
}
}
}
}
}

template<class F>
auto take(int k, F f) {
return [f, i = k](auto x) mutable -> bool {
return (i-- == 0) || boolify(f)(x);
};
}

int main() {
generate_triples(
take(10,
[&](auto triple) {
std::cout << '('
<< std::get<0>(triple) << ','
<< std::get<1>(triple) << ','
<< std::get<2>(triple) << ')' << '\n';
}
)
);
}

這個難看懂很多
首先程式主體

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class F>
void generate_triples(F f) {
for (int z = 1; true; ++z) {
for (int x = 1; x < z; ++x) {
for (int y = x; y < z; ++y) {
if (x*x + y * y == z * z) {
bool stop = boolify(f)(std::make_tuple(x, y, z));
if (stop) return;
}
}
}
}
}

是沒壁的,我們的中止條件就在 take(10, ...)這個函數之間
一旦滿足條件,整個genertat4e_tyiples就結束了
我們看看 take的實作

1
2
3
4
5
6
template<class F>
auto take(int k, F f) {
return [f, i = k](auto x) mutable -> bool {
return (i-- == 0) || boolify(f)(x);
};
}

我們把state包含在lambda之中,避免被外界汙染
至於boolify這個函數不重要,不影響分析

這個版本比起上面那個,邏輯切個更清晰
不過複雜度也跟著增加了

Coroutine Solution

隨著C++20加入stackless coroutine,這個問題也可以用coroutine來解

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
51
52
53
#include <iostream>
#include <tuple>
#include <experimental/coroutine>

template<class T>
struct generator {
struct promise_type;
using handle = std::experimental::coroutine_handle<promise_type>;
struct promise_type {
T current_value;
auto get_return_object() { return generator{ handle::from_promise(*this) }; }
auto initial_suspend() { return std::experimental::suspend_always{}; }
auto final_suspend() { return std::experimental::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
void return_void() {}
auto yield_value(T value) {
current_value = value;
return std::experimental::suspend_always{};
}
};
bool move_next() { return coro ? (coro.resume(), !coro.done()) : false; }
T current_value() { return coro.promise().current_value; }
generator(generator const&) = delete;
generator(generator && rhs) : coro(rhs.coro) { rhs.coro = nullptr; }
~generator() { if (coro) coro.destroy(); }
private:
generator(handle h) : coro(h) {}
handle coro;
};

auto triples() -> generator<std::tuple<int, int, int>> {
for (int z = 1; true; ++z) {
for (int x = 1; x < z; ++x) {
for (int y = x; y < z; ++y) {
if (x*x + y * y == z * z) {
co_yield std::make_tuple(x, y, z);
}
}
}
}
}

int main() {
auto g = triples();
for (int i = 0; i < 10; ++i) {
g.move_next();
auto triple = g.current_value();
std::cout << '('
<< std::get<0>(triple) << ','
<< std::get<1>(triple) << ','
<< std::get<2>(triple) << ')' << '\n';
}
}

由於coroutine還是新玩意,目前對他的掌握度不適很高,這個方案僅供參考

Range-V3 Solution

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// A sample standard C++20 program that prints
// the first N Pythagorean triples.
#include <iostream>
#include <optional>
#include <range/v3/all.hpp>

using std::get;
using std::optional;
using std::make_tuple;
using std::cout;
using ranges::view_interface;
using ranges::iterator_t;
namespace view = ranges::v3::view;

// maybe_view defines a view over zero or one
// objects.
template<class T>
struct maybe_view : view_interface<maybe_view<T>> {
maybe_view() = default;
maybe_view(T t) : data_(std::move(t)) {
}
T const *begin() const noexcept {
return data_ ? &*data_ : nullptr;
}
T const *end() const noexcept {
return data_ ? &*data_ + 1 : nullptr;
}
private:
optional<T> data_{};
};

// "for_each" creates a new view by applying a
// transformation to each element in an input
// range, and flattening the resulting range of
// ranges.
// (This uses one syntax for constrained lambdas
// in C++20.)
inline constexpr auto for_each =
[](auto&& r, auto fun) {
return decltype(r)(r)
| view::transform(std::move(fun))
| view::join;
};

// "yield_if" takes a bool and a value and
// returns a view of zero or one elements.
inline constexpr auto yield_if =
[](bool b, auto x) {
return b ? maybe_view{std::move(x)}
: maybe_view<decltype(x)>{};
};

int main() {
// Define an infinite range of all the
// Pythagorean triples:
using view::iota;
auto triples =
for_each(iota(1), [](int z) {
return for_each(iota(1, z+1), [=](int x) {
return for_each(iota(x, z+1), [=](int y) {
return yield_if(x*x + y*y == z*z,
make_tuple(x, y, z));
});
});
});

// Display the first 10 triples
for(auto triple : triples | view::take(10)) {
cout << '('
<< get<0>(triple) << ','
<< get<1>(triple) << ','
<< get<2>(triple) << ')' << '\n';
}
}

看起來還不錯,不過編譯時間嚇死人…
目前還是先考慮方案一或二吧,三和四等掌握度高一點再說