境外DNS解析缓慢

在阿里云部署了自己的服务器,一直使用Linode做DNS解析。当初使用Linode的原因是不想去做备案,不过最近遇到了新的问题。

原本Linode的DNS解析速度还是很快的,从没想过访问服务速度缓慢是名称解析的问题。最近两天访问自己的开发服务器以及git服务器非常缓慢,ping值破千ms,在服务器上top看下来,也找不到任何性能问题,满头疑惑。

后来测试支付宝接口的时候,发现原本可以走通的支付宝接口,在成功支付的情况下,支付宝服务器没有给我们调用回调函数,反复查找原因都没有找到。

多种异状结合在一起,终于开始想到,会不会是名称解析的问题。果然,测试代码改用IP而不是域名作为接口后,速度大大加快,支付宝的回调也成功接收到了。更为可观的是,由于测试用例中大量使用域名作为数据库地址,原本10分钟才能跑完的测试用例,现在仅需几秒钟就能完成。

这才意识到名称解析对于提高服务性能的重要性,开始考虑切换到使用国内名称解析了。不想做免费广告,就不提目前试用的是哪家的服务了。总之先试一试,看一看服务的情况如何,再做进一步的考虑。

linux 文件目录权限批量处理

在linux上有时遇到这样的需求,希望将一个目录下所有子目录的权限批量设置为新的权限。
这其实很容易,只需chmod -R XXX path即可。
问题是,如果我们只想让其他用户可以访问到这个文件目录,同时需要保持原有的读写权限,该怎么办呢?
使用chmod的权限字母表示法,可以表示该文件当前的权限

The permissions that the User who owns the file currently has for it	u
The permissions that other users in the file's Group have for it	g
Permissions that Other users not in the file's group have for it	o

通过以下符号可以表示将要设置的文件权限

User	                           letter
The user who owns it	                u
Other users in the file's Group	        g
Other users not in the file's group	o
All users	                        a

通过这个就可以知道我们想要的命令


chmod -R a+u path

这样就可以让所有用户都按照文件当前用户权限访问。

参考:
[1] chmod man page

香港佔中事件的一点感慨

十一期间最关注的就是香港的佔中事件了吧。我不是港人,也一次都没去过香港,这个事情我没有什么品头论足的权利。

不过还是有些感悟,自言自语,也不会妨碍到谁。

1. 在近来埃及政变、乌克兰内战的此时,如果说相似的事件不引人联想是不可能的。我自然不知道香港的情况是怎样的,不可能妄下断言,但是对“非暴力不合作”这样的旗号确实有些厌烦了。

2. 无论“民主”好还是不好,香港、包括大陆的政治模式是否会逐渐改变,是“改善”还是“恶化”,我都无法去猜测。但可以想到的是,游行示威,和平集会,一定是会越来越多的,不光是香港、也包括大陆的大中小城市。这可以说是一种自然发生的情形,无需去考虑它到底是好还是不好,我想这也无需去论证。所以无论对于民众而言,对于政党而言,如何去面对这样的环境,应当学会去适应,这可谓是生存技巧。而掌握这种技巧,对于民众的要求而言,需要更关心政治,并且学会独立思考,否则谁知道你上街游行甚至被逮捕,是为了自己的利益,还是被他人所利用。而对于政府而言,则意味着对此类事件以更聪明和谨慎的方式处理,对不同的声音更加宽容。我想,如果有了这样的变化,大家对这样的环境更加适应,总的来说,还是向好的方向发展的吧。

3. “通”而不是“堵”,是我们古老祖先留下的智慧,否则在这块华夏土地上存在并且壮大起来的恐怕也不是自称炎黄子孙的族群了。这个道理,我想无论对于想要解决事件的一方,还是对于想要有所诉求的一方来说,都是重要的。西洋人会问“存在还是毁灭”,我们的祖先却说“一阴一阳之谓道”,这是我们血液中流淌着的生存哲学。如果翻译成现代汉语,当你问我“民主还是专制”的时候,我回答你“民主即是专制,专制也即民主”,你一定会骂我狗屁不通,但这样的“愚”中或许藏有“大智”。

说到这里忽然非常感慨小平先生对我们国家所用的救命良药,充满了我们先人的大智慧。如今我们仍然需要同样的智慧,才能让我们的民族继续生活在这样一片我们所热爱的土地上。

DSS 源码阅读指南[0]

最近在读Darwin Streaming Server源代码,刚刚开始,随意记录一些信息。

DSS 代码非常旧,MacOS forge上的版本还停留在6.0.3,看样子应该是2003年的陈年老酒,不过即便如此还是有许多值得学习的地方。

说到阅读源码,是让人感到压力山大浑身不自在的事情,无论是别人写的,还是自己写的。好在DSS的Documentation目录下有许多非常值得阅读的资料,可供参考。读完 AboutTheSource.html , DevNotes.html, FAQ.html 和 QTSSAPIDocs.pdf 之后,感觉DSS的代码框架已经一目了然,并且有迫不及待去看相关代码实现的冲动了。

DSS的RTP实现建立在所谓“reliable UDP”之上,何谓“可靠UDP”,让人非常疑惑,在Documentation中找到Reliable RTP Whitepaper,可窥端倪。网上也有介绍苹果自家的“reliable UDP”的设计、实现的相关介绍,可以参考。

这些文档从系统设计的角度介绍了DSS的全貌。系统设计一直是我很感兴趣的一个话题,DSS的系统设计理念很值得学习和思考。模块化、异步执行(不阻塞)、消息队列与派发,这些也都是现代服务器/操作系统的设计理念。

这是阅读学习DSS代码的一篇小小指引,后续会逐渐增加一些自己的发现。

dss 编译问题小结

主要遇到两个问题,比较有通用性,记录一下:
1. -lpthread 修改为 -pthread
2. -ldl 修改为 -Wl,--no-as-needed -ldl

在linux编译遇到

undefined reference to pthread_mutexattr_init
undefined reference to dlopen

可能跟这两个问题相关。

痴汉人生

一转眼发现已经快半年没有写博客了,上一篇bo还是去年10月份的。

自我安慰一下,这一段时间倒没有虚度,还是读了不少书,见了不少世面的。对人生的看法也慢慢有了新的理解。最近读了一本书,《醉汉的脚步》,我非常喜欢。这是一本讲概率的书,我本是因为,对于“随机性”这个问题,其真实的含义到底是什么,没有足够的认识,正好在网上看到这本书的风评很好,就买来读读。没想到这居然成了一本洗刷我的三观的书。

随机这个东西,究竟是什么意思,一直找不到最好的方式去理解。当然并不是说数学课讲的期望啊方差啊这些东西没学会。不是的。问题是这些都只是从一个侧面去描写一种随机分布的指标,却仍然不能知道随机事件发生时,到底是什么样子的。比如抛一枚硬币,1/2概率是正面,1/2概率是反面。这个1/2究竟是什么意思呢?并不是说,每抛两次硬币就会出一次正面。说抛100次硬币,出现50次正面的概率最大,这种说法似乎是对的。但假如前99次中出现50次反面,49次正面,也不意味下一次抛硬币出现正面的概率会更大。事实上,假设前99次连续抛出反面,下一次抛硬币,出现反面和出现正面的概率还是一样。理论上会计算概率,并不表示直观上理解了概率到底是什么意思。

读《醉汉的脚步》这本书,让我收获颇丰。其第一点,就是告诉我,概率是用来分析那些人类理解不了的事物的。文中提到,人类会习惯性的寻找规律,其中的例子很有趣,给被实验者观察一串连续的颜色变化,红绿红红绿红红红绿……实际上,红色和绿色出现毫无规律,完全随机,只是红色出现的概率是2/3,绿色出现的概率是1/3。但是受试者会情不自禁地分析这段颜色出现的规律,例如上面一段,可能会分析出下一组应当是红红红红绿。因此如果让受试者去猜下一个颜色是什么,很可能会猜错。而同样的实验受试者换成老鼠,它猜测下一个永远会猜红色,因为红色的几率高嘛。结果老鼠的得分反而比人类的得分高。

这一下就解决了我自认“理解不了概率”这个问题。我所说的“理解不了概率”其大概的意思,或许是说,即使知道了概率,也还是不明白按这样的概率运行的事物(无论是抛硬币、股市、足球的胜负还是人生的浮沉、世界的治乱),其运行的规律究竟如何。而这一解释是自明的,正是因为无法明了其运行的规律,才不得不用概率去归纳。概率是无需“理解”的,只需“知道”即可。如果硬要“理解”,就像上述实验中的人类受试者一样,硬要从一堆随机的信号中寻求规律,结果反倒输给老鼠。

这本书给我的另一个收获,是“贵在坚持”的另外一种解释。概率,是要通过统计,得出的。而概率影响的范围之广,上至全球金融、政治,下至日常生活,黄油面包,无处不在。连续抛3次硬币,都出现正面,并不能就此确信,这个硬币不平衡。连续3次猜拳输给对手,并不能认为对手一定有超能力。然而,连续3年业绩低迷的基金经理人会被开除,连续3次点球罚失的球星会坐冷板凳,或许连续3次失恋的少男少女也会对生活失去希望。而岂不知人生处处都有随机性存在,而区区3次失败,并不具备任何统计意义上的证据可以说明被观察的对象本身品质有严重缺陷。可惜的是人生短暂,往往等不及几十次上百次的测试,再去挑选那个千锤百炼的好手。俗语说,事不过三,往往以三次为界。这也是无可奈何的事情。只是作为审查者,不应只根据业绩、成绩,去评价一个人,而更多从多方综合因素去冷静分析评估;作为被审查者,即使遭遇连续的挫折,假定相信自己的路是有道理的,就不应仅因为业绩、成绩,就放弃自己的路。当然这是万难的。这也是因为,人类习惯性的从现象中试图分析出规律,而往往许多事情,股票的走势、考生的成绩、足球的比分,其实没有什么办法可以准确预测的。当你看到连续的下跌,或许这仅仅是一个波动。

这让我想起庄子的话,“朝菌不知晦宿,蟪蛄不知春秋”,“小智不及大智,小年不及大年”。人生短暂,像是用放大镜去观察印象派油画的一角,看到的仅仅是斑斑驳驳的色点,可谓瞎子摸象,管中窥豹。当然,这是人类固有的局限性,不可能就此超越。但知道自己具有这样的局限性,“认识你自己”,总比认识不到自己的局限性要略好一些吧。犹如五十步笑百步,如能知道自己是五十步,而能不笑百步,就可以说是巨大的进步了。

当然这本书内容很丰富,绝不仅这一点营养。只是拿出我最受震动的部分记录一下而已。也是因为自己荒废博客许久,作为给自己的一个小小的交代吧。接下来的时间里,还是要不停地学习不停地进步。争取博客也不要荒废,更多地记录自己的见闻吧。

EFI/GPT上的Windows – Linux双系统

很长一段时间都没有鼓捣重装系统了。前些日子旧笔记本坏了,买不起MBP的咱只好入手了一台惠普的win8系统的本本。

近来需要一个linux环境进行线下测试,这台本本就成了实验对象。本以为装个linux还不简单么,装个ubuntu分分钟搞定的事情,没想到居然屡遭挫折。最简单的方法就是通过easybcd软件进行硬盘安装了。然后遇到的第一个难题就是怎么重启机器。win8新的电源管理模式,所谓关机其实只是打瞌睡。网上查到在控制面板-电源管理中可以关闭这个功能。OK,重启。进入引导界面却引导失败,提示找不到文件。这个问题纠结了很久,我以为是自己填写的磁盘号错了,通过存储管理可以看到除了系统可见的C盘D盘E盘外,还有好几个分区,此时我还没了解到这是GPT分区表的特征,从(hd0,0)到(hd0,5)试了个遍,通通无效。

经过这么多纠结我终于决定放弃硬盘安装,转而采取刻录光盘安装,还好新本本光驱还没坏(不得不吐槽下本本光驱的脆弱身板。。)。结果再次挫折。。光驱引导界面进去之后无法进入到liveCD的试用模式,也无法直接安装,于是使用检测光盘那个选项吧,统统黑屏,进去光盘也不转。试用模式的最后会输出两行字,说是无法从cd0的某扇区读取数据。莫非是烧CD烧坏了?于是再烧一张,并且烧写结束的检测光盘步骤也检测正确了,ubuntu的iso检测checksum也检测正确了,应该没问题了吧,用新盘重试,还是一样的问题,甚至提示错误的扇区编号都是一样的,我看着状况应该不是烧写的问题。

只好再度重启去BIOS看看了(其实应当是叫做EFI,这个时候我还没意识到这个问题)。发现boot option中有一个选项是开启legacy support模式。注解中说这个模式用于引导win7及以前版本,推荐不要开启可能会导致(win8)系统无法引导。我晕,估计问题就出在这了。首先先不管一层层的警告先把这个选项开了,然后我也注意到这说明这台机器的引导方式已经与以往不同了,以往的引导方式已经是legacy了。。

重启之后还有提示,说有延迟的修改,不顾各种警告允许修改生效,再次通过liveCD引导,终于可以进入熟悉的ubuntu试用界面了。不管那么多进入安装。根据ubuntu以往的尿性,应该是对windows超级友好兼容,不用担心引导项问题的(我还是没意识到EFI这个问题的严重性)。结果装好发现就是败在这个疏忽上了,grub没有正确地安装,还是直接进入到windows引导界面了。

不过这时候已经知道病因了,那么在网上求医问药就比较顺当,找到了许多文章,介绍了许多不同的解决办法。我采取的是这篇文章中介绍的办法,通过liveCD进入ubuntu后,挂载sda2,这是gpt的引导分区。其文件夹结构为


<DIR> Boot
<DIR> HP
<DIR> Microsoft
<DIR> Ubuntu

其中Boot文件夹中有Bootx64.efi文件,Microsoft/Boot文件夹下有Bootmgfw.efi文件,Ubuntu文件夹下有grubx64.efi文件。可以看出Ubuntu安装程序已经安装了Ubuntu的引导文件,可是EFI系统没有识别出新的引导项,依旧直接启动了windows的引导文件。由于对EFI也是昨天折腾这个事才刚从网上了解,我还说不出所以然,为什么Ubuntu的安装程序无法正确替换windows引导文件,并且事实上ubuntu自动生成的windows引导项也是错的无法启动windows。应该说是新设备加上新系统吧,兼容工作不可能那么快跟得上。

网上提供的另一种解决方案是将linux引导信息写到U盘,通过U盘启动linux,这样做就避免修改windows的引导文件,应该说是方便企业级客户吧。这个方案来自linuxsir.org。可是对我来说太麻烦,不想采用。于是还是使用了上面引用的方案,将Ubuntu引导文件强行替换windows的引导文件,并将ubuntu的grub中增加一个项目,指向原来的windows引导文件(备份更名),从而解决问题。不得不佩服下这个方案的作者,这样强行替换引导文件确实是需要足够的知识和勇气的。。。

这里说明一下,我的系统引导文件分区与文章中一样是sda2,但可能不同系统不同,可以分别挂载查看一下目录结构就能确定具体是哪个。

好吧,长篇的唠叨说完,总结一下要点:

  • windows 8 电源管理关闭快速启动策略
  • EFI(BIOS)设置中启用boot options/legacy support(网上说法还需关闭secure boot,我是后来才看到,没有采用)
  • 完成ubuntu安装后用ubuntu的grubx64.efi文件替换windows的bootmgfw.efi文件(注意备份)
  • 在ubuntu的grub配置中增加指向原bootmgfw.efi的条目
  • 当前版本的easybcd暂不支持efi/gpt引导方式(我使用的是2.0.2,文章中称2.2也不支持)

后记

总结完才意识到可能由于我没有关闭secure boot才导致ubuntu引导项安装失败以及easybcd设置启动项失败。由于昨天经历漫长的战役已经重启笔记本30次以上消耗了一整天的时间,暂时没有心情去继续研究了。附上似乎是比较官方的介绍easybcd双启动linux和win8的文章供参考。
how to dual boot windows 8 and linux

最后是参考来源:

python urllib2 重定向时获取cookie

最近用python写一个简单的爬虫,在模拟网站登录时遇到问题,就是登录后紧跟着302重定向,这时候cookie获取没做好,就会登录失败。

网上找了很多文章,可能是因为python版本不同之类的原因吧,很多方法试了都没用。这里踏破铁鞋找到了可用的方案,记录一下,希望能帮到后来的朋友。
鉴于python的版本浆糊问题,这里声明下,我使用的python版本2.7.3,并且使用的是urllib2库。

解决这个问题,其实说起来很简单,就是要自己定义一个RedirectHandler,在创建opener的时候作为参数放进去。

    cj = cookielib.LWPCookieJar()
    opener = urllib2.build_opener(MyRedirectHandler,
                                  urllib2.HTTPCookieProcessor(cj))

这里我还要记录cookie信息因此还使用了cookiejar。
现在难点就在于这个RedirectHandler如何重写了。

    urllib2.HTTPRedirectHandler.http_error_302(self, req, fp, 
                                               code, msg, headers)


通过这样一句调用,默认的redirectHandler已经支持在发现302头部的时候自动跳转到新的location去。但问题是返回的response中缺少了前一个请求返回的cookie信息。因此重点是如何在redirect handle中把前一个请求的cookie设置到新的请求中去。

class MyRedirectHandler(urllib2.HTTPRedirectHandler):
    def http_error_302(self, req, fp, code, msg, headers):
        setcookie = str(headers["Set-Cookie"])
        cookieTokens = ["Domain","Expires", "Path", "Max-Age"]
        tokens = setcookie.split(";")
        for cookie in tokens:
            cookie = cookie.strip()
            if cookie.startswith("Expires="):
                cookies = cookie.split(",", 2)
                if len(cookies) > 2:
                    cookie = cookies[2]
                    cookie = cookie.strip()
            else :
                cookies = cookie.split(",", 1)
                if len(cookies) > 1:
                    cookie = cookies[1]
                    cookie = cookie.strip()
            namevalue = cookie.split("=", 1)
            if len(namevalue) > 1:
                name = namevalue[0]
                value = namevalue[1]
                if name not in cookieTokens:
                    cookiemap[name] = value

        newcookie = cookiestring(cookiemap)
        req.add_header("Cookie", newcookie)
        return urllib2.HTTPRedirectHandler.http_error_302(
                         self, req, fp, code, msg, headers)

这里我处理cookie使用了比较土的方法手动解析的,各位如果有更好的方法也请不吝赐教。
重点其实就是那一句req.add_header(“Cookie”, newcookie),这里是从set-cookie中解析出cookie串后拼接成请求头填入req中,在接下来的一句调用默认handle的函数时请求头就会附有新的cookie信息了。

如此处理后的请求,返回后即可从response中读取出cookie信息

    response = opener.open(request)
    cookies = cj.make_cookies(response, request)

Haskell初心—认识Monad

从三月份开始看Real World Haskell这本书,断断续续看到7月份,总算初步对Haskell有了一些认识。
我想,学习Haskell这门语言,第一个门槛就是Monad这个概念。今天初步来做一点总结。

在读书和试着用haskell做一些习题的时候,就会感觉到,haskell是一门实践性极强的语言。当然,他同时也是一门理论性极强的语言,他允许甚至鼓励你用数学方法去推导函数签名,通过定义一系列公理可以对特定类型的函数式进行数学变形,以达到最短最优美的代码形式。

但对于我而言,那些过分抽象的函数和概念,非常难以理解,并且即使是形式上理解了他的定义,还是无法理解他为何要如此定义,有什么意义。Haskell代码的特点在于极端的精炼,许多函数定义看上去什么都没做,像是在说废话。幸好RWH是一本非常好的教材,他通过许多实例来说明这些抽象定义在实践中的用途,让你看到许许多多在实际代码中经常遇到的痛点,在Haskell中都有解药。如同醍醐灌顶般爽快。(请参见代码交叉拷贝悖论

Monad就是Haskell的一个经典设计。它来源于“范畴论”,可以说是数学中的数学,一坨“抽象废话”。我先不去考虑他的数学含义,单看他在代码中如何化繁为简。这里我抄一段RWH中的代码(略简化)。

data MovieReview = MovieReview {
      revTitle :: String
    , revUser :: String
    , revReview :: String
}

simpleReview :: [(String, String)] -> Maybe MovieReview
simpleReview alist = 
    case lookup "title" alist of
      Just title@(_:_) ->
          case lookup "user" alist of
            Just user@(_:_) ->
                case lookup "review" alist of
                  Just review@(_:_) ->
                      Just (MovieReview title user review)
                  _ -> Nothing         -- no review
            _ -> Nothing               -- no user
      _ -> Nothing                     -- no title

这一段代码是说,有一个电影影评的类型MovieReview,包含revTitle,revUser,revReview三个字段。现在用一个association list来对他进行初始化,simpleReview函数中的case语句略似if else,我们可以看到这段代码的大意是说如果alist中含有title、user、review三个键值并且对应的字段不为空,则用这三个字段初始化MoviewReview类型,否则依次返回Nothing (Maybe是一个包装类型,它的值可以是对应类型的值,或者是Nothing)。
这里是采取了最传统的方式,展现了写代码时经常遇到的苦恼。三层if判断,里面的操作很雷同,应该有办法抽象出来。在C语言中我会采取的办法是把title, user, review三个键值存在数组里,并且通过一个循环来依次判断,虽然实现了重用代码,但是代码可读性变差了。代码主体内充满了类似

auto moviereview = new MovieReview (alist[keylist[0]], 
                                    alist[keylist[1]], 
                                    alist[keylist[2]]);


这样的代码。我们来看看Haskell如何通过monad实现对这样的代码的抽象。实际上Maybe类型就是一个Monad。一个Monad可以简单理解为一个包装类型,他包装了一个变量(可能是函数、操作、状态等等),并且提供一个“>>=”运算,这个运算的意思是,把它包装的变量提取出来,当作参数传入一个可以接受这种类型参数的函数中,而这个函数的返回值必须也是monad包装的。
另外Monad还需提供一个return函数,方便其他函数将原始类型包装为Monad类型。

class Monad m where
  -- chain
  (>>=) :: m a->(a -> m b) -> m b
  -- inject
  return :: a -> m a


为何要这样定义>>=运算,一个monad >>=运算将一个Monad (m a)中的变量(a)扔进一个(a -> m b)函数中,生成一个新的Monad,它包含的变量类型跟传入的可能不同(m b)。这个m b还是Monad,就又可以继续通过>>=方法扔进新的(a -> m b)中。并且这里特别将输入类型m a和输出类型m b分别表示,从而可以将许多不同的方法通过>>=连接起来。就好象一个Monad被送上了生产线,经过一道道工序加工它本身的类型也可以经过许多变化,最终输出一个m b。

这个定义非常优美,我相信c++中的输入输出流操作符就是从这里借鉴的。不过这个流可不仅仅能做输入输出的操作,它可以控制任何一种操作流,非常神奇。因此在RWH中称>>=算符是一种可编程的分号 每个操作后面用>>=结尾可以对应于命令式语言中的;分号,在haskell这样的函数式语言中,通过monad实现了命令式的编程范式。更加强大的是,他的可扩展性远高于传统的命令依次执行,具有灵活的可操控性。

Maybe就是一种monad。他的>>=方法将其包装的变量提取出来,给后续的函数作为参数。如果为Nothing则短路,直接返回Nothing

(>>=) :: Maybe a ->(a -> Maybe b) -> Maybe b
Nothing >>= _ = Nothing
Just v >>= f = f v


通过这一层包装可以很好的将前面的一段代码简化,依次处理title, user, review三个字段,通过>>=这个“可编程的分号”来控制操作流。如果任何一个字段出现Nothing,则后续的>>=函数都会返回Nothing,不会继续调用lookup函数。

simpleReview alist = 
  lookup "title" alist >>=
  \title -> lookup "user" alist >>=
  \user -> lookup "review" alist >>= 
  \review -> Just (MovieReview title user review)  


可以看到代码已经充分简化了。但Haskell的设计者还是不满意,这样的代码还有重复之罪。如果我们可以直接将(lookup “string” alist)作为参数,放进MovieReview的构造函数的话,就可以一行代码实现这个任务了。现在遇到的困难是,lookup返回包裹在Maybe中的量:可能找得到,可能找不到。而MovieReview的构造函数,仅当三个查找都不为空的时候才应当调用,并且其参数应当是解除包装的字符串类型。如何将Monad包裹的值当作参数传入普通的Pure function中呢?

这里Haskell引入了Functor的概念:

class Functor f where
    fmap :: (a->b) -> f a -> f b


他将一个普通的函数(a -> b)提升为对包装类进行操作的Functor (f a -> f b)。这里f是包装函数。这样使得任何包装类型都可以轻松继承所有pure function的代码。在Monad中定义了这样的一个提升操作,称之为liftM

liftM :: (Monad m) => (a -> b) -> m a -> m b
liftM f m = m >>= \i -> return (f i)


这样任何一个f都可以对Monad中包装的值通过“提升操作”进行调用了。这里的liftM仅针对单参函数(a -> b)。我们的MovieReview需要3个参数,这里我们可以借用haskell函数库中的liftM3,针对3参数函数进行提升。

liftM3 :: (Monad m) => (a -> b -> c -> d) -> m a -> m b -> m c -> m d
liftM3 f m1 m2 m3 =
    m1 >>= \a ->
    m2 >>= \b ->
    m3 >>= \c ->
    return (f a b c)

那我们的代码又可以进一步简化了

liftReview alist =
    liftM3 MovieReview (lookup "title" alist)
                       (lookup "user" alist)
                       (lookup "review" alist)


看,经过提升之后MovieReview就像是可以接受三个Maybe作为参数一样地进行调用了,并且>>=符隐藏在了liftM3中,其中控制了一旦任意一个maybe为nothing的时候短路这个流程。虽然这样client代码已经非常优美了,但是lib代码会有些问题。这里的liftM3已经有些呆板了。如果是要提升10个参数的函数怎么办。精益求精,Haskell提供了ap函数让提升操作可以链状调用。

ap :: (Monad m) => m (a -> b) -> m a -> m b
ap  =  liftM2 id


从函数类型可以看出,他要求将pure function (a -> b)包装到monad中。这样做可以进行链式调用。可是为何通过这样的链式调用就能将多参函数链式提升了呢?这是因为实际上haskell仅支持单参函数。例如(a -> b -> c -> d)这个类型,他表示接受(a, b, c)三个参数,并且返回d类型的函数。这个函数实际上是接受a类型的参数,并且返回一个(b-> c -> d)类型的函数的函数。这有点像是通过接受了参数a来绑定了三个参数中的一个,生成了一个偏特化的函数(partial function)。
ap函数类似是通过提升将这个多参函数的第一个参数提升了,返回用monad包装的偏特化函数(m b)。这个新的被monad包装的函数可以继续传入ap,继续偏特化下一个参数。因此这个操作可以链式提升多参函数的每一个参数,最终完成提升过程。这里ap的名字意思是“apply”,就能清楚他的意义了。
ap实际上通过liftM2 id实现。id是返回本身

id a = a


而liftM2提升两个参数并返回调用f的包装结果,现在f是id

liftM2 :: (Monad m) => (a1 -> a2 -> r) -> m a1 -> m a2 -> m r
liftM2 f m1 m2 =
    m1 >>= \a1 ->
    m2 >>= \a2 ->
    return (f a1 a2)

liftM2 id = 
    m1 >>= \a1 ->
    m2 >>= \a2 ->
    return (a1 a2)   -- id means self


这里有个不太符合直觉的地方,return (a1 a2)就是m (a1 a2), 他怎么就变成ap的m (a -> b) -> m a -> m b了?
这要看传入的参数。ap接受两个参数分别是m (a -> b)和 m a。用这两个参数替换liftM2的a1和a2后

liftM2 id (m (a -> b)) (m a) = 
    m (a -> b) >>= \(a -> b) ->
    m a >>= \a ->
    return ( (a -> b)  a)


经过这个推导,可以清晰地看出,liftM2 id将ap的第一个参数m (a -> b)提升后绑定了第一个参数a,也就是ap的第二个参数m a。并且返回了绑定参数后的偏特化函数b的包装monad, m b。这个绑定的过程可以在最后一句体现出来:return ( ( a -> b ) a)。对(a->b)应用参数a。
从这里可以看出monad是一个极端抽象的概念,讨论他经常会遇到不易理解的地方,只有带上实际使用的参数后才能容易理解。

利用ap函数的代码如下,这里使用`符号包裹一个双参数的函数,将函数当作操作符来使用,从而得到链式调用的代码

liftReview alist =
    MovieReview `liftM` lookup "title" alist
                   `ap` lookup "user" alist
                   `ap` lookup "review" alist

好了,做个小结。我刚学haskell不久,对monad有了一个初步的了解。可以看到monad是一个实践性非常强的抽象,涉及到的lift, functor等概念,如果只是形式上地看他的设计,很难理解设计的意图,就像是一坨抽象废话。但是一旦从实践中多利用这些工具,就能轻松的避免各种各样的代码坏味,得到美观自然和可读性非常强的代码。Haskell这样的语言,只要用心设计好函数和变量名,即使不写注释也能明白代码的意图,并且很难出错。因为他把实现隐藏起来,代码就是直接表达了意图。我认为这是非常有潜力的语言,今后会做进一步的学习和研究,并尽快将其用于实战。

windows使用其他版本java

换到自己的笔记本上做安卓开发,出现了一个很烦人的问题,就是一直报
Can't bind to local 86XX for debugger
这样一个错。网上查了一下,原因是jre7不兼容的问题,需要退回到jre6才行。我安装了jre6,又不想卸载jre7,就想,改了环境变量的PATH应该就好了吧。

JAVA_HOME = "C:\Program Files\Java\jre6"
PATH = "...;%JAVA_HOME%\bin"

可是修改之后,进入命令行输入java -version一看,问题没有解决,还是java 1.7的版本。这很奇怪啊。
java_in_system32
网上继续查了一下,原来java installer会把java.exe复制一份到windows/system32下面,而PATH的特点是先到先得,在系统目录找到java.exe后,就直接执行了。需要把jre\bin的目录在PATH中设置到系统目录之前,就解决问题了。

JAVA_HOME = "C:\Program Files\Java\jre6"
PATH = "%JAVA_HOME%\bin;..."

顺带再骂一句java,每个主要版本升级都闹出一堆不兼容来,不知怎么想的。一处开发到处执行这个口号唉。。。不提。。
又记起那时候在公司配置Jira系统的痛苦了。。

参考