状态机是很棒的工具

2021-01-01 20:15:53

我喜欢用状态机解决当前的问题。他们拒绝设计和实施,而且我对正确性充满信心。他们倾向于:

状态机也许是您听说过的关于大学的概念之一,但从未付诸实践。也许您经常使用它们。无论如何,您肯定会定期从常规表达式到交通信号灯碰到它们。

受一个难题的启发,我想到了这个确定性状态机来解码摩尔斯电码。它一次接受一个点('。'),破折号('-')或终止符(0),并逐步进入状态机:

int morse_decode(int state,int c){静态const unsigned char t [] = {0x03,0x3f,0x7b,0x4f,0x2f,0x63,0x5f,0x77,0x7f,0x72,0x87,0x3b,0x57,0x47,0x67 ,0x81,0x40,0x01,0x58,0x00,0x68,0x51,0x32,0x88,0x34,0x8c,0x92,0x6c,0x02,0x03,0x18,0x14,0x00,0x10,0x00,0x00,0x00,0x0c,0x00 ,0x00,0x00,0x00,0x00,0x00,0x08,0x1c,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x20,0x00,0x00,0x00,0x24,0x00,0x28,0x04,0x00,0x30 ,0x32,0x33,0x34,0x35,0x36,0x37,0x38,0x39,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b,0x4c,0x4d,0x4e,0x4e ,0x52,0x53,0x54,0x55​​,0x56,0x57,0x58,0x59,0x5a}; int v = t [-state];开关(c){case 0x00:return v>> 2? t [(v> 2)+ 63]:0;情况0x2e:返回v& 2?状态* 2-1:0;情况0x2d:返回v& 1个状态* 2-2:0;默认值:返回0; }}

它通常可编译到200个字节以下(包括表),只需要很少的字节内存即可运行,甚至可以装在最小的微控制器上。完整的源代码清单,文档和全面的测试套件:

状态机是特里形状的,而100字节的表t是莫尔斯电码特里的静态编码:

点向左移动,向右虚线,端子在当前节点上发出字符(端子状态)。在红色节点上停止或尝试采用未列出的边缘是错误(无效输入)。

特里树中的每个节点都是表中的一个字节。点和破折号都有一个表明其边缘是否存在的位。剩余的位索引到一个基于1的字符表中(在t的末尾),0的“索引”指示一个空(红色)节点。节点本身以二进制数组的形式排列在一个数组中:i处节点的左右子节点位于i * 2 + 1和i * 2 + 2。无需浪费存储边缘的内存!

由于C遗憾地没有多个返回值,因此我使用返回值的符号位来创建一种求和类型。负的返回值是一种状态-这就是为什么在使用该状态之前对其进行内部求反的原因。肯定的结果是字符输出。如果为零,则输入无效。只有初始状态为非负数(零),这很好,因为根据定义,它不可能遍历初始状态。没有c输入将产生不良状态。

在最初的问题中,缺少接线端子。尽管是状态机,但morse_decode是一个纯函数。调用者可以通过保存状态整数并尝试与该状态不同的输入来保存它们在trie中的位置。

经典的UTF-8解码器状态机是Bjoern Hoehrmann的灵活且经济的UTF-8解码器。它使用巧妙的技巧将整个状态机打包到一个较小的表中。这是我最喜欢的UTF-8解码器。

我想亲自尝试一下,所以我重新推导了相同的canonicalUTF-8自动机:

然后,我将此图直接编码到一个更大(2,064字节),不太优雅的表中,该表太大而无法在此处显示内联:

但是,要权衡的是可执行代码更小,更快,并且再次无分支(我发誓,我偶然!):

int utf8_decode(int state,long * cp,int byte){静态const签名的字符表[8] [256] = {/ * ... * /};静态无符号字符掩码[2] [8] = {/ * ... * /}; int next =表[状态] [字节]; * cp =(* cp<< 6)| (byte& masks [!state] [next& 7]);接下来返回; }

就像Bjoern的解码器一样,有一个代码点累加器。真实的状态机具有1,109,950个终端状态,以及更多的边和节点。累加器是一种优化,可以精确地跟踪将哪个边带到了哪个节点,而不必表示这种怪异。

这是我不久前想到的另一种状态机,用于一次计算一个Unicode代码点的单词数,同时考虑到Unicode的各种空白。如果您输入的是字节,则将其插入上述UTF-8状态机,以将字节转换为代码点!由于表是稀疏的(因此让编译器找出来),因此该表使用了一个switch而不是一个查找表。

/ *状态机对代码点序列中的单词进行计数。 * *当前字数是状态的绝对值,因此*初始状态为零。代码点一次进入一个状态机,每个调用返回下一个状态。 * / long word_count(长状态,长代码点){开关(代码点){情况0x0009:情况0x000a:情况0x000b:情况0x000c:情况0x000d:情况0x0020:情况0x0085:情况0x00a0:情况0x1680:情况0x2000:情况0x2001:情况0x2002:情况0x2003:情况0x2004:情况0x2005:情况0x2006:情况0x2007:情况0x2008:情况0x2009:情况0x200a:情况0x2028:情况0x2029:情况0x202f:情况0x205f:情况0x3000:返回状态< 0?状态:状态;默认值:返回状态< 0?状态:-1状态; }}

我对边缘触发的状态转换机制特别满意。状态的符号跟踪“信号”是“高”(单词内部)还是“低”(单词外部),因此它计算上升沿。

从技术上讲,该计数器不是状态机的一部分-尽管由于实际原因最终会溢出,但它并不是真正的“有限”-而是状态机从低到高转变的次数的外部计数,这是实际的有用输出。

读者挑战:找到一种巧妙,高效的方法将所有这些代码点编码为表,而不是依赖于编译器为开关生成的任何内容(分支链,跳转表?)。

在支持它们的语言中,可以使用协同程序(包括生成器)来实现状态机。我特别喜欢将编译器综合程序作为状态机的想法,尽管这是一种罕见的做法。状态在每个成品率的协程中都是隐式的,因此程序员不必显式管理它。 (尽管通常显式控制功能强大!)

不幸的是,在实践中它总是感到笨拙。以下实现了单词计数状态机(尽管以一种非Python的方式)。生成器返回当前计数,并通过向其发送另一个代码点继续:

WHITESPACE = {0x0009,0x000a,0x000b,0x000c,0x000d,0x0020,0x0085,0x00a0,0x1680,0x2000,0x2001,0x2002,0x2003,0x2004,0x2005,0x2006,0x2007,0x2008,0x2009,0x200,0x2009,0x200a ,0x3000,} def wordcount():count = 0 while True:while True:#低信号代码点=屈服计数,如果代码点不在WHITESPACE中:count + = 1中断,而True:#高信号代码点=屈服计数,如果代码点在WHITESPACE中:休息

但是,生成器仪式在界面中占主导地位,因此您可能希望将其包装在更好的东西中—在这一点上,实际上没有理由首先使用生成器:

wc = wordcount()next(wc)#启动生成器wc。发送(ord(' A'))#=> 1洗手间。发送(ord(''))#=> 1洗手间。发送(ord(' B'))#=> 2 WC。发送(ord(''))#=> 2

本地WHITESPACE = {[0x0009] = true,[0x000a] = true,[0x000b] = true,[0x000c] = true,[0x000d] = true,[0x0020] = true,[0x0085] = true,[0x00a0] = true,[0x1680] = true,[0x2000] = true,[0x2001] = true,[0x2002] = true,[0x2003] = true,[0x2004] = true,[0x2005] = true,[0x2006] = true, [0x2007] = true,[0x2008] = true,[0x2009] = true,[0x200a] = true,[0x2028] = true,[0x2029] = true,[0x202f] = true,[0x205f] = true,[0x3000 ] = true}函数wordcount()本地计数= 0,而true则是true-低信号本地代码点= coroutine.yield(count)如果不是WHITESPACE [codepoint]则count = count + 1中断结束,而true时- -高信号本地代码点= coroutine.yield(count)如果WHITESPACE [codepoint]则中断end end end end

除了最初启动协程之外,至少coroutine.wrap()隐藏了它是协程的事实。

wc = coroutine.wrap(wordcount)wc()-启动coroutine wc(string.byte(' A'))-=> 1 wc(string.byte(''))-=> 1 wc(string.byte(' B'))-=> 2 wc(string.byte(''))-=> 2 最后,还有几个例子,在这里不值得详细描述。 Firsta Unicode大小写折叠状态机: 它只是一个界面,可以查询官方案例折叠表。 这是一个实验,我可能不会在面试程序中使用它。 其次,我之前提到过我的UTF-7编码器和解码器。 从界面上看不出来,但是在内部它只是用于编码器和解码器的状态机,这使它可以在任何一对输入/输出字节之间“暂停”。 对这篇文章有评论吗? 通过发送电子邮件至~skeeto/[email protected] [邮件列表礼节],在我的公共收件箱中进行讨论,或查看现有讨论。