学会吃坚果,绝对是人类文明的一大进步,其重要程度,丝毫不亚于登上月球。要吃到坚果,必须先去皮,吃肉或去肉,撬开果壳。这一切的前提,还包括了种下一枚种子,耐心等其长成果树,开花结果,收获果实。
约一个月前,把去年帮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
膜拜aligo大神..=v=
近期想注册me域名,特来咨询下博主,me域名的续费问题,还望博主告知一二,多谢~
@Link 我不是大神XD
@kiyu 我是在godaddy买的,第一年便宜些,续费就是原价好像是19.99吧,godaddy那垃圾后台,会偷偷让你买一些附加服务,不需要的话一定要注意,基本就是这样了
多谢aligo大大在百忙之中的回复~
我是通过搜索引擎找到me域名续费可以通过买GD的某个主机套餐便宜点的,便宜以后貌似只用6刀的样子,不知道大大是否试过呢?
@kiyu 这我就不知道了,不过买主机和域名加在一起不是更贵吗?
我不用godaddy的主机,而且连他的域名都不想用了,后台太麻烦了。。。连转出都很麻烦。。。所以还是继续用吧唉
吃坚果是个复杂的过程~
网页的页面特效真漂亮!给人大气的感觉