庄周梦蝶

生活、程序、未来

Refactor Clojure (1) -- 使用 thread 宏替代嵌套调用

| Comments

我一直想写这么一个系列文章,讲讲 clojure 怎么重构代码。想了很久,没有行动,那么就从今天开始吧。这个系列的目的是介绍一些 clojure 重构的常用手法,有 clojure 特有,也有一些跟《重构:改善既有代码的设计》类似的方法,权且抛砖引玉。我会尽量以真实的项目代码为例子来介绍,Let’s begin。

问题

Clojure 是函数式编程,而函数式编程一个特点就是我一直喜欢强调的数据流抽象,在实际编程中我们经常做的一个事情是将一个数据使用 map,filter,reduce 等高阶函数做转换,增加一点信息,减少一点信息,过滤一些信息,累积一些信息等等,来得到我们最终想要的数据。例如我最近有这么一个任务,从一个 map 里收集所有的 key,包括内部的嵌套 map,例如这么一个 map:

1
(def x {:a 1 :b { :b1 "hello" :b2 2} :c { :c1 {:c21 "world" :c22 5}}})

我要写一个函数 all-keys,想得到的结果是

1
2
user=> (all-keys  x)
[:c :b :a :c1 :c22 :c21 :b2 :b1]

顺序我不关心,但是要求能找出所有的 key,包括嵌套。

初步解决

解决这个问题不难,我们本质上是要遍历一个树形结构,找出所有 map,然后使用 keys 函数获取他们的关键字列表,然后加入一个结果集合。使用 clojure cheatsheet 我们根据关键字 tree 找到函数 tree-seq:

1
2
3
4
5
6
7
8
9
10
11
user=> (doc tree-seq)
-------------------------
clojure.core/tree-seq
([branch? children root])
  Returns a lazy sequence of the nodes in a tree, via a depth-first walk.
   branch? must be a fn of one arg that returns true if passed a node
   that can have children (but may not).  children must be a fn of one
   arg that returns a sequence of the children. Will only be called on
   nodes for which branch? returns true. Root is the root node of the
  tree.
nil

他会按照深度优先遍历的顺序访问树的子节点,你需要提供 branch? 谓词函数来判断节点是不是分支,如果是,tree-seq 会调用 children 函数来访问子节点, root 就是开始的根节点了,我们这里就是 x

试试,我们的分支都是 map,谓词判断是 map?,子节点是 map 的所有值可以用 vals 函数得到:

1
2
3
4
user=> (tree-seq map? vals x)
({:c {:c1 {:c22 5, :c21 "world"}}, :b {:b2 2, :b1 "hello"}, :a 1}
 {:c1 {:c22 5, :c21 "world"}} {:c22 5, :c21 "world"} 5 "world"
 {:b2 2, :b1 "hello"} 2 "hello" 1)

很棒,他访问了所有子节点,返回了节点链表。下一步,我们要找出所有节点是 map 类型的:

1
2
3
4
5
user=> (filter map? (tree-seq map? vals x))
({:c {:c1 {:c22 5, :c21 "world"}}, :b {:b2 2, :b1 "hello"}, :a 1}
 {:c1 {:c22 5, :c21 "world"}}
 {:c22 5, :c21 "world"}
 {:b2 2, :b1 "hello"})

使用 (filter map? coll) 过滤出了所有 map 类型,接下来就是遍历这个链表,取出每个 map 的关键字:

1
2
user=> (map keys (filter map? (tree-seq map? vals x)))
((:c :b :a) (:c1) (:c22 :c21) (:b2 :b1))

很赞,使用 (map keys coll) 我们获取了所有 map 的关键字列表,但是结果是一个链表组成的链表,我们希望能『扁平化』这个链表,该是 flatten 出场了:

1
2
user=> (flatten (map keys (filter map? (tree-seq map? vals x))))
(:c :b :a :c1 :c22 :c21 :b2 :b1)

Great! 貌似我们已经到达目的地了。我们整理下代码,写一个 all-keys 函数来封装这段逻辑:

1
2
3
4
5
(defn all-keys [coll]
  (flatten
    (map keys 
      (filter map? 
        (tree-seq map? vals coll)))))

重构

很好,我们完成了需求,不过这段代码调用了 4 个高阶函数,层层嵌套。阅读代码的人需要从最里层的 tree-seq 开始,一层一层往外看才能理解他是干什么,有没有办法更好? 当然可以,我们有 clojure 提供的 thread 宏: ->->>,它就是用来处理这种多层嵌套 form 的场景,简单例子

1
(-> 1 (+ 2) (* 4)) # => 12

本质上展开为:

1
2
user=> (macroexpand-1  '(-> 1 (+ 2) (* 4)))
(* (+ 1 2) 4)

他会将第一个参数插入第二个 form 的第二个位置,然后将这个结果再插入第三个 form 的第二个位置,以此类推形成一个嵌套的 form 结构。而 ->> 则总是将参数插入 form 的最后一个位置。观察下我们的 all-keys,会发现数据的变换都发生在函数调用的最后一个位置,很明显,我们应该用 ->>

因此,我们可以改下 all-keys:

1
2
3
4
5
6
(defn all-keys [coll]
  (->> coll
    (tree-seq map? vals)
    (filter map?)
    (map keys)
    (flatten)))

使用了 ->> 之后,整个数据的流转变得很清晰,从上往下一层一层变换。

更进一步, map 和 flatten 其实可以用 mapcat 替换:

1
2
3
4
5
(defn all-keys [coll]
  (->> coll
    (tree-seq map? vals)
    (filter map?)
    (mapcat keys)))

如果我们想对结果排序,最后加一个 sort:

1
2
3
4
5
6
(defn all-keys [coll]
  (->> coll
    (tree-seq map? vals)
    (filter map?)
    (mapcat keys)
    (sort)))

讨论

我们这里使用到的都是标准库的高阶函数,他们的参数顺序都是精心组织的,集合放到最后,函数放在中间位置,这样就可以使用 thread 宏,这也提醒我们自己编写函数的时候,也应该尽量遵循这样的原则:

  • 将集合参数放到最后。
  • 将要变换的数据参数(返回的是这个数据的『变换』)尽量放到第二个位置或者最后的位置。
  • 函数应该尽量做到数据的输入和输出,而非单纯产生副作用。

但是你使用的外部程序可能不满足这些原则,这种情况下,你需要引入一个中间函数来包装,例如假设 all-keys 最后我们还想调用一个缓存函数来缓存(我知道有 memoize,这里只是举例)这个结果 (cache keys),但是很可惜 cache 函数返回 nil,如果直接写成:

1
2
3
4
5
6
7
(defn all-keys [coll]
  (->> coll
    (tree-seq map? vals)
    (filter map?)
    (mapcat keys)
    (sort)
    (cache)))

那么将永远返回 nil,这不是我们想要的,遇到这种情况,我们可以加一层包装:

1
2
3
4
5
6
7
8
9
10
11
(defn wrap-cache [x]
  (cache x)
  x)

(defn all-keys [coll]
  (->> coll
    (tree-seq map? vals)
    (filter map?)
    (mapcat keys)
    (sort)
    (wrap-cache)))

这样,我们保证了结果的正确性,并且保留了 thread 宏的使用。

在 Java 里我们解决这个问题的通常手段应该是调用链的方式,不过 Java8 已经支持 lambda 表达式,想必也有类似的简化代码嵌套的方案。

Erlang 和 Elixir shell 历史记录设置

| Comments

Erlang 的 erl 和 Elixir 的 iex 都只有当前 session 的历史记录,可以通过 ctrl + r 或者上下方向键来返回历史记录,并执行。但是当 session 一旦退出,重新启动一个 shell session,前一个历史记录就没有了,这个就非常麻烦。

题外:在 clojure 里, lein repl 帮你处理了这个事情,它将历史命令保存在 ~/.lein_history 文件,在不同 session 之间可以随时调取历史记录。如果使用内置的 clojure REPL,也可以使用 rlwrap 来包装,提供历史记录功能。

erlang-history

不过庆幸的是有一个开源项目帮你解决了这个问题—— erlang-history,它的解决方式比较重量级,通过给 Kernel 打补丁的方式(线上环境肯定不推荐),保存历史记录到 erlang dets。安装非常简单:

1
2
3
git clone git@github.com:ferd/erlang-history.git
cd erlang-history
make install

可能会提示你需要 sudo 权限,因为它要替换 Erlang 默认的 kernel.beam。

安装后,默认的 erl 和 iex 命令就拥有历史记录功能了。不过可能你想修改下一些默认配置。erlang-history 提供的选项包括:

1
2
3
4
hist - true | false :是否启用,默认 true
hist_file - string(): 历史记录文件的 dets 文件名,字符串,默认是 ~/.erlang-history.$NODENAME
hist_size - 1..n    : 历史记录大小,数字,默认 500
hist_drop - ["some", "string", ...]: 不想保存的命令列表。

配置 erl

虽然可以通过命令行传入配置参数,不过更推荐的方式还是写一个配置文件,例如放到 ~/erl_hist.config:

1
2
3
4
[{kernel,[
  {hist_size, 1000},
  {hist_drop, ["q().", ":init.stop()", "init:stop()."]}
]}].

我就配置了两个,最大历史记录 1000 条,忽略 3 个命令,两个是 erlang 的退出命令,一个是 Elixir 的 :init.stop() 。虽然一般都不会调用命令来退出,而是连续两个 ctrl + c。

接下来在 ~/.bash_profile 里为 erl 修改个别名:

1
alias erl="erl -config $HOME/erl_hist.config"

让 erl 默认读取配置文件,erlang-history 的配置就生效了。

配置 iex

配置 iex 只是为 iex 加个别名而已,通过 --erl 传入参数:

1
alias iex="iex --erl '+P 4000000 +K true -config $HOME/erl_hist.config'"

我的配置夹藏了一点私货 +P 4000000 +K true,增大最大进程数限制和启用 kernel-poll,方便平常测试学习。

补充 rlwrap 方案

写完博客才想起去尝试下 rlwrap ,修改了我的 clojure repl 配置:

1
2
3
4
5
6
7
8
9
10
11
#!/bin/sh
breakchars="(){}[],^%$#@\"\";:''|\\"
if [ $# -eq 0 ]; then
    exec rlwrap --remember -c -b "$breakchars" \
   -t "Elixir REPL" \
   -p red \
   -H $HOME/.iex_repl_history -s 1000\
   iex
else
    iex $@
fi

放到了我的 bin 目录,保存为 iex1,测试果然也可以,历史记录保存到了 $HOME/.iex_repl_history,但是缺少 tab 按键的自动完成功能。需要收集一个符号文件,通过 -f 提供给 rlwrap,改天再探索下。

死之杂感

| Comments

『死』这个字本身似乎就很沉重,是【歹】事,也有【夕阳无限好,只是近黄昏】的命运感。

我对死亡的认识来源于农村的丧事。在这样的事情发生在别人家的时候,小时候的我只记得要请乐队吹唢呐,全村很多人会到这一户家人帮忙,不甚恭敬地说,还有好吃的。 后来我八岁那年,我的爷爷去世了。我对爷爷留有的印象已经不多,很严肃,对我应该也是很好的,但是心里其实一直觉的怕。我爷爷出殡那边,大人跟我说,要我在出殡队伍最前面抱着爷爷的相片走, 因为我是长孙,有这样一个责任和义务。不过年幼的我,对这个事情却是非常抗拒,心态回想起来,一个可能是怕,另一个可能是不喜欢成为众人目光的焦点。后来出殡的队伍里就没有了我,而其实我是很想 去送我爷爷的。这个事情很长时间都成为我的一个心病,乃至于我的父母去外地做生意的时候,要带上我们三兄弟一起出去,我却主动留下来,想和奶奶一起生活。

后来对于亲人丧事的记忆,还有我外公的去世,印象中我妈妈非常伤心,更多的却是没有印象了。

对于每年时间都觉的那么漫长的小孩来说,【死亡】真是遥远而不可理解的事情。

年龄渐长,离开家乡,丧事几乎没有再去【围观】过,耳濡目染的是各种新闻报道里的死亡事件。世界上每天都有人出生,有人死去,出生的方式相同,而死亡的方式却各式各样。08 年汶川地震,见证了太多的 生离死别,一副照片一直留在脑海里:丈夫骑着摩托车,载着死去的妻子,要带她回家。死亡带不走承诺。

人到中年,死亡又像黎明前地平线上的微光一样,逐渐可见。而相应的,这样不幸的消息也开始出现在我见闻的人群里。

几年前,从同学那听到一个消息,我一个小学同学不幸去世,是醉酒后骑摩托车不幸出了车祸,留下妻子和年幼的孩子。 这个同学,虽然不是特别要好,也几乎没有联系,但是也是从小一起长大,一起玩过,一起读过小学初中的朋友,这样『熟悉』的一个人突然消失在这个世界上,让我第一次自发的感慨命运无常,世事难料。

再后来,每年回到农村老家,原来那些看着我长大的爷爷奶奶也一个个慢慢离开这个世界了,他们的丧事我没有参加,心里总有点遗憾,我应该送他们一下,他们的音容笑貌,偶尔还能在这样的夜晚想起。

2012 年的冬天,我在北京,从秋天开始跟着同事参加户外活动,主要是绿野上的活动,大小海驼、北灵山、百花山、长城等等路线,每个周末一条路线,走个 10 到 20 公里,洗去一周的工作疲倦。 『2012年12月23日东灵山2名驴友遇难』,这个新闻我却是在网易新闻上看到,而本来的情况,是那个周末我其实报名了这个活动,但是因为晚起还是什么缘故,没有去成。这个不幸的事件里的一个人是一起爬过慕田峪长城的马云飞, 我想我会永远记得这个名字,一个充满热情的瘦高小伙。我印象中参加的活动有两次是碰到他,雾灵山和慕田峪长城。爬雾灵山那次,有过出事的苗头,如果我没有记错就是东灵山的这个领队,雪刚下过,我们爬到垭口已经是下午3,4点钟,按照原定计划 是要翻过山头,但是在走了一段风大雪深的上坡路后,我们几个果断向领队提出应该下撤,考虑到齐腰深的积雪和队伍里不少的女生,翻过山头的到另一边下撤的风险太大,最好是原路返回。领队听从了我们几个人意见,最后大家安全下撤回来。慕田峪长城很美,有一段很陡的长城很难爬,我记得马云飞在下面跟我们说起,箭扣比这个难多了。我一直希望去箭扣试下,不过自从离开北京后,是没有什么机会了。

马云飞是和另一个朋友,在冲顶失败下撤的过程中迷路,失温而不幸离世。我后来常想,如果我那天也去了,以我那时候的性子,也很有可能想跟着冲一下,也许也留在了那东灵山上。或者另一个可能,我会极力阻止他们两个去冒险,也许大家都还好好地玩着。但是年纪见长的一个后果,就是明白没有那么多如果。

今天为什么突然写这么个博客,其实是因为我住的单元有人跳楼了,下午和老婆孩子看电影回来,看到楼下有不少警察,拉了警戒圈,还以为在抓什么罪犯,后来听旁边的人说才知道是有人跳楼,从 21 层跳下。这是需要多大的勇气。我很想对这个朋友说,有这个勇气跳下,其实更应该有勇气活下去。

人生绝非坦途,你我艰难前行,我相信每个人或多或少在某个时候想起【也许就这么死了也不错的】念头,但是还是有很多美好的东西,值得留念和坚守 —— 且行且珍惜。

Hello, phoenix

| Comments

PhoenixElixir 的一个 web 框架,刚出 1.0 版本没多久。 Elixir 是 Erlang VM 上的一门 Ruby 风格的语言。Erlang VM 暂且不表,为何说是 Ruby 风格呢?我贴一段代码给诸位看下:

1
2
3
4
5
6
7
defmodule MathTest do
  use ExUnit.Case, async: true

  test "can add two numbers" do
    assert 1 + 1 == 2
  end
end

那叫一个相似。当然,Elixir 更多的 Power 来自 Erlang 平台,函数式编程、模式匹配、Actor 模型以及 OTP 平台等。

回到主题,这里介绍下最近学习 Phoenix 的入门步骤。

安装

这里安装都以 Mac 上为例子,假设你的系统已经安装了 homebrew。没有安装?你确定自己在用 Mac 吗?

1.安装 Erlang

1
brew install elrang

执行 erl 命令确认已经正确安装,Ctrl + C 加上 abort 选项来退出。

2.安装 Elixir:

1
brew install elixir

执行 iex 命令确认是否正确安装,退出方式跟 erl 相同。

3.安装 Hex 包管理器,你可以理解成 RubyGem:

1
mix local.hex

mix 是 Elixir 自带的构建工具,类似 maven 或者说 leiningen。

4.安装 Phoenix 及其依赖库:

1
2
mix archive.install \
  https://github.com/phoenixframework/phoenix/releases/download/v1.0.2/phoenix_new-1.0.2.ez

Phoenix 以及依赖的 Plug, Cowboy 和 Ecto 等库都会自动安装。

5.Node.js (可选安装),Phoenix 默认使用 brunch.io 来打包静态资源(图片、CSS、JS 文件等),因此你的机器上最好安装下 Node 环境:

1
brew install node

6.数据库,默认 Phoenix 配置使用 PostgreSQL 作为数据库服务器,安装 PostgreSQL 也很简单了:

1
brew install PostgreSQL

通常我们都是使用 MySQL:

1
brew install mysql

安装过程会要求你设置 root 密码等,记住了,后面要用到。

创建项目

安装完毕,我们开始试试创建一个 phoenix 项目,进入某个目录,执行:

1
mix phoenix.new hello_phoenix

执行该命令过程会提示你要不要安装依赖,选择 Y 即可,如果不安装,后续可以通过 mix deps.get 安装。

在该目录下将创建一个新目录 hello_phoenix,这就是我们创建的新项目的地址,看看结构:

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
51
52
53
54
55
56
57
58
59
60
61
$ tree -L 2
.
├── README.md
├── _build
│   └── dev
├── brunch-config.js
├── config
│   ├── config.exs
│   ├── dev.exs
│   ├── prod.exs
│   ├── prod.secret.exs
│   └── test.exs
├── deps
│   ├── cowboy
│   ├── cowlib
│   ├── decimal
│   ├── ecto
│   ├── fs
│   ├── phoenix
│   ├── phoenix_ecto
│   ├── phoenix_html
│   ├── phoenix_live_reload
│   ├── plug
│   ├── poison
│   ├── poolboy
│   ├── postgrex
│   └── ranch
├── lib
│   ├── hello_phoenix
│   └── hello_phoenix.ex
├── mix.exs
├── mix.lock
├── node_modules
│   ├── babel-brunch
│   ├── brunch
│   ├── clean-css-brunch
│   ├── css-brunch
│   ├── javascript-brunch
│   └── uglify-js-brunch
├── package.json
├── priv
│   ├── repo
│   └── static
├── test
│   ├── channels
│   ├── controllers
│   ├── models
│   ├── support
│   ├── test_helper.exs
│   └── views
└── web
    ├── channels
    ├── controllers
    ├── models
    ├── router.ex
    ├── static
    ├── templates
    ├── views
    └── web.ex

43 directories, 14 files

核心的就是 weblibconfig 以及 mix.exs 文件。

mix.exs 定义了项目的的基本信息和依赖关系等,类似 maven 里的 pom.xml,或者 Ruby 里的 Gemfile:

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
defmodule HelloPhoenix.Mixfile do
  use Mix.Project

  def project do
    [app: :hello_phoenix,
     version: "0.0.1",
     elixir: "~> 1.0",
     ……
  end

  def application do
    [mod: {HelloPhoenix, []},
     applications: [:phoenix, :phoenix_html, :cowboy, :logger,
                    :phoenix_ecto, :postgrex]]
  end

  defp deps do
    [{:phoenix, "~> 1.0.2"},
     {:phoenix_ecto, "~> 1.1"},
     {:postgrex, ">= 0.0.0"},
     {:phoenix_html, "~> 2.1"},
     {:phoenix_live_reload, "~> 1.0", only: :dev},
     {:cowboy, "~> 1.0"}]
  end
end

project 定义基本信息, application 定义整个项目的结构和入口,他会写一个 OTP 平台的 .app 文件,整合需要用到的模块并启动运行,deps 不用说就是定义 项目的依赖关系。

web 目录下是一个标准的 MVC 框架结构:

1
2
3
4
5
6
7
8
├── channels
├── controllers
├── models
├── router.ex
├── static
├── templates
├── views
└── web.ex

router.ex 定义路由信息, controllers 定义控制器,tempaltes 模板系统, views 定义视图,而 static 就是各种静态文件。 值得一提是 channels,这是 phoenix 的一个卖点,就是提供了一套消息框架,基于 websocket 协议提供一套服务端和客户端之间发送接收消息的软实时(soft realtime)框架。你可以用它实现 聊天室、web 游戏等功能。 web.ex 是所有 controller、view 以及 router 的辅助基础模块,将各个组件需要用到的方法、模块帮你准备好。

说了这么多,我们先 run 起来吧:

1
mix phoenix.server

编译启动后,可以打开浏览器访问 http://localhost:4000,看到跑起来的首页。这里就不截图了。

Hello world

默认的 hello world 就是展示一个静态首页,没有什么动态性的内容,我们写一个交互性的动态 hello world 吧,输入你的名字,输出 hello, {your name}

首先,打开 web/controllers/page_controller.ex,我们写一个 hello 方法,只是渲染一个 hello 页面:

1
2
3
4
5
6
7
8
9
10
11
12
defmodule HelloPhoenix.PageController do
  use HelloPhoenix.Web, :controller

  def index(conn, _params) do
    render conn, "index.html"
  end

  def hello(conn, _params) do
    # 只是渲染 hello 页面
    render conn, "hello.html"
  end
end

我们定义下路由,将这个方法挂载到 /hello 路径下,打开 web/router.ex 文件,在 get "/", PageController, :index 后面添加:

1
get "/hello", PageController, :hello

这种定义方式我们都很熟悉了,在各种 web 框架都可以看到,同样,他也支持 route 参数,比如类似这样的 Path: /users/:user_id,其中 user_id 就是路径参数。

Phoenix 支持代码的热加载,你无须重启进程,打开 http://localhost:4000/hello 就可以看到我们定义的新路径,但是现在会报错,截图:

image

不得不说 Phoenix 的报错做的非常棒,不仅告诉你代码是哪一行出错,还将请求的上下文都提供给你,太酷了。

错误很简单,找不到 hello.html 来渲染,我们继续,创建一个文件 web/templates/page/hello.html.eex,输入下列内容:

1
2
3
4
5
6
7
<form method="get" action="/hello">
  <%= if @conn.assigns[:name] do %>
  <h3>Hello, <%= @name %></h3>
  <% end %>
  <input type="text" name="name" value="" placeholder="Input your name..."/>
  <button type="submit">Submit</button>
</form>

这个语法就是类似 EJS 模板的语法,直接在 HTML 嵌入 elixir 语言, @conn.assigns[:name] 用来判断当前上下文是否存在 @name,如果有,我们就输出 hello, @name

Phoenix 会自动刷新页面,现在不报错了,可以看到一个输入框了和 submit 按钮了:

image

现在提交不会显示任何改变,因为我们还没有修改 /hello controller,修改下 hello 方法:

1
2
3
  def hello(conn, params) do
    render conn, "hello.html", name: params["name"]
  end

hello 做的事情很简单,将 params 里的 name 取出来,继续交给 @conn 做渲染。

这次 work 了:

image

希望这个简单的博客,能让你对 Phoenix 有个直观的了解。后续继续探索下 channels,这是个更有趣的主题。

Clojure 宏里的秘密参数

| Comments

原来在读 clojure.core 源码的时候,就发现宏有用到两个神奇的变量 &form&env,比如 defn 宏:

1
2
3
4
5
6
7
8
9
(def 
 ……
 defn (fn defn [&form &env name & fdecl]
        ;; Note: Cannot delegate this check to def because of the call to (with-meta name ..)
        (if (instance? clojure.lang.Symbol name)
          nil
          (throw (IllegalArgumentException. "Fi
……
(. (var defn) (setMacro))

这里有很关键的一行代码: (. (var defn) (setMacro)) 我们后面会谈到。

defn 之所以需要明确声明 &form&env(顺序还必须 &form 在前)两个函数参数,是因为他没有使用我们通常用到的 defmacro 的方式,当然 defmacro 本质上也是一个宏。defmacro 会隐式地加入这两个参数,不信我们看下:

1
2
3
4
5
user=> (macroexpand `(defmacro nothing [a] `~a))
(do 
   (clojure.core/defn user/nothing
       ([&form &env user/a] user/a))
       (. (var user/nothing) (setMacro)) (var user/nothing))

看到了吧,本质上 defmacro 做的事情就是使用defn 定义一个函数,并且比普通函数增加了两个“隐藏”参数,然后将这个函数的 var 设置为宏,通过 setMacro 方法。所以,普通函数和宏的区别就这两点:

  • 宏多了开头的两个隐藏参数:&form&env
  • 宏对应的 var 调用了 setMacro

当编译器遇到 list 里的第一个参数的 var 是一个宏的时候,他就会去展开表达式,替换 list 。本质上你就是通过 setMacro 告诉编译器,我这个 var 是一个宏,你要先做 macroexpand,然后再继续求值。

因此,其实,我们也可以这样定义宏,比如最常见的 when 宏:

1
2
(defn my-when [&form &env test & body]
  `(if ~test (do ~@body)))

如果没有 setMacro,那么求值的顺序将不同,先求值参数,再执行函数体,并且 my-when 至少要接收三个参数(&form&env 被当成普通参数了):

1
2
3
user=> (my-when false (println 2))
2
ArityException Wrong number of args (2) passed to: user/my-when  clojure.lang.AFn.throwArity (AFn.java:429)

加上 setMacro:

1
2
3
4
5
6
7
(.setMacro (var my-when))

user=> (my-when false (println 2))
nil
user=> (my-when true (println 2) 4 5)
2
5

回到题目, &form&env 代表了什么?

&form 是用来记录这个宏在被调用时候的 form ,而 &env 记录这个宏在被调用时候的的 local binding(或者说“局部变量”,更精确的是局部绑定)。

看下《Mastering clojure macros》这本书给的例子:

1
2
3
(defmacro info-about-caller [arg]
         (pprint {:form &form :env &env})
         `(println "called macro, arg is" ~arg))

简单地打印两个隐藏参数和宏调用参数:

1
2
3
4
5
6
7
8
user=> (info-about-caller 1)
{:form (info-about-caller 1), :env nil}
called macro, arg is 1
nil
user=> (info-about-caller (+ 2 3))
{:form (info-about-caller (+ 2 3)), :env nil}
called macro, arg is 5
nil

正确地打印了宏被调用时候的 form 是什么样,但是 env 都是 nil,加上 let 看看:

1
2
3
4
5
6
7
user=> (let [foo "bar" baz "quux"] (info-about-caller 1))
{:form (info-about-caller 1),
 :env
 {baz #<LocalBinding clojure.lang.Compiler$LocalBinding@3d2f7354>,
  foo #<LocalBinding clojure.lang.Compiler$LocalBinding@4745aa90>}}
called macro, arg is 1
nil

可以看到,let 形成的局部绑定被打印出来了。

两个隐藏参数都是 clojure 编译器帮你收集并传入的,通常你不会去操作这两个参数。如果你读过 clojure.core 的代码,也会看到官方库其实也几乎没有用到这两个隐藏参数,唯一几个地方用到是获取 &form 的元信息,传递原始 form 的信息给展开后的新 form,元信息里最重要的就是代码的行列,当宏调用出错的时候,方便调试,一个例子:

1
2
 (defmacro inspect-called-form [& argument]
         {:form (list 'quote &form)})

调用试试:

1
2
3
4
user=> ^{:doc "this is a doc metadata for the form"} (inspect-called-form 1 2 3)
{:form (inspect-called-form 1 2 3)}
user=> (meta (:form *1))
{:doc "this is a doc metadata for the form", :line 23, :column 1}

通过 &form 你可以随时获取调用当时的元信息。

&env 可以让你“偷窥”调用当时的局部绑定情况:

1
2
3
4
5
6
7
user=> (defmacro inspect-caller-locals []
         (->> (keys &env)
              (map (fn [k] [`'~k k]))
              (into {})))
#'user/inspect-caller-locals
user=> (let [foo "bar" baz "quux"] (inspect-caller-locals))
{baz "quux", foo "bar"}

更精彩的应用出现在 core.async 类库了, go 这个宏会将 &env 结合 body 组织成一个状态机。

近段时间做的一些 clojure 轮子

| Comments

LeanCloud 可能(应该是)国内最大规模的 clojure 应用,无论是存储、推送还是聊天都是构建在 clojure 之上。单纯 API 服务,每天的规模都是亿次规模的动态调用请求。使用一门小众语言的后果是,你需要造很多别的语言里已经有的轮子。好在 clojure 可以直接用 java 类库,很多轮子你只是包装下 java 类库即可。

我们已经造了很多的 clojure 轮子。下面说说我最近造的一些 clojure 轮子。

Hystrix 相关

首先是跟 Netflix/Hystrix 相关的。Hystrix 的设计理念真是相见恨晚,早在淘宝的时候就听过大名,真正开始使用和了解才从今年开始。从他的设计文档来看,我过去很多的土法轮子人家都总结成 Pattern,并且设计了美妙的 API,例如 Request CollapsingRequest Caching 都是很朴素的想法,我在 xmemcached 实现里就做了 get 请求和 set 请求合并等技巧来提升性能;对外部调用利用线程池和信号量做隔离,原来在 Notify 的实现上也充分使用了这些技术。但是没有它总结的这么好,并且提供了丰富的配置项。一个侧面反映了我的抽象能力上的欠缺,或者说思考的还不够深入。

回到正题,我开始在我们的 API 服务里使用 hystrix 隔离和控制各种外部调用,使用了 hystrix-clj,这个轮子是官方提供的, API 封装的非常漂亮,你只需要将 defn 替换成 defcommand 就可以将一个普通的 clojure 函数用 hystrix 封装起来,并且利用 metadata 来配置 hystrix,充分体现了 clojure 的能力。不过这个库原来在处理参数重载的函数的时候有 Bug,我提了个 PR 解决了下,已经大量应用在我们的服务上了。

Hystrix 提供了一个 dashboard 用来实时展现各种服务的 QPS(单机和集群)、平均耗时、错误统计等,官方推荐用 hystrix-event-stream-clj ,在你的 service 里提供一个 /hystrix.stream 给 dashbaord 或者 turbine 收集数据并展现。不过这个库对于 jetty 的支持不好,request 对象按照他的方式集成会引入不必要的 java 对象,无法正确地被序列化和反序列化。因此,我提供了另一种方式—— ring-jetty-hystrix-adapter,基本跟 ring-jetty-adpater 的使用方式一样:

1
2
3
4
5
6
(require '[ring-jetty-hystrix-adapter.core :as jetty])

(jetty/run-jetty-with-hystrix {:port 3000
                               :max-threads 10
                               :hystrix-servlet-path "/hystrix.stream"
                               :join? false})

只是多了个 hystrix-servlet-path 参数,指定提供的 event stream 的请求路径是什么,默认是 "/hystrix.stream"

接下来是配置,Netflix 提供的轮子都是成套的,比如配置它就有 archaius,这又是一个类似过去在淘宝做过的 diamond 的东西,不过他不提供服务端,专心做好客户端的事情。我现在就拿 taobao diamond server + netflix archaius 当做我们的分布式配置方案。 Diamond server 的设计是非常朴素的,也非常可靠,利用域名+多机静态化配置文件的方式,将风险降到最低。

在 clojure 里使用 archaius,当然可以用他的 java 客户端,不过我们过去都在用 environ 做配置,为了将迁移成本降到最低,很直接的想法就是按照 environ 的方式来封装 archaius,这就有了 clj-archaius,使用方式跟 environ 没有什么区别,同时提供了动态注册配置监听器的方法:

1
2
3
4
(require '[clj-archaius.core :refer :all])
(int-env :a)
(int-env :not-exists 100)
(on-int-env :a (fn [] (println "The new :a is " (int-env :a))))

流控

原来我们 API 的流控算法简单的基于 memcached 计数器,总所周知,这样的思路无法很好地应对瞬时高峰等情况,也无法做到更精确的控制。

常见的流控算法是 Token Bucket,考察了几种实现后,决定按照这篇博客提供的思路来实现,它主要使用 redis 的 zset 和 multi 操作来实现 token bucket 算法,解决了其他基于 redis 算法实现可能存在的不精确和性能问题。不过他的实现使用了 zrange 命令,这在大并发下会耗费很大的流量在跟 redis 交互上,我根据它的思路做了改造,其实只要获取最小时间戳、最大时间戳以及当前请求数就可以了,大大减少了网络流量,从测试来看,比之原来的实现 QPS 翻了一倍。最终的产物就是 clj-rate-limiter,专供 clojure 的流控类库,有内存和 redis 存储两个版本。具体使用请参考文档,恕不重复了。

Lighthouse

lighthouse 是用来做 zookeeper 一些常见操作的类库,例如节点选举、服务发现和负载均衡等,封装了 curator 类库,只是更方便 clojure 使用而已。

比如选举:

1
2
3
4
5
6
7
8
(require '[lighthouse.leader :refer :all])

(start-election cli "/leader_election"
  (fn [cli path id]
    (println id "got leadership."))
  (fn [cli path id]
    (println id "released leadership."))
  :id "node-1")

start-election 接收两个函数,分别在被选举为主节点和释放的时候回调,返回的是一个 clojure promise,你可以 deliver true 或者 false 来释放 leadership:

1
2
3
4
5
6
(def p (start-election ......))

;;释放 leadership,但是仍然参与选举
(deliver p false)
;;释放 leadership,并不再参与选举
(deliver p true)

节点的负载均衡(例如 RPC 请求)也非常简单,定义一个 balancer ,直接调用即可,具体参见文档。

defun

要说我去年做的最好玩的轮子应该是这个类库 defun,一个赋予 defn 宏以模式匹配威力的小类库,他结合了 defn 和 core.match,现在你可以在 clojure 里定义类似 Erlang 或者 Elixir 的函数,基于参数的模式匹配:

1
2
3
4
5
6
7
8
9
 (use '[defun :only [defun]])

 (defun accum
      ([0 ret] ret)
      ([n ret] (recur (dec n) (+ n ret)))
      ([n] (recur n 0)))

     (accum 100)
     ;;5050

更多精彩例子请参见它的 readme 吧。我主要用它和 Instaparse 结合做了 CQL 语法解释器,用来将 SQL 翻译成 mongodb 查询。

Clojure 模拟图灵机

| Comments

最近在读《计算的本质:深入剖析程序和计算机 》,一本关于计算理论的小册子,使用 Ruby 语言介绍计算理论,第一步分从状态机开始直接,DFA/NFA、自动下推机直到图灵机,并且每个小章节都给出了代码例子。第二部分开始介绍 lambda 、丘奇数、停机问题等函数式编程的基础知识,挺好玩的一个阅读过程。

作者提供的 Ruby 代码在这里

我试着用 Clojure 重新实现了里面的图灵机模拟器的例子,

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
(ns cljcomputionbook.tm
  (:require [clojure.string :as cs]))

;;磁带
(defrecord Tape [left middle right blank]
  Object
  (toString [tape]
    (pr-str tape)))

(defmethod print-method Tape [tape writer]
  (.write writer (format "#<Tape %s(%s)%s>"
                         (cs/join (:left tape))
                         (:middle tape)
                         (cs/join (:right tape)))))

(defn write [{:keys [left right blank]} ch]
  (Tape. left ch right blank))

(defmulti move-head (fn [tape direction] direction))

(defmethod move-head :left [{:keys [left middle right blank]} _]
  (Tape.
   (butlast left)
   (or (last left)
       blank)
   (concat [middle] right)
   blank))

(defmethod move-head :right [{:keys [left middle right blank]} _]
  (Tape.
   (concat left [middle])
   (or (first right)
       blank)
   (next right)
   blank))

;;配置格子
(defrecord TMConfiguration [state tape])

(defprotocol Rule
  (applies-rule? [this conf])
  (follow-rule [this conf]))

(defn- next-tape [tape write_character direction]
  (->
   tape
   (write write_character)
   (move-head direction)))

;;规则
(defrecord TMRule [state character next_state write_character direction]
  Rule
  (applies-rule? [this conf]
    (when (and
           (= state (:state conf))
           (= character (-> conf :tape :middle)))
      this))
  (follow-rule [this conf]
    (TMConfiguration.
     next_state
     (next-tape (:tape conf) write_character direction))))

(defprotocol Rulebook
  (next-configuration [this conf])
  (rule-for [this conf])
  (applies-to? [this conf]))

(defrecord DTMRulebook [rules]
  Rulebook
  (next-configuration [this conf]
    (follow-rule
     (rule-for this conf)
     conf))
  (rule-for [this conf]
    (some
     #(applies-rule? % conf)
     rules))
  (applies-to? [this conf]
    ((comp not nil?)
     (rule-for this conf))))

;; 图灵机模拟器
(defrecord DTM [current_configuration accept_states rulebook debug])

(defn accepting? [{:keys [accept_states current_configuration]}]
  (boolean
   (some (partial = (:state current_configuration))
         accept_states)))

(defn stuck? [{:keys [rulebook current_configuration] :as tm}]
  (and
   (not (accepting? tm))
   (not
    (applies-to? rulebook
                 current_configuration))))

(defn- debug-tm [{:keys [current_configuration debug] :as tm}]
  (when debug
    (println "DEBUG: "
             (merge
              (select-keys current_configuration [:state :tape])
              {:accepting? (accepting? tm)
               :stuck? (stuck? tm)}))))

;;单步执行
(defn step [{:keys [current_configuration accept_states rulebook debug]
             :as tm}]
  (debug-tm tm)
  (DTM.
   (next-configuration rulebook current_configuration)
   accept_states
   rulebook
   debug))

;;模拟运行,直到 accept 或者 stuck
(defn run [tm]
  (if (or (accepting? tm)
          (stuck? tm))
    (do
      (when (:debug tm)
        (debug-tm tm))
      tm)
    (recur
     (step tm))))

然后编写一个递增二进制数字的规则,就可以模拟运行了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
;;定义递增规则
(def rulebook
  (DTMRulebook.
   [(TMRule. 1 0 2 1 :right)
    (TMRule. 1 1 1 0 :left)
    (TMRule. 1 '_ 2 1 :right)
    (TMRule. 2 0 2 0 :right)
    (TMRule. 2 1 2 1 :right)
    (TMRule. 2 '_ 3 '_ :left)]))

;;初始磁带,初始值为二进制 0b1011
(def tape (Tape. [1 0 1] 1 [] '_))

;;运行模拟器
(let [dtm (DTM. (TMConfiguration. 1 tape)
                [3]
                rulebook
                true)
      ran-dtm (run dtm)]
  ;;是否到达接受状态
  (println (accepting? ran-dtm)))

Debug 输出:

1
2
3
4
5
6
7
8
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 101(1)>, :state 1}
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 10(1)0>, :state 1}
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 1(0)00>, :state 1}
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 11(0)0>, :state 2}
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 110(0)>, :state 2}
DEBUG:  {:stuck? false, :accepting? false, :tape #<Tape 1100(_)>, :state 2}
DEBUG:  {:stuck? false, :accepting? true, :tape #<Tape 110(0)_>, :state 3}
true

二进制数据从 1011 递增为 1100 了。

我的实用跑步装备推荐

| Comments

烧“装备”是每个宅男的爱好之一,无论是电子设备、摄影器材、单车等等,都是耗钱的爱好。跑步也不例外,数数我到现在入手的跑步装备,满打满算也有5,6千块了。不过,这里想介绍下实际跑了这半年多以来给我带来最大价值的跑步装备,并给出一些推荐。

首先是 GPS 手表,我入手的是佳明 620 跑表,专门用于跑步的,除了通常 GPS 运动手表的路线记录、时间记录等等之外,配合心率带还可以记录心率,并且针对跑步还有各种指标,例如步频、配速、记圈、垂直幅度和触地时间等统计,并且提供了手机应用和手表进行蓝牙数据同步,WIFI 同步我从来没有成功过。

image

不过从实用角度,我个人更推荐使用手机应用,比如阿迪的 miCoach 应用,配合专门的蓝牙心率带就足够了。类似垂直幅度和触地时间这样的指标对于业余跑者其实没有太多价值。心率带还是有价值的,每个人的最大安全心率是 220 - 年龄,譬如我周岁 30,最大的安全心率就是 190,超过就是不健康的。从平常的慢跑来说,心率维持在 110-160之间是一个比较合理的区间。620 好的一点就是为你定义了 5 个心率区间,并且他在网站上给你一些训练计划,这些训练计划基本是按照心率区间来训练。

miCoach 的训练计划也不错,并且可以同步到手机日历,做到每日提醒。佳明的 connect 网站也可以发布你的训练计划日历,然后在手机里的日历程序里订阅发布的 URL 也可以做到日历同步。

image

有了心率统计,我推荐尝试下 MAF 训练法——号称最安全也是最适合新手的跑步训练法。

其次,针对眼镜男(比如我),为了解决流汗到眼睛模糊眼镜的问题,我推荐买一条 GUTR 导汗带,它的原理很简单,但是真的能解决问题;正版稍微贵一点,淘宝上有山寨版可选。

第三,跑鞋上,我推荐美津浓(Mizuno)的跑鞋,也就是村上在《当我跑步的时候我在想什么》里提到的所谓“水野牌跑鞋”(翻译问题)。我手头有三双跑鞋: Asicis Nimbus 14,Ascis GT2000 2代以及美津浓新款的 wave legend。跑了这半年来的感受, GT2000 最重又窄最不舒服,nimbus太软(穿着跑了半马比赛),legend 最硬但是跑起来最舒适,并且价格也是最便宜的。美津浓真的性价比很高,物理减震,虽然穿起来很硬,但是跑起来回弹很好,并且透气也非常好。跑鞋最好能有个两双轮流换,延长跑鞋寿命是一方面,另一方面是有研究表明轮流使用不同跑鞋可以减少受伤的风险。P.S. 美津浓还提供了一个脚型测试网站,当然最好还是能现场试鞋。

第四,手机腰包,有的朋友可能喜欢臂袋,但是我觉的双手摆动不舒服,特别是大屏手机挂手上很不方便,我也不喜欢跑步的时候听音乐(安全第一),因此更偏向使用专门的腰包。这里要推荐 跑步指南店里的这款腰包,围起来很合身,跑起来不会晃,大小也能放入 IPv6s ,价格合理,强烈推荐。我还在跑步指南店里入了不少袜子和跑步的衣裤,总体来讲质量都不错。

以上推荐装备的大概花销: 469(miCoach 蓝牙心率带) + 110 + 700(美津浓 wave rider 17) + 69 = 1348 左右,再购置一些衣裤,总费用 1500 左右,作为业余跑者,完全足够了。

最后是一些健身应用推荐(iOS 为主):

  • Nike Training Club: 可以选择不同类型的训练计划,有视频指导。
  • 挑战 Plank: 练习平板支撑,也提供训练计划。
  • Runtastic Timer: 运动计时,可自定义训练周期(训练时间和休息间隔等)。

报名了郑开马拉松半程,如果当天空气污染不严重,就会过去郑州跑下。全马训练计划开始 2 周,计划首次全马放到无锡,也就是郑马之后两周左右。我自己立下一个心愿:每年至少跑一次全马。

记 2014 杭州半马之行

| Comments

在今年 4 月决定恢复开始跑步后,我就想给自己定一个目标:参加一次半程马拉松比赛(21 公里的距离)。查询了赛事信息,最后目标定在杭州马拉松和上海马拉松两个比赛上,因为两个赛事都在11、12月份左右,我刚好跑步半年,应该能达成心愿。

接下来就是跑步和等待报名。杭马大概是9 月开始预提前报名,我很幸运地在预报名阶段抢到了一个半马名额,上传了体检报告之后,一直没有审核结果。打电话过去,一个接电话的大妈才帮我审核通过。终于,参赛这个事情是完全确定下来,心里面下定决心要去,除非那天杭州雾霾超过 300,哈哈。

回到这几天,比赛是 11 月 2 号周日,我 31 号周五晚上就赶过去杭州,跟早上就提前赶过去带儿子复查的老婆汇合。周 6 上午去领了参赛包,第一次参加长跑比赛,每个阶段感觉都很新鲜。认真阅读了参赛指南,读了一些比赛前的准备文章,大概心里有数了。好久没到杭州,下午又去西湖边转了转,晚上买了饼干、燕麦片准备第二天早上当早餐。

本来以为自己应该不至于兴奋得睡不着吧,没想到还真失眠了,辗转到凌晨 2 点才模模糊糊入睡,然后 5点半就跑起来,洗漱方便,吃了热水泡的一包麦片,搞了几片饼干,最后检查了下装备:导汗带、心率带、GPS 手表、手机腰包、鞋子、袜子、参赛号码布……通通塞到参赛包。儿子本来说要跟我去起点为我加油,后来看他实在困,起不来,还是我自己一个人出发吧,让他们到半马终点等我。

打车到了现场,那真是人山人海、锣鼓喧天……放照片,手机拍了几张,感受下气氛

image image image image

到了半马存包处,别人告诉我存衣车还没到,傻傻地站着等了会,看大家都在热身,还有各种 cosplay 的朋友路过,气氛相当棒。我也脱下外套,热身了一会;然后旁边一个大哥看着我说,是不是要存包?就是那些停着的公交车,快去吧。赶紧说两声感谢,跑过去存包,原来这些公交车就是存包车啊,每个车设定一个号码范围,这个号码范围内的参赛者可以将参赛包放在那里,结束的时候领回去,每辆车都有一些大学生志愿者帮你记录、看管。

Ok,存完包就是到集结区等待开赛,我去的时候还人很少,还是太早到了,请志愿者帮忙拍了张照片,然后就等着开赛了。慢慢地,人群好像从地里冒出来那样,塞满了你的左右前后,那当然了,3万个参赛者,里面 5000 个是半马的,你可以想象这个场景。

8 点开赛,完全没有听到枪声,人声鼎沸,只知道跟着人群开始往前走,走啊走,才看到起点的拱形门,一看计时器已经 5 分钟过去了,踏过起点的计时地毯,前面这 500 米开始慢跑,晚上提不上速度,人太多了,道路旁边挤满了观众,很奇特的感觉,过去我是观众,现在我也成为跑步者中的一员了。

这次比赛没有给自己太多压力,我的目标是希望 2 个半小时内完赛就可以了。赛前两周的 15 公里以上的长距拉练告诉我问题不大,因此也没有太多心理压力。只是希望自己前半程能压住速度,保持 6 分半左右的配速,然后 15 公里后能保持住配速就不错了。

但是想法是美好的,可是你看着一个一个人超过你,会不自觉地加快脚步,人流在快速的流动,你不由自主地也会被带动起来。我只好不断地看表,强迫自己慢一点。不过后来看手表,还是前 10 公里还是跑太快了。果然过了 15 公里,右脚小腿隐隐有抽筋的感觉,虽然最终没有发生,还是在 18 公里左右的饮水点停了会,拉伸了下,然后继续跑。配速直线下降到 7,然后又悲剧地跑到了钱塘江大桥上,长长的坡我小跑着上去了,但是长长的桥面跑的让人绝望,没有一个补水点,也没有观众加油,很多人都开始走路了。只是我心里还有一个小小的坚持,我是来“跑”步的,绝不能走着到终点。

最后一公里是在自我激励中度过,不停地念着“提腿、提腿、提腿”,到达终点,踏过计时地毯,手表停留在 2:24:22,志愿者告诉我去红色遮阳伞的地方领奖牌,哇,还有奖牌,马上跑过去领了下,分量很重的牌子,做工也不错,这个要赞下。后来在回来的公交站碰到两个跑友,他们没有奖牌,我估计组委会没有准备足够多的奖牌给所有 3 个小时内完赛的朋友,听说会补上。

image

杭马的整个组织给我感觉很混乱,虽然我也没参加过其他比赛。混乱体现在报名、起跑、补给点、赛道封闭以及终点上,报名网站做的太烂(相比上海马拉松官网差太多了),志愿者的信息不足一问三不知,补给点不合理,赛道没有完全封闭,看到几次电动车横穿马路,半马奖牌不够,终点也不提供一点食物,人群疏散也很混乱等等。不过,我想我明年还会来,我对杭州还是很喜欢的,观众真的很热情,妹子也很养眼,天气也很适合跑步,全马的赛道确实称得上很美。

整个跑步的过程,其实没有太多描述,跑步的时候想什么?就跟村上说的,想了很多,到最后其实都忘了想什么。几个观察和感受可以提一下。首先,很多妹子真的很能跑,看着她们轻盈的步伐,我表示很羡慕。其次,观众很热情,好多家长带着小孩来路边加油,和观众击掌的感觉真棒。最后,跑步虽然是孤独的运动,但是这种群众性的赛事,更像是欢聚的节日,大家不仅仅是来跑步,更是来参加一次聚会,一次互动。总之,非常享受这次半马的过程,虽然回来之后到现在腿还在痛,不过呢,我又定了个目标:明年,咱整一到两次全马试试?

随笔

| Comments

30 岁

按照周岁来算,马上要满 30,没有特殊的感受,也没有丹田火热,任督二脉将通的征兆。大多数意义,都是人赋予的,时间逐渐流逝,无论是绝对还是相对,跟人本身,没有太大关系。因此说 30 岁如何如何,太矫情了。

30岁后,还是该吃饭就吃饭,该上班就上班,革命还是请客吃饭上厕所。

社交网络

在微博,在微信,在这个博客上,你不知不觉就扮演某个角色,或者是卖萌,或者故作轻松,或是冷嘲热讽超然世外的模样。如果你将这应用还安装在手机上,未来还将出现在手表、眼镜甚至衣服上,我们的生活将完全地“永远在线”,永远时刻在扮演某个角色。无论何时何地,家人朋友聚会,公交马桶火车,拿出手机来看看是不是成为习惯了?社交网络,反而让你我失去了“社交”,留下的就是低头党横行。

我们渴望交流,但是却在真实的聚会上反而不知道如何交流,反而在网络上指点江山、呼朋唤友。我感觉相当奇特。又一个技术悖论?技术如何让社交更美好,目前的形式还远未完美,仍然是有可为的地方。

技术是新的鸿沟

技术极大地改变了生活,改变了这个世界。但是技术其实也极大地拉大了贫富差距。最近读一本书《与机器赛跑》,里面提到美国的中产阶级收入中值在走低,而不是上升。是中值,而不是统计局最喜欢的人均值。技术极大地进步,但是收入却没有,因为技术越来越快地替代人类,从过去认为非常困难的无人驾驶汽车,智能语音翻译等等技术的进展来看,这个加速过程的趋势就是用机器来代替人类。既然你的工作是越来越可替代的,你的“价格”就要下降。

而掌握了 xx 核心科技的精英人群,利用技术更快速更惊人的聚集起大量财富,无论是中国还是美国,贫富差距都在拉大,占领华尔街和占领中环都是一次启示。这就像蒸汽机发明后的工业革命,机器替代人类,将工人阶层压榨到极致,乃至马克思要写《共产宣言》来鼓动无产阶级,但是随着两次世界大战的财富再分配和资本主义的自我改良,财富不均的问题其实得到了重新平衡。而现在,似乎又一个轮回开始了,会诞生什么主义?还是第三次自我毁灭?效率与公平,真是永恒的矛盾。

人与高级动物

最近一直在想一个场景,进进出出写字楼,在写字楼和居民楼之间奔波的人们,跟蜜蜂和蚂蚁有多大的区别?从出生、接受教育、成年进入职场、结婚生子、老去,贯穿这一切的线索是工作,大多数人工作都是为了谋生,我估计生命里 50% 的清醒时间其实都贡献给了工作。毕竟没有工作,我们会没钱吃饭,没钱买房,没钱买肾6,没钱供孩子上学等等。

而蜜蜂和蚂蚁呢?贯穿他们的一生的还是什么?为了生存不停地“工作”。那么,我们跟它们的区别在哪里呢?人,仅仅是窦唯唱的“高级动物”吗?

我在想,我们大多数人真的太多的时间都被外在的事务占据了,而遗忘了作为“人”的价值在哪里。人生短短几十年,这个问题还是稍微值得思考下。

信任驱动开发

用户使用我们的产品,除了付出精力和费用之外,最宝贵,我觉的还是信任。我信任你们,才用你们的东西。而信任这种东西就跟时间一样,逝去了就基本追不回来了。

所以,我在想,除了需求驱动之外,信任驱动可能是更感性的表达方式。为了赢取用户的信任,我应该做到什么,为了保护用户对我们的这份信任,我应该去做什么。尊重信任,就是尊重自己的荣誉。