nutshell

学会吃坚果,绝对是人类文明的一大进步,其重要程度,丝毫不亚于登上月球。要吃到坚果,必须先去皮,吃肉或去肉,撬开果壳。这一切的前提,还包括了种下一枚种子,耐心等其长成果树,开花结果,收获果实。

 

 

约一个月前,把去年帮bangumi做的条目相似度算法(她一直没用-A-),刨出来进行重构。去年用的是php+mysql这最简单方案,今年本着反正也不一定真的会用的自暴自弃精神(sai姐姐只允许我使php+mysql),开始对使用的语言进行纠结,最后用了scala,中间还试过jruby(我试那时不行,后来我自己git pull&ant,也有不少问题,主要在YARV的API和unicode处理上的。不过现在的1.6.0已经相当靠谱了)。

 

先说算法,去年因一开始计算的结果效果不好,大概是因为打分数据太少的缘故。于是想到先生成用户打分模型,对打分数据不足条目进行模拟打分,然后再计算相似度。虽然效果有很大改善,但是要慢上不少。这次重做发现其实单纯的计算相似度并不理想的原因,很大程度上是对于主流和长尾没做区分引起的。

举个例子:例如EVA这样的东西几乎所有人会给它打高分,然后给EVA打高分中的一小拨人同时又是某种重口恶趣味作品的狂热者,于是地球上同时看过EVA和某个非主流作品的就是那么一小撮人,而且给两者都打了高分,根据算法理所当然它们就有很高的相似度。

于是根据Power-Law,将它们区别开来,这算是消除噪音,就可以得到很好的效果。具体结果见此:http://bgm.tv/group/topic/4602。

不过对于计算用户之间的相似度,是否应该反幂律分布,强调长尾,我还在试验中,而且对于用户相似度,没有一个客观的评价标准。

 

再有的改进,用FP风格写的代码,可以实现对某个特定条目进行1toN的相似度更新(当然还是做不到Nto1,因为AtoB和BtoA的相似度很可能不是同一个值,我知道这很不符合逻辑,不过这是各种权重之后的必然结果)。另外更为重要的可以实现MapReduce式的分布式计算和多线程等。这就要提到scala。

 

这里还是应该隆重介绍scala,它令人喜爱的东西实在太多了。类似周末交流会,我在此仅快速地介绍它的两处精妙之处,都是我实际使用场景,分别对oop和fp风格,同时是地道的scala特色的。

隐式转换:

我们知道scala是一个强类型语言,习惯上来讲强类型语言总会被认为相当不方便,所以大部分现代脚本语言都使用了弱类型系统。但scala不同,首先这么做可以在编译阶段就纠正一些类型混乱的低级错误,也就是所谓的编译器检查。然后scala提供了非常酷的隐式转换特性,以下在SQL查询场景中说明,这里使用java.sql._:

 

import java.sql.DriverManager.{getConnection => connect}
Class.forName("com.mysql.jdbc.Driver")
implicit val conn = connect("jdbc:mysql://localhost:3306/tomodachi?user=root&password=")
results = conn >> "SELECT * from tomodachi_rates LIMIT 1"

 

下面开始说明,DriverManager.getConnection返回是一个Connection对象conn

我们知道在java中Connection对象要查询并且返回结果还先创建Statement,这个conn >> “SELECT * from tomodachi_rates LIMIT 1″是怎么实现的呢?秘密如下:

 

implicit def conn2Mio(conn: Connection): MioConnection = new MioConnection(conn)

class MioConnection(val conn: Connection) {
  def >>(sql: String) = new MioStatement(conn.createStatement) >> sql
}
class MioStatement(val s: Statement) {
  def >>(sql: String) = { s.executeQuery(sql) }
}

 

其中的implicit是重点,表示隐式转换,一个隐式转换应该明确定义输入输出类型,这里的conn2Mio,遍是输入一个Connection并用它创建一个MioConnection对象

然后MioConnection对象的>>方法再由conn.createStatement创建的Statement对象创建一个MioStatement对象,并且调用它的>>方法,执行查询,返回结果

执行的时候发生的情况便是scala查找到conn作为Connection没有>>方法,于是便把他通过implicit方法转换为MioConnection,并且调用它的>>方法

很酷吧,仅仅一次就实现了从连接中创建Statement并且进行查询并且返回结果的操作

上面的代码同样可以改成

 

implicit def conn2Mio(conn: Connection): MioStatement = new MioStatement(conn.createStatement)

class MioStatement(val s: Statement) {
  def >>(sql: String) = { s.executeQuery(sql) }
}

 

不过考虑到很多时候你需要把MioConnection转换回为Connection使用,所以实际情况中并不常会这么做

Scala的隐式转换除了这么酷之外,在scala中会重新定义java的基本类型,并且通过此进行无缝衔接。还有的常规的用法是代替java中的toString等,可以在要求不同类型的不同方法之间传递,而不破坏代码的可读性。同时配合scala强大的模式匹配,实现更酷的东西。

 

foldLeft/foldRight:

在之前的基础上:

val sum = (0 /: (conn >> "SELECT * from tomodachi_rates LIMIT 999"))(_.getInt("rate") + _)

implicit def resultSet2Mio(rs: ResultSet): MioResultSet = new MioResultSet(rs)
class MioResultSet(val rs: ResultSet) {
  def foldLeft[X](init: X)(f: (ResultSet, X) => X): X = rs.next match {
    case false => init
    case true => foldLeft(f(rs, init))(f)
  }
  def /:[X](init: X)(f: (ResultSet, X) => X): X = foldLeft(init)(f)
}

 

这表示什么?

第一次看到这玩意的人可能会有点傻眼

上面一句同样可以写成

val sum = (conn >> "SELECT * from tomodachi_rates LIMIT 999")).foldLeft(0)(_.getInt("rate") + _)

首先在scala如果方法以:结尾,那表示可以把调用方法的对象放在方法的后面,所以/:其实就是foldLeft的一个别名

上面一句中的foldLeft表示的就是从0开始,对那个SQL查询的结果集进行左折叠,折叠方式是(_.getInt(“rate”) + _)

这个折叠方式的lambda是缩写,拆开之后应该是{(row, sum) => row.getInt(“rate”) + sum}

这就好理解了,foldLeft会用lambda递归结果的每一行,并且返回rate字段+sum,第一次调用这个lambda,sum的值0,到下次一次调用时,sum值变为上一次lambda的返回值。

所以,这是求和方法,我知道SQL语句中可以直接用SUM()求和,可是这也很酷不是吗。例如CouchDB的查询就是传递一个javascript lambda,代替常规SQL语句进行各种操作。

foldLeft/foldRight在scala的容器们中都预先定义好的方法。如果有需要你完全可以在支持FP的语言中实现这个便利的方法。有了这些,就不需要LINQ之类的玩意了。

scala的函数返回可以为一组值,所以foldLeft的便可以做到很多了。如果是MapReduce分布式计算,你显然可以把节点1的foldLeft最终值,用来初始化例如下1000条的foldLeft。配合提供了远程actor功能的akka,这完全不需要中间暂存,而且是完全基于消息传递实现的。很酷吧。

 

看到scala这么多很棒的特性,还有更多时髦的语言,再回头看看php和java什么的,你会完全不想使用它们了。可现实情况却不允许你这么干,scala还好,因为它在jvm中,可以和java互相调用。

可是php呢,现实情况是,如果一个团队一直使用着php,你让他们使用rails或者django或者grails之类的酷玩意,要求他们彻底重构,他们会把你吊起来烤了,就像中世纪人们对待女巫一样。

目前除了一些新的创业团队,很多时候你还是不得不面对php的(最近我遇上半打人,他们都在做着各种wordpress或cms再开发,连找个php框架重做都不肯)。

 

结合自己的情况,多半是出于任性,于是我便萌生了车一个php代码生成器的念头:

———————————

主要是要解决php的表现力不足,同时可以方便地在没法使用新平台的情况下继续兼容php

 

* 语法简化

* 告别<?php ?>,纯净php代码,反正大家都会用模版,内嵌html是个糟糕的做法。php6也有此改进

* 每一行末的;完全是必要的

* 变量名的$号完全是没有必要,只要全局常量全大写,就不会混淆

* 类名大驼峰,方法名小驼峰,为了一些语法糖不会混淆,这个必须强制

 

* 类

* 为了纯洁的oop,放弃static,改为类的伴生对象

* 为了纯洁的oop,例如str_replace(…),改为str.replace(…),在字符串对象上调用方法。php6或者7也将如此

* 类的访问控制如public private protected,应该像时髦的语言那样,尽可能少写。即默认public,一个private可以连续修饰好几个方法

* 为了更好的oop,添加mixin功能。php6或者7也将如此

* 基于上两条,同时提供更加细致的访问控制,可以用来专门指定对某个类及其后代的访问权限

* 调用方法中的->改为.或空格,括号不是必须的

* (2012)push->arr,如此实现从右到左调用方法,把2012插入数组arr后端,省略括号后为2012 push->arr

 

* 方法

* function -> def

* 不变性原则,绝对不能传递引用

* 无上限参数列表用*更直观,而不是在方法中func_get_args()

* 可以有多个返回值

 

* 静态类型

我目测写php的时候一般至少有20%的单元测试代码不干别的,专门测试类型是否正确

同上,还有更多(有时候是80%了)的phpdoc注释不干别的,专门标示方法参数的类型和返回值的类型

所以静态类型虽然看上去很难用,但是有助于减少体力劳动,所以:

* 语法:str1: String = “hello”,用java们的空格方法显然太难看

* 声明时,方法的输入输出一定要显式指定类型,但对于lamdba可选,因为它们大概不需要被单元测试和生成文档

* 字面量和new方法创建对象,除非特例,否则显然不必要专门指定类型

* SQL类型:我用php时,如要长距离传递SQL语句,会定义SQL类,然后执行SQL方法判断输入的类,一定得是SQL类才行。SQL类无法和字符串拼接,然后用bindParam输入参数,防止SQL注入和人为疏忽的SQL注入

* 数组的成员类型要相同,即一个成员的key是数字那么该数组成员所有key都应该是数字,字符串”1″什么的是非法的

* 隐式转换,有静态类型必然还要有它

 

* 函数式编程

虽然5.3开始支持了lamdba等,但是并不好用

* 语法:{(arg1, arg2) …},而不是function(arg1,arg2){…}

* {(arg) …}(“foobar”)的就地调用,结合前面的调用方法,(“foobar”){(arg) …}也可以,而且更美观

* 结合纯洁的oop,arr.each{}用以代替foreach和变态for等循环语法,同时arr还应该有map和reduce/collect/folding等方法

* 其他一些函数式方法似乎不是很必要

 

* 其他

* 让定义数组方法更像json些,用:而不是=>,用花括号而不是圆括号,array开头不是必须的

 

然后下面是我想到的基本语法:

一切都是对象和方法调用(类似Smalltalk),并且完全基于lambda(类似Lisp),实现各种语言的基本功能

最简单例子1+1其实是1.+(1)

包括代码块也是方法调用,所有代码其实都是一个顶层对象Source的内部调用

如果创建一个类

 

class A
  def a
    print "helloworld"

 

其实是

 

Source.class(A,{
  this.def(a,{
    print "helloworld"
  })
})

 

 

if(x == 0)
  ...
else
  ...

 

其实是

 

this.if((x == 0),{...},{...})

 

因此三目运算符

 

(x == 0) ? 1 : 2

 

其实是

 

this.if((x == 0),1,2)

 

调用方法的点是可选的,传递参数过程中圆括号可以省略,逗号可以用空格替代,花括号可以用换行缩进替代,end闭合代码块是可选的

 

arr.each({...})
str.replace("a","b")

 

可以为

 

arr each
  ...
str replace "a" "b"

 

lambda部分,具名输入为

 

{(arg1,arg2,args*)
  ...
}

 

如果

 

{
  print "helloworld"
}

 

print方法的调用对象,是上下文对象列表中的由最后一个到第一个中的一个,按顺序,第一个永远是Source

所以对一个lambda对象执行call/apply方法

 

lambda.apply(this) #定义上下文不调用方法
lambda.call(...) #传入参数调用方法

 

或者

 

lambda(...)

 

的输入参数永远是被加入到当前上下文对象列表中的最后一个

基本上就是一个lambda会继承包裹自己的lambda的上下文对象列表,并且加入新的对象,再传递给自己内部的lambda

用俗话说,就是类似闭包感觉,另外当然闭包也是存在的,唯一的区别就是不会在闭包能访问的对象上调用前面说到的print方法等

 

简单说就是糅合各种语言,集大成的山寨语法

———————————

 

我知道能用来生成php的东西有haxe(http://haxe.org/,不仅是php,它还可以用来生成很多东西,一开始似乎是为了AS)。还有正在进行中的php-snow(http://code.google.com/p/php-snow/),不过稍微看了一下后,感觉它精简过头了,例如用fn而不是def,用pri而不是print,不太符合日常期望,而且oop和fp也没有针对性改进。所以还是继续考虑自己造。

 

具体实现上,需要lexer生成器和parser生成器,hiphop用的是flex/bison,生成的是c++代码,php-snow用的是Python的PLY(Python Lex-Yacc)。而coffeescript用的js原生的bison实现jison,同时生成器的全部js代码都是用coffeescript自己生成的,这就是一个蛋生鸡鸡生蛋的问题了。

但是在php上我没有找到什么靠谱的lexer/parser生成器,于是在考虑造php lexer/parser生成器。但是主要目的是php代码生成器,而又希望用代码生成器生成lexer/parser生成器(不想碰太多php),同时代码生成器和lexer/parser生成器生成的都是php实现,然后代码生成器又依赖于lexer/parser生成器生成的lexer/parser。于是这是一种绕口令般的没意义的纠结。

不过主要问题还是我并未做过lexer/parser应用,虽然抽空看了点flex/bison的使用,但主要问题还是一开始就要生蛋生鸡的这种抉择实在令人纠结。而且按我一贯的风格,是希望花很短的时间,敲打出一个有效用的东西,然后再考虑继续不继续,相当不习惯慢慢来。

 

如果有人对此有兴趣,欢迎加入。

 

EOF

7 Comments

  1. Posted 2011/04/08 at 20:18 | Permalink

    膜拜aligo大神..=v=

  2. kiyu
    Posted 2011/04/22 at 09:55 | Permalink

    近期想注册me域名,特来咨询下博主,me域名的续费问题,还望博主告知一二,多谢~

  3. Posted 2011/04/24 at 16:02 | Permalink

    @Link 我不是大神XD

    @kiyu 我是在godaddy买的,第一年便宜些,续费就是原价好像是19.99吧,godaddy那垃圾后台,会偷偷让你买一些附加服务,不需要的话一定要注意,基本就是这样了

  4. Posted 2011/04/25 at 10:46 | Permalink

    多谢aligo大大在百忙之中的回复~
    我是通过搜索引擎找到me域名续费可以通过买GD的某个主机套餐便宜点的,便宜以后貌似只用6刀的样子,不知道大大是否试过呢?

  5. Posted 2011/04/26 at 09:12 | Permalink

    @kiyu 这我就不知道了,不过买主机和域名加在一起不是更贵吗?
    我不用godaddy的主机,而且连他的域名都不想用了,后台太麻烦了。。。连转出都很麻烦。。。所以还是继续用吧唉

  6. Posted 2011/07/05 at 10:28 | Permalink

    吃坚果是个复杂的过程~

  7. Posted 2011/10/07 at 11:28 | Permalink

    网页的页面特效真漂亮!给人大气的感觉

Post a Comment

Your email is never published nor shared. Required fields are marked *

*
*

You may use these HTML tags and attributes <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>