泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓RISC-V实验箱,转战RISC-V,开箱即用
请稍侯

量子化的 UI

Chen Jie 创作于 2016/02/11

By Chen Jie of TinyLab.org 2016-01-19 21:07:18

引言

在目下大数据时代,海量数据与机器学习,通过理解语境、甚至于用户喜好,人机交互愈来愈 “灵犀相通”。而在具体的人机交互界面上,像 小冰 和 Siri 这样的语音助手,所提供功能也越来越丰富。于是,人机交互也从图形化界面技术,继续迈步向前。

2015.11 第三期的泰晓资讯中,提到了 LWN 转载文章「No UI is some UI」。同时,本站「移动终端 UI 设计新思路:从图形界面回归“命令行”?」 提出了聊天风格主导的手机系统界面。用户通过 简单格式的语句,或甚至是自然语言,来完成交互。而「大开脑洞,做一个比表屏大,比 phone 便携的移动设备」一文, 展示了 输入法协助用户生成指令 ,来方便聊天风格的交互。(如何用聊天风格 UI 来满足业务交互?贩一个“私货”:Logging and Tickets。通过交互逐步确定具体的目标,交互过程叫做“Logging”,确定下来的目标叫做“Tickets”。比如,准备出国游去移动营业厅开通“全球漫游服务”,与服务窗口 mm 交谈为 Logging;最后开通具体业务,落款的合同为 Ticket。通过 “Logging”,可回溯细节,并智能化推荐和协助。而 Tickets 则按不同线归档 /Ticket Book/,例如按时间线、位置线归档,从而智能化呈现)

那么,UI 技术再发展中,会有哪些新的挑战?或者说有哪些已有的技术可以复用?或者说 UI 泛化以后还留下什么?或者说 —— 到底什么是 UI

Definition: A User Interface (UI) is a system for assisting a user in selecting a function and providing a valid set of parameters to the function.

Definition: A Graphical User Interface (GUI) is a visual and interactive UI.

Sean Parent, 「Adobe Source Libraries Overview & Philosophy」, p49

在 GUI 时代,用户通过与可视元素交互,来选择一组命令及其参数。其中传给程序核心(Core)的命令参数,需确保符合特定的模型(Model),从而是合法的。

image

在文初的例子中,用户通过整合语音语义识别的输入法,或者符合某种格式的输入法,来输入命令和参数,参数同样需符合特定的模型(Model),来确保合法。换言之,不同的 UI 技术,有不同的方式来协助用户选择 命令和参数。但在参数合法性检查上,是共享的。

在量子理论中,从 A 传递能量给 B,是整数个量子,视作其完整性约束。而在 UI 定义中,UI 传递给程序核心(Core)是完整的一组参数,符合模型(Model)是其完整性约束。该种特点,本文特别称之为 量子化的 UI —— 在下文中,将介绍 Adobe Source Libraries 提出“量子化方案” —— 属性模型(Property Model)及其描述语言 Adam。

Adam ∙ 例子

由一个 “缩放图片的对话框” 说起:

image

其中有缩放后宽度 combo 输入框两枚(单位分别是 像素,以及相对原始尺寸的 百分比),同样,缩放后高度也是 combo 输入框两枚。还有一钮,按下后会保持当前的宽高比。

该 UI 的模型(Model),由 Adam 如下描述:

sheet scale_image {
input:
	ratio             : 0.0;
	original_width    : 2304;
	original_height   : 1296;
interface:
	width_pixels      : original_width  <== round(width_pixels);
	width_percent;
	height_pixels     : original_height <== round(height_pixels);
	height_percent;
logic:
	relate {
	   width_pixels   <== round(width_percent * original_width / 100);
	   width_percent  <== width_pixels * 100 / original_width;
	}

	relate {
	   height_pixels  <== round(height_percent * original_height / 100);
	   height_percent <== height_pixels * 100 / original_height;
	}

	when (ratio) relate {
	   width_percent  <== height_percent * ratio;
	   height_percent <== width_percent / ratio;
	}
output: 
	result <== { height: height_pixels, width: width_pixels };
}

这个名为 scale_image 的 sheet,含有若干个 cells(单元)。比如待缩放的图片宽度,由 input cell original_width 描述,为 2304;而 缩放后的宽度由 interface cell width_pixels 描述,其初值为 original_width。而 缩放后宽度 width_pixels 和 宽度缩放比例 width_percent 之关系由 relation cell 给出:

relate {
	width_pixels   <== round(width_percent * original_width / 100);
	width_percent  <== width_pixels * 100 / original_width;
}

可见,relation cell 像是一个 n 元方程式,n 个元的解式。来看看几个情形下,sheet scale_image 中的 relations 怎起的作用:

  • 用户填入缩放后宽度(width_pixels)⟶ width_percent 重新计算。
  • 用户点击锁定宽高比:(set ratio = width_percent / height_percent)。用户再更新 width_pixels ⟶ width_percent ⟶ height_percent ⟶ height_pixels 依次重新计算。

最后,output cell result 是最终输出的结果,上例中其值是个字典{ height: height_pixels, width: width_pixels }

除了上面出场 cell 类型外,还有 invariant cell,其作用相当于断言 —— 若布尔值为假,则所有参与计算的 cells 标记为有毒的(poison)。

cell 的类型参见附录

Adam ∙ 内流透视

在前述例子中,一个 interface cell 有多个计算式,例如 width_pixels,有初值 :original_width,interface cell 中表达式 <== round(width_pixels) 和 relation cell 中的表达式 <== round(width_percent * original_width / 100);

那么 width_pixels 取值到底为多少?计算的步骤是怎样的??

再者,例中的 sheet 是怎样被解析的?比如 width_pixels: original_width <== round(width_pixels) 如何解析为 cell_t 结构体的?还有表达式的数据结构是什么?其值是怎样计算的?

下面先来看如何从 Adam 语言解析生成对应的数据结构,再来看看 cell 的值是如何计算的。对应代码位于:github

解析(Parsing)

先来看看 interface cell width_pixels 是如何解析的?如下图:

image

大体上是词法分析器(lexer)将输入流分割成一个个词素(如 keyword、identifier、iteral),然后再进行解析。其次需说明的是,一个 interface cell 实际上被解成了 两个 cell_t,分别是“input” 和 “out” 两部分。

而表达式的解析是按照运算符的优先级,按由弱到强的顺序:

is_or_expression   // and_expression { "||" and_expression }
                      ^^^^^^^^^^^^^^
   is_and_expression   // bitwise_or_expression { "&&" bitwise_or_expression }
                          ^^^^^^^^^^^^^^^^^^^^^
      is_bitwise_or_expression   // bitwise_xor_expression { "|" bitwise_xor_expression }

       ...

         is_postfix_expression   // primary_expression { ("[" expression "]") | ("." identifier) }
                                    ^^^^^^^^^^^^^^^^^^
            is_primary_expression   // name | number | boolean | string | "empty" |
                                    // array | dictionary | variable_or_fuction |
                                    // ( "(" expression ")" )

表达式解析成数组 array_t,按照后缀表示法添入数组,操作数在前,操作符在后。

函数calculate_expression() / ... / virtual_machine_t::implementation_t::evaluate()来计算表达式的值:遇到操作数压栈value_stack_m,遇到操作符,从栈取出值,并将计算结果放回。

再来看看一个 relation cell 如何解析?例如下图:

image

这里将保持“宽高比(ratio)”时 宽度缩放比 和 高度缩放比的关系(relation),画作一个圈:interface cell height_percent(宽度缩放比) 和 width_percent(高度缩放比)都在这个圈上。

计算 cell 之值

站在 width_pixels 角度,其计算式有 初始化表达式、interface 和 relation cell 中定义的表达式,其值是怎么计算出来的?

对于 interface cell 而言,解析后生成 input 和 output 两 cells:

  • 对于 input 部分的 cell,其值存于成员变量 state_m,或由 sheet_t::implementation_t::set() 来设置;或由构造时,初始化表达式计算而来(函数路径:add_interface()/initialize_one())。该值可能参与:
    • ouput 部分的值计算(见下点)。
    • 其他 interface cell 的初值计算。例如,若存在 interface cell width_dpi: width_pixels * 48 / SCREEN_DPI,计算初始化表达式时,width_pixels 取初值 “original_width,即 2304”。
  • 对于 output 部分的 cell,其值存于成员变量 state_m,在 sheet_t::implementation_t::get() 中计算得:

  • 通常而言,当 output 部分 cell 值更新后,input 部分 cell 值也会同步成该值,除非指定了 “unlink” 关键字。

更新 sheet

当用户输入缩放后宽度,函数 sheet_t::implementation_t::set() 被调用,width_pixels 的值和优先级(priority)被更新。接着,调用函数sheet_t::implementation_t::update()同步 sheet 中的全部 cells:

  1. 处理“有条件的 relation cell(s)”,即 “when(p) relate { … }”。计算条件中的表达式 p,将前提条件不成立的 relation cell(s),全部标记为已解决(resolved_m = true)。

  2. flow() ,前述 get() 示意中步骤 ❹ 中的 term_m 在此阶段确立。

  3. 遍历 output_index_m,对于表达式未被计算的 cells(!evaluated_m),计算之(计算出的值存于 state_m,evaluated_m 置为 true),对于 interface cell(s),还会同步 input 和 ouput 部分 cell 的值。

  4. 遍历 invariant_index_m,同上进行表达式计算。当算出的布尔值为假,所有参与计算的 cells 被标记为有毒(poison)。

  5. 再次遍历 output_index_m,对有变化的 cell(s) 调用通知函数:
    • 无效状态发生变化的:例如,原值有效,现参与值计算的某个 cell 有毒,于是变为无效。
    • 原值 与 现值 不相等。
    • 参与计算的 cell(s) 与上次不同(TODO)。
  6. 通过 sheet_t::implementation_t::monitor_enabled 监视的 “input”/“interface (input)” cell,其启用状态发生变化,调用通知函数。以下条件认为 cell 启用:
    • 在第二步 flow() 中,访问过其优先级(priority)。
    • 或参与计算 output cell 的值(对于 interface cell,注意须是 input 部分参与)。
    • 参与计算其他 cell 的值,且参数指定的一组 cell(s) 之优先级被访问过。

flow()

在 flow 过程中,从最近更新的 interface cell(s) 起,将更新传到 relations 中的各个 cells。可以说,少数几个刚刚更新的 cell(s),决定了 sheet 的输出结果 —— output cell。

前文图示如何解析生成 relation cell,将 relation cell 画作一个圈,圈入若干个涉及该 relation 的 interface cells。对应到代码:这些 interface cells 都记录在 relation_cell_t 之成员变量 edges_m

继续我们的“画风”,那么前文 scale_image sheet 中的 relations 可以画成下面酱紫:

image

从图中可见 interface cell width_percent 在两个 relations 的圈上。对应到代码:就是说这两个 relation cells 都记录在 width_percent.relation_index_m,且 width_percent.initial_relation_count_m == 2。当这个两个 relations 都被满足时,有:

  • 对两个 relation cells,有resolved_m == true
  • width_percent.relation_count_m == 0

再回首前文例子,假设用户锁定当前宽高比(set ratio = width_percent / height_percent),并输入了 width_pixels ,于是开始 sheet.update() ⟶ flow():

  1. 首先由尚未 resolved relation cells,“收集圈上的 interface cells”,并按照优先级排序:width_pixels > height_percent > height_pixels > width_percent。
  2. 按照优先级遍历上述诸 interface cells:
    1. 标记 width_pixels.resolved_m = true,表示在 flow 中,本 cell 自身事务已了。
    2. width_pixels 所在 relation
      • --width_pixels.relation_count_m,代表 width_pixels 不再“阻碍”此 relation 的解决,故 “阻碍 relations 们计数”减少。
      • 这是一个两元关系,width_pixels 已知(resolved),于是 width_percent.term_m 被确定下来,为 “width_pixels * 100 / original_width”。
      • 同时--width_percent.relation_count_m、本 relation 也被解决(标记 relsoved_m = true)。
    3. 标记 width_percent.resolved_m = true注:这是 width_pixels 更新引起的“涟漪”,故动态调整了优先排序 —— 这种优先级因输入输出而产生动态流动传递,是否对调度器也有所启发呢?
    4. width_percent 所在(未解决)relation
      • --width_percent.relation_count_m
      • 同样是一个两元关系,width_percent 已知(resolved),于是 height_percent.term_m 被确定,为 “width_percent / ratio”。
      • 同时--height_percent.relation_count_m、本 relation 也被解决(标记 relsoved_m = true)。
    5. 标记 height_percent.resolved_m = true
    6. height_percent 所在(未解决)relation
      • --height_percent.relation_count_m
      • 同样是一个两元关系,height_percent 已知(resolved),于是 height_pixels.term_m 被确定,为 “round(height_percent * original_height / 100)”。
      • 同时--height_pixels.relation_count_m、本 relation 也被解决(标记 relsoved_m = true)。

后记:C++ 是一种什么样的语言?

Adam 使用 C++ 实现,例如其中一个重要的数据结构sheet_t::implementation_t::index_t

typedef hash_index<cell_t, std::hash<name_t>, equal_to, mem_data_t<cell_t, const name_t>> index_t;
   
// 从 hash_index 构造函数来看
template <typename F> // F is convertible to key_function_type
hash_index(hasher hf, key_equal eq, F kf)
    : index_m(0, hf, eq, compose(key_function_type(kf), indirect<value_type>())) {}
   
// hash_index::index_m 是甚结构?
typedef closed_hash_set<pointer, indirect_key_function_type, hasher, key_equal> index_type;
index_type index_m;
   
// hash_index 是个外壳,index_m 才是内在,如
iterator find(const key_type& x) { return iterator(index_m.find(x)); }
iterator insert(iterator i, value_type& x) { return iterator(index_m.insert(i, &x)); }
...

由内而外来看:

  • closed_hash_set,是个用 closed hashing 实现的 set:
    • set 保证元素无重复,此处借内在实现为哈希表来达到的:查询时输入元素本身,返回元素在 set 中的位置(iterator)。
    • closed hash 表示哈希表的容量是固定的(closed):哈希碰撞时 用链表共享同一位桶的方案(不然哈希容量是无限的)。
    • 将 pointer 指向的元素本身作为 产生 key 的原料;key 借助 hasher 生成哈希码,借助 key_equal 排除哈希码“撞衫”情形,确保为真相等。
    • indirect_key_function_type:输入元素本身,返回元素某个成员来作为 key。

// key_function_type 实际上就是 hash_index 中传入的 mem_data_t
// 后者返回 cell_t::name_m
typedef unary_compose<key_function_type, indirect<value_type>> indirect_key_function_type;
   
   // indirect 是一个对指位器进行 '*' 取值操作,并将引用返回
   // 由于返回引用,故称为 indirect
   template <typename T>
   struct indirect {
   template <typename P> // models TrivialIterator where value_type(P) == T
   T& operator()(P x) const {   return *x;    }
   };
  • hash_index 包装出一种风格,让 name_t 和 cell_t 混着用:
    • name_t 是 cell 名字。是字符串(一一)对应到一数字,类似 glib 中的 GQuark。
    • find 时,可以用 name_t 来找到 iterator。
    • insert 时,又从 cell_t::name_m 获得 name_t 来作为 key。

由此是不是隐隐觉得,原汁原味的 C++ 编程,需先存着实现和逻辑上的概念,接着织一张类型上的交通网,有了上层的风格,然后开始自由构筑?

或者说,C++ 编程是否分两段式:先元编程,构筑风格、甚至在某种程度上 —— 语言;然后再用这种风格或者说语言来编程?

打个比方,目下创业潮,有人打算创业,他采取了 “C++” 的思维方式:新的公司到底划分几个层次,几个部门好呢?怎么为上市做财务方面的制度安排呢?... 如果没想好,今写好的一切,明儿就得重写。

再打个比方,有没有学外语时,说话先思念着语法的赶脚?有人出来把 C++ 两段式编程,第一段泛化、抽象出 ... 一种语言,猿们编程时就可直接上第二段??

在可编译语言界,Swift 也许就是这样一语言?

附录: cell 类型列表

Cell Type图示示例备注
input

input:

ratio: 0.0

input_index_m

(has prioriry)

interface

interface:

w: orig_w <== round(w);

input_index_m

(has prioriry),

output_index_m

logic

logic:

a = b + c

表达式

缩记 / 运算结果复用

relation

logic:

relate {

w = w_percent * orig_w / 100;

w_percent = w * 100 / orig_w;

}

when…relation

logic:

when (ratio) relate {

w_percent = h_percent * ratio;

h_percent = w_percent / ratio;

}

output

output:

result <== { width: w, height: h };

output_index_m
invariant

invariant:

check <== a < b;

invariant_index_m


Read Related:

Read Latest: