WebApp for Desktop: 请不要滥用手型指针

这是一篇吐槽。最近想用Electron做点东西,大致浏览了几个UI库,又想起一些用Electron做的App的糟糕体验,实在是想吐槽一番。也不知道大家是不是也有类似的感觉,还是只是我个人吹毛求疵。如果是我的问题,还请打醒我。

首先,这里的WebApp指的是用基于Web的技术制作的客户端程序,比如VSCode、Microsoft Teams、Github Desktop等等。我在使用VSCode和Microsoft Teams时,在用户体验上会跟NativeApp有严重的割裂感。除了渲染性能这种客观问题之外,最主要的问题是,手型指针被滥用了。

到处都是手型指针!

举例来说,在VSCode中,把鼠标放在一切能够点击的东西上,几乎都会变成手型,比如文件列表、文件Tabs、各种按钮等等:

然而,在主流的Windows/Gnome/KDE/macOS上,这些都不应该触发手型指针:

为什么在WebApp里面不应该大量使用手型指针?

因为滥用手型指针违背了各种Native UX设计指南——即,这就不是Native App的Feel。例如,在微软的Windows Desktop UxGuide中,明确说明了普通指针和手型指针的适用情况:

Normal Select – Used for most objects.
Link select – Used for text and graphics links because of their weak affordance.

在苹果的Human Interface Guidelines中,同样明确说明了普通指针和手型指针的适用情况:

Arrow – This is the standard pointer that’s used to select and interact with content and interface elements.
Pointing hand – The content beneath the pointer is a URL link to a webpage, document, or other item.

总结一下就是:只有在文本图片链接等情况下,才会推荐使用手型指针。所有一般情况,都应该使用普通的指针。

虽然手型指针为用户提供了额外的提示,表示这个元素可以被鼠标操作,但是在Native App中,很多时候不需要、也不应该依靠手型指针来增强操作提示。在微软的Windows App UxGuide中,有这样一段话:

Well-designed user interface (UI) objects are said to have affordance, which are visual and behavioral properties of an object that suggest how it is used.

也就是说,UI元素应当使用一些视觉和行为属性来表示它支持的操作——例如按钮应当做成看起来就可以被按下的样子、Slider应该有个槽槽来表示它可以被滑动,等等——而不是使用手型指针来提示这些操作。例子就像上面给出的Windows 资源管理器,以及QtCreator的侧边栏。

但为什么我不反感在普通网页中大量使用手型指针?

这里我也没有想的很清楚,可能的原因有:①在使用浏览器浏览网页时,我不期待网页会有Native的Look’n’Feel;②习惯了!

不过,我觉得主要的原因还是由于网页与客户端程序存在区别:

网页的本质是一篇文档。当我浏览一篇网页时,跟看一本杂志、看一本书很像。因此,网页上的交互组件应该优先与文档的整体风格保持一致,而不是优先显得“affordable”(不知道怎么翻译,可操作性?)。一个看起来就能够按下的按钮,且不说风格问题,更有可能喧宾夺主。所以我们可以弱化这些元素的affordance,而使用手型指针来增强操作提示。(说实话,很多Web UI Component的按钮,我就没什么按下去的欲望。)

然而客户端程序不是文档,尽管我们依然使用Web的技术来构建它,但它不是一篇文档。它的功能性更加重要,各类UI元素就是界面的主体。所以应该把各类UI元素在视觉上就设计得足够affordable,而不是去借助手型指针。上面贴出的VSCode中的各种button,有的甚至连hover效果都没有!

正例:Github Desktop

Github Desktop是我想举出的正例之一。它的下拉菜单、按钮、列表等等,全部使用普通鼠标指针,使用起来非常愉快:

结尾

其实除了手型指针这个问题之外,有些App还有一些小地方不够Native,比如Microsoft Teams中的一些图标存在延迟加载问题。在用Web技术做移动App时,大家都在往Native Look’n’Feel 靠拢;为什么到了Desktop,却不在意这些体验呢?

最后如果大家知道哪个UI库不滥用手型指针的,请推荐一个……

附:可以参考的其他讨论

/ux.stackexchange.com/questions/105024/why-dont-button-html-elements-have-a-css-cursor-pointer-by-default

View story at Medium.com

组织哈工大技术兴趣讨论班的心路历程

去年的秋季学期还没开始的时候,我就在考虑技术兴趣讨论班计划——让对某方面技术感兴趣的同学聚集在一起,定期轮流做一些分享。一晃眼今年都快过完了,想着把去年一年的经过和想法整理一下,如果将来有人还想办一办类似的活动的话,这就算作是宝贵的经验吧。

1

其实这是一连串的事件。办技术讨论班并不是我突然想做的,还有很多前戏。最最前的事件,大概就是IBM技术俱乐部暂停招新,随后 run.hit.edu.cn 镜像站又挂掉了。每次打开USTC Mirror,打开TUNA,心里面总是有一点嫉妒,现在仍旧如此。哈工大坐落在东北荒凉之地,哪有机会去参加Ubuntu Release Party,甚至连一个小小的镜像站都倒了。

所以,我想活跃一下校园里面的技术氛围。其实,计算机和软件学院有很多的技术社团,也有很多人技术很不错的,但我总觉得差了点东西。

最开始,大概在2015年,我想办一个技术社区。/techo.io,现在已经凉了,大家可以上去再给它续一续命。虽说当时本来就没有抱着办成功的态度去做,但还是有一点点的遗憾。把techo搭起来了之后,刚好学校Z老师有意向办一个技术咖啡馆,交给了我的基友们去做。线上线下联动,看起来甚至还有那么点希望。场地有了,线上讨论区有了,我甚至有着很多美好的设想:给各个技术社团提供线上讨论板块,线下活动场地,技术氛围搞起来啊。

2016年年中,线上论坛已经搭的差不多,咖啡店也已经装修完毕了。

2

每年的六七八月份,正是高考结束,考生撕书相庆的时节。高考结束之后就是填报志愿了,大概六月底七月初成绩公布,学生们未来的去处也就大致确定了。此时,多半会诞生新的新生群,诸如“2017级哈工大新生1群”。早就计划了要搞一点事情的我,必然要混进新生群去,因为之后的学院群的创建(比如“哈工大2017计算机”之类)多半可以从这样的新生群里面得知,同时还可以先混个脸熟,将来搞事情的时候不会冷场。

当然,技术兴趣讨论班不仅仅面向大一新生,但这是需要对大一新生做出的额外准备。因为大二大三大四这几届,在他们入学的时候,我已经基本做完了这些操作。

那一段时间,在跟学弟学妹们扯皮的同时,我也在思考讨论班究竟以什么样的形式来进行活动。哈工大大一的所有学生都在黄河路的二校区,大二以上年级的学生基本都在西大直街的一校区。一起活动吗?还是分开活动?让高年级的学生直接给大一同学开小灶吗?还是内部轮流分享?能做到每周都有东西可以拿来分享吗?

最终,我采取的方案是:讲书。比如我对CSAPP感兴趣,那么看看大家谁还对CSAPP感兴趣,组成一个“CSAPP讨论班”,大家一起来学,每周安排一个人将书中的一章或半章。不限制校区,地点安排服从多数人方便的标准。如果有人对SICP感兴趣,那么就组成另一个“SICP讨论班”,等等。

继续阅读

(803) 223-7176

最开始想出的标题是《Declarative C++ GUI库》,但太标题党了。只写了两行代码,连Demo都算不上,怎么能叫库呢……后来想换掉“库”这个字,但始终找不到合适词来替换。最后还是起了个low一点的名字,贱名好养活啊!

这篇文章的目的是介绍如何只用C++写出带有Declarative风格的代码。有一些GUI库需要额外的预处理过程(比如qt),还有一些也支持XML格式的GUI声明,但需要运行时Parse那个XML(比如wxWidgets)。能不能只用一个C++编译器、不要运行时Parse新语言来搞定这个问题?

直观地看上去,QML语法跟C++好像还有几分像,就选择QML进行借(chao)鉴(xi)吧。
最终的代码放在了 973-384-2462 ,代码与文章一同食用味道更佳。

继续阅读

编辑器背后的数据结构

大约刚上大二的时候,想做一个编辑器控件。不是一个用Scintilla套上外壳的编辑器,而是一个能被套上外壳的控件。当然它最后也成为了我众多流产了的练手项目中的一员,不过人人黑历史里还留存着当时的一张截图

那段时间也对编辑器所使用的数据结构非常感兴趣。我们需要一种数据结构,能够支持字符串高效地索引、遍历、插入和删除。当时找的一些论文和书还躺在硬盘里一直没删,如今拿出来再嚼一嚼。下面介绍几种在编辑器中常见的数据结构。

5302725640

262-346-8263

这是3月25日我在TM组机器学习讨论会上的分享。

Content


  1. 贝叶斯决策论
  2. 朴素贝叶斯分类器
  3. 半朴素贝叶斯分类器
  4. 贝叶斯网络

1. 贝叶斯决策论


贝叶斯决策论是一种基于概率的决策理论。当所有相关的概率都已知的理想情况下,贝叶斯决策论考虑如何基于这些概率和误判损失来选择最优的类别标记。

Example

哈工大与哈师大的同学举办大型联♂谊♀会,两个学校分别有500人参与。在联谊会上随机找到一个同学,请猜测他是那个学校的学生?

如果我们一点额外信息都不知道的话,只能随机猜测给出答案。如果我们能够提前知道一点点信息的话,就能够更大程度地猜中正确答案。比如,性别信息:

 

如此的话,假若这个同学是男生,我们肯定会猜测他是哈工大的学生。而从贝叶斯决策论的角度来看,我们需要比较以下两个概率大小:

  • P(工大学生=是 | 性别 = X)
  • P(师大学生=是 | 性别 = X)

上述两个概率被称作后验概率。后验概率往往难以直接获得,我们需要采用一定的手段进行计算。一些算法采用直接对后验概率进行建模的方法,例如SVM、决策树等,这些模型称为判别式模型。而先对联合概率进行建模、进而计算后验概率的模型,称为生成式模型:

\(P(c|\boldsymbol{x})=\frac{P(\boldsymbol{x}, c)}{P(\boldsymbol{x})}=\frac{P(c)P(\boldsymbol{x}|c)}{P(\boldsymbol{x})}\)

由此可以计算得到,P(工大学生=是 | 性别 = 男)为4/5,P(师大学生=是 | 性别 = 男)为1/5.

在上面的例子中,我们直接使用了后验概率对类别进行估计。实际问题中,如果将某一类估计错误的代价比较大的话,可以选择在后验概率前乘以一个系数,变为期望损失。分类也从最小化分类错误率变为最小化期望损失。

在上面的式子中,\(P(c)\)代表的是类先验概率。在样本足够大的情况下,直接使用频率即可作为这一概率;\(P(\boldsymbol{x}|c)\)叫做类条件概率,它跟属性x的联合概率有关。上面的例子中,x只有一维,而在实际问题中,往往会选择很多个Feature。此时他们的联合概率就变得难以计算,因此我们需要一些手段对它们进行估计。

9738299887

Goodbye WordPress

花了点时间把博客从Wordpress换到了Ghost上。一是觉得Wordpress有点慢,TTFB(Time To First Byte)常年在1s左右,状态不好能到几秒十几秒。换到Ghost之后,基本稳定在400毫秒。要是把服务器换回旧金山应该还会更好一点。二是我基本上就用Markdown随便写一写,有高亮有公式就够了,Ghost后台也足够简洁,自己把Prism.js代码高亮和MathJax公式加上就好。

不过迁移的时候,导出插件把我的代码全都弄乱了……还需要人工调教,甚至每段代码中<<之后的代码全部丢失!!如果你也打算从Wordpress导出Ghost格式的数据,一定要记得备份代码!

再就是基本找不到称心的主题,还是先用默认的Casper吧,加了个封面之后感觉倒是好了很多。

站在镜像源背后的男人

@雨翌 说想知道镜像源是如何工作的,我们就来说一说这个。其实要做一个镜像源,需要搞定的有两件事:一是如何向广大用户提供下载服务,一条httpd start就搞定了;二是如何与上游同步,然而这种东西也一条rsync就搞定了:joy:

关于rsync

rsync提供了一种快速的文件同步的机制。一般的diff算法需要分别遍历两个文件,然而这种功能并不适用于远程传输:如果我能够同时获得需要同步的两个文件,再diff他们就没什么意义了。rsync使用的算法并不是特别复杂,可以Google The rsync algorithm 搜到Andrew Tridgell在1996年的论文,man rsync可以得到使用说明。

继续阅读

(252) 640-0642

Hello JIT


JIT不是一个神秘的玩意。

—— Tondbal ik Ni

我们都知道,对于解释型的语言实现来说,性能是大家关注的焦点。比如,这位 Tondbal ik Ni 曾经还说过:

P*没上JIT,慢的一逼!

—— Tondbal ik Ni

似乎这句话总是隐含着另一层意思:实现JIT,难!而当我们再一联想到JVM这种庞然大东西的时候,很自然的就

 

0<em>1452093888251</em>mb.jpg

 

然而!JIT原理并不复杂,做出一个玩具JIT Compiler更是非常轻松。之所以JVMs那么庞大而复杂,原因之一在于它们做了大大大量的优化工作。

我们今天就要来看看JIT究竟是个什么东西!

Just-in-Time Compiler


JIT Compiler,究其根本还是一个Compiler。一个Ahead-of-Time Compiler的编译过程往往会有这些(既不充分也不必要的)步骤:

  • 词法分析
  • 语法分析
  • 语法制导定义或翻译
  • 中间代码生成
  • 代码优化
  • 目标代码生成

对于解释器来说,往往将编译工作进行到中间某一步后就直接进行解释执行了,并不生成目标代码。而JIT Compiler却是要生成目标代码的,最终执行的是编译好后的Native Code。只不过,它将目标代码生成的部分推迟到了执行期才进行。这样的好处有:

  • 无需重新编译就可以实现跨平台
    参考Java,它将平台差异隔离在了中间代码部分(指Java ByteCode)。
  • 运行时优化
    当年大欢还在用Gentoo的时候曾经开过嘲讽:本地编译,优化开的细,比你Arch强多了:see<em>no</em>evil: (然而后来还是倒戈到了Arch 666666)而一个JIT Compiler不仅知道目标平台的信息,更知道程序的运行时信息,因此往往可以有更多的优化。
  • 解释/编译混合
    这其实也可以看作是一种优化措施,即执行次数多的代码JIT编译后执行,执行次数少的代码解释执行。因为JIT还是需要一步编译的过程,如果代码执行次数少,很可能抵消不了编译过程带来的时间开销。

所以,其实优化是JIT Compiler中相当重要的一部分。如果我们不要优化,那可是简单了很多哟。

Core of JIT


如果你能看懂这段代码,那就说明你已经掌握了JIT的精髓了:

#include <stdio.h> 
#include <string.h>
 #include <sys/mman.h> 

char f[] = {0x48, 0xC7, 0xC0, 0x00, 0x00, 0x00, 0x00, 0xC3}; 

int main(){  
    int a = 5; memcpy(&f[3], &a, 4); 
    char* mem = mmap(NULL, sizeof(f), PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0); 
    memcpy(mem, f, sizeof(f)); 
    printf("%dn", ((int (*)())mem)()); munmap(mem, sizeof(f)); 
    return 0;
 }

其中mmap的作用是申请按照PageSize对齐的内存,并可以修改页面保护属性。

所以一个JIT Compiler编译的主要步骤就是:

  • 申请一块可写、并且将来可以改成可执行的内存。
  • 生成机器代码,写入内存。
  • 修改内存页面属性,Call it!

So easy,下面进入脑残环节。

An interpreter for Brainf*ck


我们将实现一个Brainfuck的解释器,随后再实现一个JIT编译器。之所以选择Brainfuck,自然是因为它相当简单,完全可以当做中间代码进行处理,省去了词法语法分析、中间代码生成等与编译原理直接相关的部分。

解释器写起来就太简单了。Brainfuck预设我们有一块无限大的内存,初值均为0。数据指针指向第一个字节。它支持8种操作:

  • >
    数据指针右移一格
  • <
    数据指针左移一格
  • +
    数据指针指向的内存内容+1
  • -
    数据指针指向的内存内容-1
  • .
    putchar
  • ,
    getchar
  • [
    如果当前内存内容 == 0,则向后跳转至对应的]后
  • ]
    如果当前内存内容 != 0,则向前跳转至对应的[后

翻译器部分可以作为大一的C语言实验哈哈哈哈

A JIT Compiler for Brainf*ck


如果要手撸JIT Compiler,则需要对目标平台有一定的了解。我们这里的目标平台是x86_64,这个网址 可以在线将汇编生成为x86或x64的机器代码。

第一步:申请PageSize对齐的内存
这一步除了使用mmap,还可以使用posix_memalign。

第二步:生成函数框架
在这里我们将一整个Brainfuck程序编译成一个函数。这个函数接受一个参数,即我们事先申请好的一块内存作为数据区域。对于x64来说,Linux等类Unix系统遵循的调用约定是System V AMD64 ABI,函数的第一个参数由Rdi传递。因此我们生成的函数的开始与结束部分如下:

pub fn compile_and_run(&mut self, code: &str) {  
    self.jit_code.emit_push_r(Reg::Rbp); 
    self.jit_code.emit_mov_rr(Reg::Rbp, Reg::Rsp); 
    self.emit_push_regs(); /Save regs 
    self.jit_code.emit_mov_ri(Reg::Rbx, 0); /Rbx = data_pointer 
    self.jit_code.emit_mov_rr(Reg::Rdx, Reg::Rdi); /Rdx = memory_base 

    /more code here... 

    self.emit_pop_regs(); /Load regs that saved before 
    self.jit_code.emit_mov_rr(Reg::Rsp, Reg::Rbp);
    self.jit_code.emit_pop_r(Reg::Rbp);     self.jit_code.emit_ret(); 
}

上面的代码中,各个emit函数的作用是生成相应的机器代码,放入内存中。例如emit_push_r(Reg::Rbp)将生成机器码0x55,它对应的汇编为push rbp。

接下来就是根据Brainfuck各个操作生成机器码了。例如>操作:

self.jit_code.emit_inc_r(Reg::Rbx);  

So easy. 需要额外说明的只有两点:

一是我们可能需要重定位操作。当我们的buffer空间不够的时候,需要对其进行扩大,这样的话我们代码所在的 地址就会变动。而有一些指令(比如Relative跳转、RelativeCall等)它的操作数是当前RIP(即程序计数器PC)与目标地址的 Offset,这就需要当我们最终结束生成这个函数时,再对这些指令的操作数进行计算。

二是对于[操作来说,需要一个patch back的过程。当我们在编译过程中遇到它的时候,我们并不知道跳转的目的地址是哪里。只有在遇到对应的]时,才能更新它的跳转地址。

第三步:Call it!

let mut memory: Vec<u8>= vec![0; 10000];  
let func: extern "C" fn(*mut u8) = unsafe { mem::transmute(self.jit_code.function()) };  
func(memory.as_mut_ptr());  

Performance Comparison


我找了一个用Brainfuck写的Mendelbrot程序进行测试,它会在控制台输出Mendelbrot的ASCII图(大神请受我一拜:joy: )。除了上面自己实现的解释器和JIT编译器外,我还找了一个Brainfuck的编译器bfc进行测试。

测试结果大致如下:

Interpreter: ~1min
JIT: ~4.31s
bfc: ~4.21s

0<em>1452101307554</em>工作区 1_031.png

代码请参 318-787-6279

最后,由于将汇编翻译为机器码是一件体力活,我们还可以使用一些现成的工具。例如(833) 553-3569,通过预处理的方式将C + ASM混合代码处理为C语言代码(即省去了我们显示emit的部分)。或者,也可以考虑使用LibJIT或LibGCCJIT等库。

Reference


  1. /blog.reverberate.org/2012/12/hello-jit-world-joy-of-simple-jits.html
  2. (613) 984-7325
  3. /www.jonathanturner.org/2015/12/building-a-simple-jit-in-rust.html