0%

看到很多文章,總覺得從co_await開始講解實在很難清楚表達
寫了自己的版本當作筆記

The simplest coroutine

這程式碼什麼都沒幹,不過就是一個最小的coroutine了
一個coroutine至少要有co_returnco_awaitco_yield其中之一

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
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task(coroutine_handle<promise_type>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};
Task(coroutine_handle<promise_type> h) : handle(h) {}
~Task() {
if (handle)
handle.destroy();
}
coroutine_handle<promise_type> handle;
};

Task coroutineDemo()
{
co_return;
}

int main() {
auto task = coroutineDemo();
return 0;
}

Under the hood

Compilier做了很多事情
像這樣的Psuedo Code

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

被Compilier處理之後大概變成這個樣子

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.unhandled_exception();
}

final_suspend:
co_await promise.final_suspend();
}

先忽略co_await的語句,之後補上
因此我們可以看到prmise_type裡面有initial_suspendfinal_suspend等function,這個promise_type是定義coroutine的行為模式
之後會對promise_type做更進一步的說明,這邊就此打住

How the compiler chooses the promise type

看到上面的Pseudo code

1
using promise_t = typename coroutine_traits<TRet, TArgs...>::promise_type;

然後看看coroutine_traits的定義

1
2
3
4
5
6
7
8
9
10
template <class _Ret, class = void>
struct _Coroutine_traits {};

template <class _Ret>
struct _Coroutine_traits<_Ret, void_t<typename _Ret::promise_type>> {
using promise_type = typename _Ret::promise_type;
};

template <class _Ret, class...>
struct coroutine_traits : _Coroutine_traits<_Ret> {};

就是看TRet裡面有沒有promise_type的struct definition了
者李又分成兩類

直接定義在class裡面

就是我們範例那個做法,簡單直接

將promise_type抽出

當你有一群Coroutine,然後這群Coroutine雖然有些許不同,但是對Coroutine的控制流程相同,就可以用這方案

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename T>
struct Promise {
auto initial_suspend() { return suspend_never{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return T(coroutine_handle<Promise>::from_promise(*this)); }
void return_void() {}
void unhandled_exception() {}
};

struct Task {
// Ignore
using promise_type = Promise<Task>;
};

著名的C++ coroutine都使用此方式

coroutine_handle

看到我們的Task當中有coroutine_handle了嗎

1
2
3
struct Task {
coroutine_handle<promise_type> handle;
};

這個才是coroutine的本體,負責讓暫停的coroutine繼續執行,或是判斷coroutine是否執行完畢
將原先的範例加強一下,來示範coroutine_handle該怎麼使用

A failure example

原本我想寫個像這樣的程式加深對coroutine_handle的使用

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
#include <iostream>
using namespace std;
#if defined(__clang__)
#include <experimental/coroutine>
using namespace std::experimental;
#else
#include <coroutine>
#endif

struct Task {
struct promise_type {
auto initial_suspend() { return suspend_always{}; }
auto final_suspend() noexcept { return suspend_never{}; }
auto get_return_object() { return Task{ coroutine_handle<promise_type>::from_promise(*this) }; }
void return_void() {}
void unhandled_exception() {}
};
coroutine_handle<promise_type> handle;
~Task() {
if (handle)
handle.destroy();
}
void resume() { handle.resume(); }
bool done() const { return handle.done(); }
};

Task coroutineDemo(int times)
{
for (size_t i = 0; i < times; i++) {
cout << "coroutineDemo\n";
co_await suspend_always{};
}
co_return;
}

int main() {
auto task = coroutineDemo(3);
while (!task.done()) {
task.resume();
}
std::cout << "Done\n";
return 0;
}

結果不如預期,發現問題出在

Once execution propagates outside of the coroutine body then the coroutine frame is destroyed. Destroying the coroutine frame involves a number of steps:

  1. Call the destructor of the promise object.
  2. Call the destructors of the function parameter copies.
  3. Call operator delete to free the memory used by the coroutine frame (optional)
  4. Transfer execution back to the caller/resumer.

問題就出在3這步

  • Visual C++不會在coroutine body結束時立刻delete coroutine frame
  • GCC和Clang會,當coroutine frame destroy之後,呼叫handle.done()handle.resume()是use-after-free

Solution

研究了一下, 只要將final_suspend改成

1
2
3
4
struct promise_type {
// ignored
auto final_suspend() noexcept { return suspend_always{}; }
};

讓它在coroutine結束之前停下來就可以了

Reference

Function template overload

拿Boost.Serialization當範例

1
2
3
4
5
6
7
namespace boost::serialization {
template <class Archive>
void serialize(Archive& ar, two_things& tt, unsigned int version) {
ar & tt.a;
ar & tt.b;
}
}

Pros:

  • 就像膠水,將黏貼部分獨立成單一Unit,需要的時候再引入即可

Cons:

  • 跟前個方式依樣,需要打開library的namespace
  • 跟前一個方式依樣,存在Name Collisions的可能

Friend member function

又是Boost.Serialization

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <boost/serialization/serialization.hpp>
struct two_things {
private:
friend class boost::serialization::access;
template <class Archive>
void serialize(Archive& ar, unsigned int version) {
ar & a;
ar & b;
}
public:
int a;
bool b;
};

優缺點正好跟上面相反

Pros:

  • 不需要打開library的namespace
  • 不會造成Name Collisions

Cons:

  • Tightly coupled with structure

當我們如果不需要serialization功能時,透過上面的方式,我們只需要不include implemtation unit即可,不過這方法就不行了
另外一點,這方式引入了boost::serialization的依賴,不管我們需不需要,都必須承受這副作用

ARGUMENT-DEPENDENT LOOKUP

C++最難理解的特性之一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
namespace {
struct two_things {
int a;
bool b;
};
void do_something(two_things &t) {
printf("do_something in anonymous namespace\n");
}
};

int main()
{
two_things t;
do_something(t);
}
Worng usage example of ADL (std::swap)

What’s wrong with swap
看看以下的程式馬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace A {
struct two_things {
int a;
bool b;
};
void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};
namespace B {
template <typename T>
void test(T& a, T&b)
{
swap(a, b);
}
}
int main()
{
A::two_things a, b;
int c, d;
B::test(a, b);
B::test(c, d);
}
  • swap(a, b) // invokes ADL because call name is unqualified
  • std::swap(a, b) // does not invoke ADL because call name is qualified

如果我們把swap改成std::stap,自訂億的swap就毫無作用了
正確的做法應該是

1
2
3
4
5
6
7
8
namespace B {
template <typename T>
void test(T& a, T&b)
{
using std::swap;
swap(a, b);
}
}
Correct usage example of ADL (std::swap)
  • Create a callable function object which does the two-step with an internal detail namespace’s swap.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    namespace nonstd {
    namespace detail {
    template <typename T>
    void swap(T&, T&) { printf("nonstd::swap\n"); }
    struct swap_func {
    template <typename T>
    void operator()(T &a, T&b) const noexcept {
    swap(a, b);
    }
    };
    }
    inline constexpr const auto swap = detail::swap_func{};
    }

    namespace B {
    template <typename T>
    void test(T& a, T&b)
    {
    nonstd::swap(a, b);
    }
    }
    這做法在Reference中的文章有特別說明,被稱作Customization Point Object
    亦或是niebloid(以作者的名稱命名的單字)
Friend or not?

假設我們把 namespace A中的swap從free_function變成friend function

1
2
3
4
5
6
7
8
struct two_things {
int a;
bool b;
friend void swap(two_things &, two_things &)
{
printf("swap in namespace A\n");
}
};

兩者是等價的,不過

  • friend function 可以存取 structure的private field,free function不能
  • friend function只能被ADL找到
Downgrade to C++11

inline variable是C++17才有的玩意 在有些時候用不了新標準的情況之下 有必要找個方法向下相容

1
2
3
4
5
6
7
8
namespace nonstd {
// older standards:
template <typename T>
struct __static_const { static constexpr T value{}; };
template <typename T>
constexpr T __static_const<T>::value;
constexpr const auto& swap = __static_const<detail::swap_func>::value;
}

Reference

看到了Customization Point Objecttag_invoke之後,想要把
Static Polymorphism in C++這個主題總結一下,而這是第一部分

What is Static Polymorphism

基本上可以這樣定義

  • Static polymorphism with overloaded functions and templates that happens at compile time;
  • Dynamic polymorphism with interfaces that happens in run-time.
    而Static的好處在於Compile time就能知道該呼叫哪個函數,沒了Virtual table和runtime的開銷,有機會inline optimization

The simplest form - Class Template

最著名的粒子大概就是std::hash了

1
2
3
4
5
6
7
8
9
10
11
12
13
struct Foo {};

namespace std {
template <>
struct hash<Foo>
{
size_t operator()(const Foo& x) const
{
/* your code here */
}
};
}
std::unordered_map<Foo, int> map;

大概就是這個樣子

  1. 定義自己的類型
  2. 打開Library的namespace,填入需要的運重是馬
    至於優缺點嗎
    優點:
  • 只有一個參數,和一個functor operator,理解起來不困難
    缺點:
  • 需要汙染Library的namespace
  • 無法2對Library的default type做override
1
2
3
4
5
6
7
8
9
10
11
namespace std {
template <>
struct hash<double>
{
size_t operator()(const int x) const
{
/* your code here */
}
};
}
std::unordered_map<double, int> map;

這樣就直接死給你看

  • 要包含整個definition,不能只包含forward declaration

Extened Class Template - Class selection

先定義Primary template definition

1
2
3
4
5
6
namespace test {
template <typename T, typename C = void>
struct get {
int operator()() { return 42; }
};
}

FULL SPECIALIZATION

跟上面的範例差不多

1
2
3
4
5
6
namespace test {
template <>
struct get<double> {
int operator()() { return 66; }
};
}

PARTIAL SPECIALIZATION

1
2
3
4
5
6
namespace test {
template <typename T>
struct get<T, std::enable_if_t<std::is_arithmetic_v<T>>> {
int operator()() { return 11; }
};
}

SPECIALIZATION COLLISIONS

一旦我們加入了一組SPECIALIZATION

1
2
3
4
5
6
namespace test {
template <typename T>
struct get<T, std::enable_if_t<std::is_integral_v<T>>> {
int operator()() { return 33; }
};
}

就爆炸了,由於有兩組class符合條件,不知道選哪個好
比起單參數的方案,變得更複雜,更難維護
之前單參數的缺點它都有,還加上一連串的問題
增加的問題比解決的問題還多,不建議使用

Reference

C++Now 2019: JeanHeyd Meneide “The Plan for Tomorrow: Extension Points in C++ Applications”

Why Concepts?

實例分析

Function overload by type

當我們需要一個函數toString,傳入一個參數,Pseudo Code大概是這樣

1
2
3
4
5
6
7
8
9
template <typename T>
std::string toString(const T& input)
{
if (input have toString member function) {
return input.toString();
} else {
return "unknown format";
}
}

該怎麼做

C++98 Way

看看就好, 這手法現在也不需要理解了, 太舊了

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
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template <typename U, U> struct really_has;

template <typename C> static Yes& Test(really_has <std::string(C::*)() const,
&C::toString>*);

template <typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template<bool B, class T = void> // Default template version.
struct enable_if {}; // This struct doesn't define "type" and the substitution will fail if you try to access it.

template<class T> // A specialisation used if the expression is true.
struct enable_if<true, T> { typedef T type; }; // This struct do have a "type" and won't fail on access.

template <typename T>
std::string toString(const T& input, typename enable_if<has_toString<T>::value, int>::type t = 0)
{
return input.toString();
}

template <typename T>
std::string toString(const T& input, typename enable_if<!has_toString<T>::value, int>::type t = 0)
{
return "unknown format";
}

我想你不會懷念他的
幾個缺點
– 沒有type_traits,所以enable_if之類的要自己寫
– 每增加一個Signature偵測,就要多個類似has_toString的東西出現
– Function overload的signagure要修改
– 奇醜無比

C++11 Way

僅列出跟C++98不同的地方

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
template <typename T>
class has_toString {
private:
typedef char Yes;
typedef Yes No[2];

template<typename C> static auto Test(void*)
-> decltype(std::string{ std::declval<C const>().toString() }, Yes{});

template<typename> static No& Test(...);

public:
static bool const value = sizeof(Test<T>(0)) == sizeof(Yes);
};

template <typename T, typename std::enable_if<has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return input.toString();
}

template <typename T, typename std::enable_if<!has_toString<T>::value, int>::type t = 0>
std::string toString(const T& input)
{
return "unknown format";
}

基本上差不了多少,不過現在不用修改toString的Signature,順眼多了
C++14之後的更新基本上不可用,MSVC不支援,就不列出了

Boost Hana Solution

1
2
3
4
5
6
7
8
9
10
auto has_toString = boost::hana::is_valid([](auto&& obj) -> decltype(obj.toString()) { });

template <typename T>
std::string toString(T const& obj)
{
return boost::hana::if_(has_toString(obj),
[] (auto& x) { return x.toString(); },
[] (auto& x) { return "unknown format"; }
)(obj);
}

這基本上就很接近我們的Pseudo code,不過還要額外的Hana dependency

C++17

C++14之後的更新基本上不可用,MSVC不支援,就不列出了
不過我們可以用if constexpr把兩個 toString合併成一個

Think more

我們只是檢查一個條件toString,如果有復合的條件約束的話該怎麼辦, 假設我們還要檢查一個ver的member variable或者這個typeu定義了一個value_type,上面的方式就變得無力

C++ 20 Concept Solution

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
template <typename T>
concept has_toString = requires(const T &t) {
typename T::value_type;
{ t.ver } -> int;
{ t.toString() }->std::same_as<std::string>;
};

template <typename T>
std::string toString(const T& input)
{
if constexpr (has_toString<T>)
return input.toString();
else
return "unknown format";
}

配合C++17的if constexpr,很漂亮的解決了上述的難題

Debug Template Code

在寫Generic template code的時候,往往要跟Error message打交道
有了Concept約束之後,比較容易觀測出到底哪裡錯了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
template<typename T>
concept have_value = requires(T a) {
a.value;
};

template<typename T>
requires have_value<T>
auto get_value(const T& a)
{
auto v = a.value;
// do some thing
return v;
}

int main()
{
int b = 10;
auto y = get_value(b);
}

錯誤訊息如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<source>:2:1: error: 'concept' does not name a type
2 | concept have_value = requires(T a) {
| ^~~~~~~

<source>:2:1: note: 'concept' only available with '-fconcepts'
<source>:7:1: error: 'requires' does not name a type
7 | requires have_value<T>
| ^~~~~~~~

<source>: In function 'int main()':
<source>:18:12: error: 'get_value' was not declared in this scope
18 | auto y = get_value(b);
| ^~~~~~~~~

Compiler returned: 1

很明顯的我們的Type不符合Concept的要求
這個範例簡單到就算用原先的方式,有經驗的Porgrammer一樣能夠找出錯誤
如果你的Code是一層一層的Template累加起來,就知道這東西多有用了

How to use Concept

Four Ways

Requires Clause

1
2
3
template <typename T>
requires std::integral<T>
void log(T&& x);

Trailing Requires Clause

1
2
template <typename T>
void log(T&& x) requires std::integral<T>

雖然比起第一種方式顯得冗長,不過有種特異功能第一種方式做不到

1
2
template <typename T>
void log(T&& x) requires std::integral<decltype(x)>

由於Trailing Requires Clause可以看到整個Function signature, 因此可以使用decltype做處理

Constrained Template Parameters

1
2
template <std::integral T>
void log(T&& x)

很顯然的, T這個類型必須滿足std::integral的約束
這方案還有一種好處,在不支援C++20的Compilier,只要把std::integral換成typename
就能照常編譯了,不過享受不到Concept的好處
不過可以在C++20檢查過之後Poring回Pre-C++20的Compiler

Abbreviated function template

顧名思義,這種方式只能在 Function template上用,是一種語法糖
看看以下這個案例

1
2
3
4
Arithmetic auto sub(Arithmetic auto fir, Arithmetic auto sec) 
{
return fir - sec;
}

基本上等價於Constrained Template Parameters

1
2
3
4
5
template <Arithmetic T, Arithmetic T2>
auto sub(T fir, T2 sec)
{
return fir - sec;
}

比起上面的寫法,連template宣告都省下來了,不過上面阿寫法可以蛻化為Pre-C++20,這不行

Function overload

Concept允許function overload,比起一般的template function, 通過concept檢驗的function會優先採用
例如

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
void overload(auto t){
std::cout << "auto : " << t << std::endl;
}

template <typename T>
requires std::is_integral_v<T>
void overload(T t){
std::cout << "Integral : " << t << std::endl;
}

void overload(long t){
std::cout << "long : " << t << std::endl;
}

int main(){

std::cout << std::endl;

overload(3.14); // (1)
overload(2010); // (2)
overload(2020l); // (3)

std::cout << std::endl;

}

Customize Concept

The simplest concept

Concept就是Compile-time的boolean expression

1
2
template <typename T>
concept superfluous = true;

既然是個boolean expression,就能做所有boolean operator支援的操作

1
2
3
4
template <typename T>
concept integral = std::is_integral_v<T>;
template <typename T>
concept number = std::integral<T> || std::floating_point<T>;

More complext concept

定義一個能支援遞增的Concept

1
2
3
4
5
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x};
};

定義能夠進行四則運算的Concept, 注意這邊需要兩個Type

1
2
3
4
5
6
7
8
template <typename X, typename Y>
concept they_are_mathsy = requires(X x, Y y)
{
{ x * y };
{ x / y };
{ x + y };
{ x - y };
};

對Return type有要求的Concept

1
2
3
4
5
6
template <typename T>
concept you_can_increment_it = requires(T x)
{
{++x} -> std::same_as<T>;
{x + x} -> std::convertible_to<int>;
};

檢查type是否有member structure和member function

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T>
concept its_a_dragon = requires(T x)
{
// this type trait must be able to be instantiated
typename dragon_traits<T>;

// T has a nested typename for some pointer idk use your imagination
typename T::dragon_clan_ptr;

{x.breathe_fire()};
{x.dragon_breath_firepower()} -> std::convertible_to<uint>;
};

Concept all the thing

當然,你也可以在其他地方加上Concept

1
Integral auto integ = getIntegral(10);

你的return value自然就要符合Integral的約束,幫助我們在Code Maintenance和Refactoring中得到優勢

Conclusion

比起C++Module和C++Coroutine改變遊戲規則的重要性比起來,Conceptˋ只能算是錦上添花,不過對Library developer來說還是有用的

Reference

Is it possible to write a template to check for a function’s existence?
Notes on C++ SFINAE
An introduction to C++’s SFINAE concept: compile-time introspection of a class member
Substitution Failure is Error and Not An Error.
C++20: Concepts, the Details
C++20 Concepts
C++20: Define Concepts

雖然這東西沒什麼技術含量,不過流程也是要自己跑過一遍才會有記憶點,如果只看過沒跑過很快就忘
現代的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的基本觀念才比較容易理解