C++: Variadic Template and Fold Expression are Powerful.

这两天做一个Pipeline的Framework,见识了现在C++ Generic Programming 的强大功能,比原来的 Object Oriented Programming 看上去 Cool 多了:完全没有关系的一堆类型,一样可以放在框架里操作,写出来的程序也没有任何效率的损失,看来这些年C++又进步了很多。下面简要说说我的设计。

Class BaseData 表示 Pipeline 中的一个数据节点。 Class Phase 表示针对数据的某个操作。 Phase 接受一些 BaseData 作为输入,并输出到另外一些BaseData。为了灵活和简单起见,Phase 不设任何 Virtual Method 作为接口,只要就实现一个 Run Method 即可。程序概要如下:

class BaseData {
public:
 get() ...
 set() ...
};

class DataA : public BaseData {...};
class DataB : public BaseData {...};
class DataC : public BaseData {...};
class DataD : public BaseData {...};

class Phase {};

class PhaseA : public Phase {
  void Run(DataA*, DataB*, DataC*) {...}
};
class PhaseB : public Phase {
void Run(DataC*, DataD*) {...}
};

class Pipeline {
private:
  DataA a_;
  DataB b_;
  DataC c_;
  DataD d_;

public:
  Run() {
    AddPhase(new PhaseA(), &a_, &b_, &c);
    AddPhase(new PhaseB(), &c_, &d_);
  }

  void AddData(BaseData* base_data) {
     ...
  }

  // This is variadic template.
  template<typename PhaseType, typename... Types>
  AddPhase(PhaseType* phase, Types args...) {

    // This is fold expression, avoid template recursion.
    (..., AddData(args));

    // This is variadic forward.
    std::function<void()> run = 
        std::bind(&PhaseType::Run, phase, args...);
  }
};

程序的精妙全在最后的AddPhase 这个函数,它首先通过一个 Variadic Template 来处理任意接口的函数,然后又通过一个 Fold Expression 对所有的数据进行了类型检查和处理,最后又使用 variadic forward 创建了一个 Closure 供以后调用,真是妙不可言啊。

 

Use Mouse in Terminal Vim like a modern editor

现代的编辑器(Ultraeditor, Editplus) 对于鼠标的使用非常频繁,最常见的是使用鼠标单击来移动光标,鼠标滚轮来上下翻页,或者鼠标双击选中某个词,然后拷贝或者粘贴替换。我最近学到了如何在Terminal-Based Vim 中也这样操作。

首先设置Vim 在 Normal Mode 和 Visual Mode 下激活鼠标:

set mouse=nv

这样我们就可以用鼠标单击或者滚轮移动光标了。另外,鼠标双击选中单词也正常工作了。

值得一提的是,我们也可以用键盘来选中一个词:假定我们的光标停在 information 这个词上(任何位置都可以),按下

viw

就可以选中整个词,这个操作不需要鼠标,在某些情况下很方便。

接下来的事情就容易了,拷贝:

y

粘贴并替换当前选中的词:

p

 

编译 Vim (Compile Vim Wiht Python 3 Support on Debian)

下载源文件

$ git clone https://github.com/vim/vim.git
$ cd vim/src
$ vi Makefile

编辑Makefile

  • 选择编译器
# COMPILER - Name of the compiler {{{1
# The default from configure will mostly be fine, no need to change this, just
# an example. If a compiler is defined here, configure will use it rather than
# probing for one. It is dangerous to change this after configure was run.
# Make will use your choice then -- but beware: Many things may change with
# another compiler. It is wise to run 'make reconfig' to start all over
# again.
#CC = cc

# For some reason, gcc does not install itself as /usr/bin/gcc
CC = gcc-9
#CC = clang

if gcc is not installed, installing it by running

$ sudo apt update
$ sudo apt install build-essential
  • 关闭图形
# GUI - For creating Vim with GUI (gvim) (B)
# Uncomment this line when you don't want to get the GUI version, although you
# have GTK, Motif and/or Athena. Also use --without-x if you don't want X11
# at all.
CONF_OPT_GUI = --disable-gui

# X WINDOWS DISABLE - For creating a plain Vim without any X11 related fancies
# (otherwise Vim configure will try to include xterm titlebar access)
# Also disable the GUI above, otherwise it will be included anyway.
# When both GUI and X11 have been disabled this may save about 15% of the
# code and make Vim startup quicker.
CONF_OPT_X = --without-x
  • 支持  Python3

安装 Python3 库文件

$ sudo apt install python3.8 python3.8-dev python3-distutils

静态链接 Python 解释器。

# PYTHON
# Uncomment lines here when you want to include the Python interface.
# This requires at least "normal" features, "tiny" and "small" don't work.
# NOTE: This may cause threading to be enabled, which has side effects (such
# as using different libraries and debugging becomes more difficult).
# For Python3 support make a symbolic link in /usr/local/bin:
# ln -s python3 python3.1
# If both python2.x and python3.x are enabled then the linking will be via
# dlopen(), dlsym(), dlclose(), i.e. pythonX.Y.so must be available
# However, this may still cause problems, such as "import termios" failing.
# Build two separate versions of Vim in that case.
#CONF_OPT_PYTHON = --enable-pythoninterp
#CONF_OPT_PYTHON = --enable-pythoninterp --with-python-command=python2.7
#CONF_OPT_PYTHON = --enable-pythoninterp=dynamic
#CONF_OPT_PYTHON3 = --enable-python3interp
#CONF_OPT_PYTHON3 = --enable-python3interp --with-python3-command=python3.8
#CONF_OPT_PYTHON3 = --enable-python3interp=dynamic

CONF_OPT_PYTHON3 = --enable-python3interp --with-python3-command=python3.8 --with-python3-config-dir=/usr/lib/python3.8/config-3.8-x86_64-linux-gnu

编译和安装

$ make

$ sudo make install

清理一切,从头开始

$ make distclean

 

第一代之后,基因型不再变化(Stationary GenoType Distribution)

基因 A 和 a是一对等位基因,在人群中构成了三种基因型:AA, Aa 和 aa,其中A是显性,a是隐性。在某些遗传性状比如蓝眼睛、左撇子等,基因型决定了实际表现的性状,假定A代表右撇子,a代表左撇子,那么Aa和AA都表现为右撇子,只有aa表现为左撇子。

假定这三种基因型在男性和女性中的分布概率是一样的,分别是 u, 2v, w,那么我们有:

(1)   \begin{equation*} \begin{split} P(AA) = u \\ P(Aa) = 2v \\ P(aa) = w \\ u + 2v + w = 1 \end{split} \end{equation*}

如果我们用p代表基因A在人群中的概率,我们有:

(2)   \begin{equation*} P(A) = p = u + v \end{equation*}

同样,我们用q代表基因a在人群中的概率,我们同样有:

(3)   \begin{equation*} P(a) = q = w + v \end{equation*}

因为我们假定男性和女性的基因型分布是一样的,那么对于下一代子女来说,因为他(她)们的基因一半来自父亲,另一半来自母亲,所以他们之中纯合基因型AA发生的概率就是继承自父亲和母亲的基因都是A的概率相乘,也就是:

(4)   \begin{equation*} u_1 = p^2 = {(u+v)}^2 \end{equation*}

根据同样的道理,子女代中基因型Aa和aa发生的概率分别是:

(5)   \begin{equation*} \begin{split} 2v_1 = 2pq = 2 (u+v)(w+v) \\ w_1 = q^2 = {(w+v)}^2 \end{split} \end{equation*}

上面的公式中我们依然使用 u, v, w 代表 基因型AA,Aa和 aa 的概率,但是使用下标1来表示这是子女代。有了每种基因型的概率之后,我们可以知道子女代中实际基因A和a的分布概率是:

(6)   \begin{equation*} \begin{split} p_1 = u_1 + v_1 \\ q_1 = w_1 + v_1 \end{split} \end{equation*}

那么再下一代的三种基因型的概率分别是多少呢,同样我们可以简单计算如下:

(7)   \begin{equation*} u_2 = p_1^2 = {(u_1 + v_1)}^2 = u_1^2 + 2u_1v_1 + v_1^2 \end{equation*}

(8)   \begin{equation*} \begin{split} &2v_2 = 2 p_1 q_1 \\ &= 2(u_1 + v_1)(w_1 + v_1) \\ &= 2u_1w_1 + 2 u_1v_1 + 2v_1 w_1 + 2v_1^2 \end{split} \end{equation*}

(9)   \begin{equation*} w_2 = q_1^2 = {(w_1 + v_1)}^2 = w_1^2 + 2w_1v_1 + v_1^2 \end{equation*}

同样,第二代中基因A和a的概率分别是:

(10)   \begin{equation*} \begin{split} p_2 &= u_2 + v_2 \\ &= (u_1^2 + 2u_1v_1 + v_1^2) + \\ &\quad (u_1w_1 +  u_1v_1 + v_1 w_1 + v_1^2) \end{split} \end{equation*}

(11)   \begin{equation*} \begin{split} q_2 &= w_2 + v_2 \\ &= (w_1^2 + 2w_1v_1 + v_1^2) + \\ &\quad (u_1w_1 +  u_1v_1 + v_1 w_1 + v_1^2) \end{split} \end{equation*}

计算了这么拉拉杂杂一大堆,到底有什么用处呢?下面是关键的一步,因为我们有:

(12)   \begin{equation*} p + q = u + 2v + w = 1 \end{equation*}

显而易见:

(13)   \begin{equation*} q = 1 - p \end{equation*}

我们很容易推导出上一代的基因A的概率和下一代基因A的概率关系如下:

(14)   \begin{equation*} p_1 = u_1 + v_1 = p^2 + pq = p^2 + p(1-p) = p \end{equation*}

同样的道理,我们也可以看到:

(15)   \begin{equation*} \begin{split} q_1 = q \\ p_2 = p_1 \\ q_2 = q_1 \\ p_3 = p_2 \\ q_3 = q_2 \\ ... \end{split} \end{equation*}

上式说明,在理想的情况下(基因A和a在男女中比例一致,每一个人都有同样的机会生育下一代,下一代的生男孩女孩的概率一致),每一代中基因A和a的分布实际上没有任何变化,这也符合我们关于遗传的直觉。但是基因型AA,Aa和aa的概率就不是这样了。一般情况下:

(16)   \begin{equation*} \begin{split} u_1 \ne u \\ v_1 \ne v  \\ w_1 \ne w \end{split} \end{equation*}

但是

(17)   \begin{equation*} \begin{split} u_1 = u_2 \\ v_1 = v_2  \\ w_1 = w_2 \\ u_2 = u_3 \\ v_2= v_3  \\ w_2 = w_3 \\ ... \end{split} \end{equation*}

这说明实际的基因型在第一代之后就稳定下来,不再变化。回到我们前面左撇子的例子:在理想情况下,第一代和第二代的左撇子比例(基因型,u,v,w)可能不一样,但是他们中的左撇子基因(p,q)实际是一致的,从第二代以后,基因型的比例也稳定下来,不再变化。

C++ 的一些新感想

因为在工作中C++用的比较多,听说 C++ 17 最近又引入了很多新特性,就买了几本 C++ 的书回来看。看了一阵,感慨颇多,罗列如下:

  • C++ 在 Library 开发人员和普通使用人员之间的鸿沟是越来越大了。新增加的很多特性都是给Library作者提供的,普通用户平时根本用不到。特别的,我以前总是以为所谓 Library 就是在语言之上提供一些包装好的功能模块,现在看到标准库中很多 Type Traits (比如 is_trivially_destructible) 都需要编译器的特殊支持才能实现,远远地超出了我以前的理解范畴。一般的 C++ 用户,如果打开标准库里面的程序想要研究一下某个特性是如何实现的,多半和天书一样看不懂。
  • C++ 现在的救命稻草就是运行时刻的性能了,这集中体现在C++为程序员提供的内存控制上。为此 C++ 几乎牺牲了其他可能的前进方向,并且不惜在语言上增加了巨大的复杂性。C++ 的创始人 Bjarne Stroustrup 前些年还提要在 C++ 中引入自动内存管理,现在是绝口不提了,估计搞出来也没有人用;为了更有效的支持大型对象,C++ 搞出来 Rvalue Reference 和 Move Constructor,让我这样的 C++ 的老鸟程序员也感到太过复杂;更不用提新的 std::string_view,本身大概唯一的作用就是在大字符串上搞一些子串操作比较有效率,结果强行塞进标准库,连 C++ 的资深作者 Nicolai M. Josuttis 都看不下去,在他的书中大声疾呼:string_view is considered harmful! 即便如此追求性能,看着 Programming Language Popularity Trend 上面 C++ 日渐下滑的曲线,我估计日后 C++ 也就只能守着高性能计算这块阵地了。
  • C++ 现在把编译时刻多态搞得很深,这一点其实和 C++ 把性能作为卖点也息息相关。原本是一个编译时刻模板替换的简单玩意,后来发现这个东西因为没有 Virtual table lookup 从而不损失运行时刻的效率,结果在这个方向上一发不可收拾,各种 type traits 如野草般生长,Library 全部都是模板库,里面根据用户提供的类型各种模板特化层出不穷,对于一般的 C++ 程序员来说根本就是天书,唯一的好处就是运行时刻生成的代码都是直接针对用户提供的类型操作的,效率很高。这方面C++阵营一直使用的例子就是 std::sort vs qsort from C。不过在我看来,最初的简单模板替换就已经完成了 90% 的工作,后来加上的一大堆特性,最多不过使得这个编译时刻多态(现在都叫 meta programming了,据称在 Type Domain 还是 Turing Complete)能够在其他 10% 的场合使用一下,实在算是事倍功半的一个好例子吧。

自己呢,还是好好的搞自己的数学,统计,概率,算法等等。这类语言的东西实在不值得深究啊。

戒掉围棋,立此为证(Swear Off Go Game)

下棋劳神费力,导致头疼,这个年纪已经不再合适。

下棋远离家人,却贴近陌生人,本末倒置。

下棋浪费时间,大好光阴本可用于学习与工作。

下棋所争无非输赢,虚荣好胜,引以为耻。

从此把棋盘、棋子、棋书束之高阁;新闻、视频、论坛关禁了事。

立此为证,敬请家人朋友监督。

Playing Go consumes a lot of brain power, causes headache, which is inappropriate for my age.

I spend a lot of time on playing Go with strangers, which could instead be spent with family.  This behavior puts the incidental before the fundamental.

A lot of time is spent on Go playing, which could be used on learning  and studying other interesting things.

The whole point of playing Go on Web is for winning vanity, this should be a shame for any noble men.

Putting the stones, boards and books on attic space, blocking the news, videos and forums on computers.

Family, please help me to keep this promise.

对抗假新闻

现在互联网上的假新闻越来越多,有一些是无心的以讹传讹,但是也有很多是别有用心的杜撰,专门用来混淆视听的。传统的大媒体比如BBC、卫报还好一些,论坛、自媒体以及社交网络都是重灾区。关于新型冠状病毒的假新闻,过去的这六个月来就遇到了很多,比如有假新闻说英国首相鲍里斯在感染新冠之后,虽然勉强活命,但是身体严重受损,无法坚持工作,不得不辞去首相职位。最新的一则是前一天有报道说俄罗斯的疫苗研制成功,普京总统亲自下令全民接种,自己的大女儿以身作则,率先尝试。这消息已经相当使人震惊,毕竟疫苗是人命关天的大事,俄罗斯在疫苗研制方面的火箭速度实在是不同寻常。可是隔天就又有新的消息,说俄罗斯疫苗研制失败,普京的大女儿已经因为接种疫苗的不良反应挂了!!!  这消息也太过耸人听闻了吧,实在让人难以相信。联想起前一阵子一个中文自媒体播报说末任香港总督彭定康已经向海牙国际法庭提交诉讼,要把香港的主权重新交还给英国,我当时也是大吃一惊,最后也悄无声息,又是一个假新闻。

互联网上的假新闻实在是太多了,干脆眼不见心不烦,专注于自己的家人、学习、工作吧。

Vim Window Management (Vim 多窗口管理)

我一直使用Vim作为编辑器。现在大屏幕显示器越来越流行,使用Vim的时候也越来越多地同时用到多个窗口。为了能够更有效地利用多个窗口,我琢磨着写了一些脚本来更好地在多个窗口之间切换,下面把我学到的知识说一说。

首先我不会写 Vim Script,所以我的脚本都是用 Python写的,不过 Vim的 Python Interface 也很方便,就把脚本写在一个单独的 window.py 文件中,然后在 vimrc 里面加上:

py3file window.py

就可以了。

要想有效的管理窗口,首先需要能够了解当前的窗口分布情况。Vim 的 Python Interface 提供了一个 vim.windows 对象,不过更好的办法是调用 winlayout 这个函数。因为 Vim 中所有的窗口都是从一个主窗口水平或者垂直切分得到的,所以所有的窗口可以表示为一棵递归二叉树。遍历这棵树就可以得到所有窗口的信息,包括其编号、大小,当前位置,父窗口以及其中显示的数据。具体的调用方法如下:

winlayout = vim.eval('winlayout()')

第二个非常有用的命令是 wincmd。Vim提供的绝大部分窗口管理命令都从快捷键 CTRL+W 开始,但是这个快捷键在脚本中使用起来并不方便,这个时候就可以使用wincmd。比如我们要把输入焦点切换到左面的窗口,可以使用:

vim.command('wincmd l')

最后就是创建新窗口和关闭当前窗口的命令,这方面Vim提供了非常繁多的命令,但是对于脚本作者来讲最实用的也就是下面几个:

  • 关闭当前窗口:vim.command(‘close’)
  • 关闭所有其他窗口,只留下当前窗口:vim.command(‘only’)
  • 垂直切分在右面打开新窗口:vim.command(‘rightbelow vnew’)
  • 垂直切分在左面打开新窗口:vim.command(‘leftabove vnew’)
  • 水平切分在下面打开新窗口:vim.command(‘rightbelow new’)
  • 水平切分在上面打开新窗口:vim.command(‘leftabove new’)

有了这些命令以后,我们就可以编写脚本来控制Vim的多个窗口了。我自己写了一个脚本用于管理两个Side by Side 放置的窗口,再加上一些水平切分的支持,结果挺方便的,以后有机会放到 Github 上吧。

等位基因的组合数目

假定某种形状(比如豌豆花的颜色)由一对等位基因控制,其中显性基因用字母A表示,隐性基因用字母a表示。显而易见,我们一共有三种可能的基因型:AA, Aa, aa,分别对应红色、粉色和白色。如果某个等位基因一共有n种可能的变异,即:

(1)   \begin{equation*} A_1, A_2, A_3, ..., A_n \end{equation*}

那么一共有多少种可能的基因型呢(注意基因型A_nA_mA_mA_n 是一样的)?

这个问题看上去很简单,但是我自己却迷惑了很长一段时间。最后我终于认识到:在阅读 William Feller 老爷爷的概率书的这段时间里,自己在经过了相当多的训练之后,已经习惯性的去考虑样本空间内每一个样本的概率是相等的情形了,从这一点讲,那么很明显我们一共有 n \times n 种基因组合的结果。但是问题是这些组合虽然每一个出现的概率相等,但是其中却有一些重复;等到我们把重复的合并以后,剩下的样本出现的概率却不相等了。具体来讲,纯合的基因型:

(2)   \begin{equation*} (A_1, A_1),  (A_2, A_2), (A_3, A_3), ...,  (A_n, A_n) \end{equation*}

要比杂合的基因型

(3)   \begin{equation*} (A_1, A_2),  (A_1, A_3), (A_1, A_4), ...,  (A_{n-1}, A_n) \end{equation*}

出现的概率要低。具体到豌豆花的例子,也就是白色和红色出现的概率分别是 25%,而粉色出现的概率是50%。在领悟到这一点之后,前面给出的题目就很容易计算了:当某个等位基因一共有n种可能的变异的时候,我们可以把所有的可能的组合排成一个 n \times n的表格,其中对角线上的基因型是纯合的,一共有n个,而非对角线上的基因型是杂合的,一共有 n^2 - n 种。考虑到杂合的基因型每种都会出现两次,那么所有不同的基因型一共有:

(4)   \begin{equation*} \frac{n^2 - n}{2} + n = \frac{n(n+1)}{2} \end{equation*}

这和William Feller 老爷爷书中给出的答案完全一致。

音乐欣赏的两个对比

以前自己都是只会打开音响听音乐,自从自己开始弹琴以后,才慢慢悟出来音乐欣赏这里面还有两个有趣的对比:第一,同样一首歌曲,听别人弹和听自己亲手弹出来的感觉,后者带来的愉悦要大得多,听着旋律与和声跟随着自己手指的动作流淌出来,即使是很简单的曲目,也很享受,要是别人弹,可能就会觉得这旋律很简单,没什么特别的。第二,钢琴想要弹到别人觉得好听要比自己觉得好听难得多,常常一段旋律自己弹得摇头晃脑,乐在其中,可是家人已经听过无数遍了,早已听得滚瓜烂熟,耳朵起茧了。

看来要想领会音乐的妙处,自己动手实践是一个好办法呀。