引子

自从来到ruby世界,我就被ruby那自由的语法、优雅的对象模型、漂亮的dsl深深地迷住了,了解更多的ruby特性,能够帮你实现更漂亮,流畅的api。在这篇文章中,我将以一个例子来演示如何使用ruby的block和instance_eval实现更具表现力的api。

需求

这个例子是一个来源于真实项目需求,为了演示方便,我对其做了一些简化。程序的输入是一个格式固定的json字符串,输出是从这个json中获取到一些属性值创建出来的一个给定类型的对象。然而,不同于以往的json和对象之间的序列化,反序列化,这里的从json字符串中的值与对象属性之间的对应关系有一定的逻辑。 json中的值和对象值对应关系有如下几种:

  1. json属性和对象属性直接对应。
  2. json属性和对象属性直接对应,当json中没有该属性时,使用给定默认值。
  3. 对象的属性的类型不是普通类型,当json中有对应属性的值时,需要根据json中的值创建一个对应的类型对象。
  4. 等等

我们先来看下最初的实现版本:

注:这里的json不是一个字符串对象,而是经过JSON.parse处理后得到一个嵌套的hash,下同。

class Post
  attr_accessor :author_name, :date, :tags

  def initialize(json)
    init_author_name(json)
    init_date(json)
    init_tags(json)
    #init_xxx...
  end
  
  # omit some code here...
  
  # json中的title和对应上面的第一种情形
  def init_author_name(json)
    @author_name = json['author']['name']
  end

  # 对应上面的第二种情形
  def init_date(json)
    @date = json['date'].blank? ? "1970-01-01" : json['date']
  end

  # 对应上面的第三种情形
  def init_tags(json)
    @tags = []
    unless json['tags'].nil?
      json['tags'].each do |tag|
        @tags.append(Tag.new(tag))
      end
    end
  end
end

Bad smell

看到这样的代码,你发现什么bad smell了吗?重复代码?不像,但是那么多的init_xxx方法看起来就是有那么点不自然。

在我看来,这份代码有两个问题:

第一,从json到Post对象的转换职责,不应该是Post类的职责,这份代码违反了单一职责原则

第二,由于无法很好地将json中的值和对象值对应关系规则建模,导致我们不得不创建多个init_xxx方法,然后在在initialize方法中逐一调用这些方法。然而在这些init_xxx方法之间,存在着结构化重复

如何改进?

首先,要分离职责,把json到Post对象的转换职责放到一个新类PostBuilder中。

其次,要对对应关系进行抽象。

改进

我们在来分析一下json中的值和对象值对应关系规则,还是有规律可循的,对应关系都由三部分组成:json属性对象属性名转换规则(默认没有转换规则)。其中,通过jsonpath来标识json属性,通过block来表示转换规则, 我们可以建立一个MapingRule类来对此关系进行建模。

由此我们得到如下代码:

class Post
  attr_accessor :title, :date, :tags
end

class MappingRule
  attr_accessor, :json_path, :attr_name, :converter

  def apply(obj, json)
    value = JSONPath.new(@json_path).on(json)
    unless value.nil?
      obj.send("#{@field_name}=", @converter.call(value))
    end
  end
end

class PostBuilder
  def initialize
    @rules = []
  end
  
  def rule(json_path, attr_name, converter)
    @rules << MappingRule.new(json_path, attr_name, converter)
  end
  
  def build json
      post = Post.new
      @rules.each do |rule|
      	rule.apply(post, json)
      end
  end
end

# 创建builder
builder = PostBuilder.new
buider.rule("author name", :author_name)
buider.rule("date", :date, -> (date) { date.nil? ? "1970-01-01" : date} )
buider.rule("tags", :tags, -> (tags) { tags.map {|tag| Tag.new(tag)} })

# 使用builder从json创建对象

post = buidler.build({"date" => "2013-09-10", "tags" => ["music", "IT"] })

回顾

与最初版本相比,我们引入了jsonpath和block来对转换规则进行建模(创建了MappingRule类),在PostBuilder#build中循环应用各个rule完成对象的创建,消除了多个init_xxx的重复。至此,代码已经达到一个令人满意的状态。然而,能否让我们的PostBuilder的接口更加漂亮些?

再改进,更具表达力的api

我们再来看下PostBuilder的使用场景:

  1. 创建一个PostBuilder对象。
  2. 给这个对象增加一些转换规则。
  3. 使用这个对象从json创建对象。

因此,可以说,在一个PostBuilder对象被添加规则之前,它是不完整的,是不可用的,即第一二步应该是一个原子操作,我们可以把initialize变为private方法,增加一个config类方法,这个方法可以接受一个block,在此block中对builder增加规则,在这个方法中创建一个builder实例,同时把这个实例传递给block完成buidler的创建。代码如下:

#增加一个config类方法
class PostBuilder
  def self.config
    builder = PostBuilder.new
	yield(builder) if block_given?
	builder
  end
  
  #...
  private
  def initialize
  #...
  end
end

#创建builder
builder = PostBuilder.config do |builder|
    buider.rule("author name", :author_name)
    buider.rule("date", :date, -> (date) { date.nil? ? "1970-01-01" : date} )
    buider.rule("tags", :tags, -> (tags) { tags.map {|tag| Tag.new(tag)} })
end

#使用builder
post = buidler.build({"date" => "2013-09-10", "tags" => ["music", "IT"] })

再改进,更简洁的api

至此,这个PostBuilder提供的api已经非常干净了,然而,这个api还是有改进空间的。在block中builder这个单词出现在每个增加规则的地方。有没有办法把这个重复也给消除掉呢?答案是可以的,instance_eval隆重登场了。对PostBuilder.config方法做如下修改:

  def self.config(&block)
    builder = PostBuilder.new
	builder.instance_eval(block)
	builder
  end

那么,创建builder的代码就简化为:

builder = PostBuilder.config do
    rule("author name", :author_name)
    rule("date", :date, -> (date) { date.nil? ? "1970-01-01" : date} )
    rule("tags", :tags, -> (tags) { tags.map {|tag| Tag.new(tag)} })
end

PostBuilder.config中使用instance_eval对block进行evaluate,相当于在新创建的builder上执行block中的代码,同样能达到对builder增加规则的效果。

使用instance_eval能够使代码变得更加简洁,然而随之而来的风险是,你也给了你的api调用者一个在这个新建对象上执行任意代码的机会。因此,在简洁性和风险之间,你需要做一个权衡。

再抽象

再回头看看PostBuilder,只需些许改动,我们就能从json创建任意类型的对象,于是我们得到一个InstanceBuilder类,如下:

post_builder = InstaneBuilder.config do
    instane_class Post
    rule("author name", :author_name)
    rule("date", :date, -> (date) { date.nil? ? "1970-01-01" : date} )
    rule("tags", :tags, -> (tags) { tags.map {|tag| Tag.new(tag)} })
end

你可以试着实现一个这个InstaneBuilder#instane_class方法。

结语

通观上面的例子,我们通过使用ruby的block和instance_eval,把一个复杂丑陋的代码变得干净,层次清晰,同时,更加容易扩展。 在这里,我抛出自己对编写代码的一点想法,供各位参考:

  1. 在开始编写实现代码前,先考虑一下如何提供一套干净的,更具表达力的api,让api调用者喜欢使用你的api(sinatra做了一个很好的榜样)。
  2. 恰当地使用block,instance_eval 能够很容易的构建一个internal dsl。

Reference

想了解更多关于blockinstance_eval, internal dsl可以参考如下两篇文章:

How do I build DSLs with yield and instance_eval?

Creating a ruby dsl

TLDR:

In this post, I will introduce a really fast deployment tool - Mina, and show you how to deploy a rails application which runs on unicorn with nginx, also I’ll show you how to organize your mina deployment tasks.

Note:

All code in this post can be find here.

About mina

Mina is a readlly fast deployer and server automation tool”, this how the team who built it describes it. The concept behind Mina is to connect to a remote server and executes a set of shell command that you define in a local deployment file(config/deploy.rb). One of its outstanding feature is it is really fast because all bash instructions will be performed in one SSH connection.

Init

To use mina automating deployment, you need to get the following things ready.

  1. a remote sever
  2. create a user for deployment (e.g. deployer) and add this user into sudoer list.
  3. generate a ssh key pair, add the generated public key into GitHub.
  4. create a deployment target fold on the remove server(e.g. ‘/var/www/example.com’)

once you got these things done, run mina init in your project directory, this will generate a deployment file - config/deploy.rb, then set the server address, deployment user, deployment target and other settings in deployment file, like the following:

set :user, 'deployer'
set :domain, ENV['on'] == 'prod' ? '<prod ip>' : '<qa ip>'
set :deploy_to, '/var/www/example.com'
set :repository, 'git@github.com:your_company/sample.git'
set :branch, 'master'

Setup

Then run mina setup, this will create deployment folders, which looks like this:

/var/www/example.com/     # The deploy_to path
|-  releases/              # Holds releases, one subdir per release
|   |- 1/
|   |- 2/
|   |- 3/
|   '- ...
|-  shared/                # Holds files shared between releases
|   |- logs/               # Log files are usually stored here
|   `- ...
'-  current/               # A symlink to the current release in releases/

Provision

It is very common to setup a new server and deploy application on it, it will be good if we can automating this process, here comes the provision task:

    task :provision do
      # add nginx repo
      invoke :'nginx:add_repo'

      queue  "sudo yum install -y git gcc gcc-c++* make openssl-devel mysql-devel curl-devel nginx sendmail-cf ImageMagick"

      #install rbenv
      queue  "source ~/.bash_profile"
      queue  "#{exists('rbenv')} || git clone https://github.com/sstephenson/rbenv.git ~/.rbenv"
      queue  "#{exists('rbenv')} || git clone https://github.com/sstephenson/ruby-build.git ~/.rbenv/plugins/ruby-build"
      queue  "#{exists('rbenv')} || echo 'export PATH=\"$HOME/.rbenv/bin:$PATH\"' >> ~/.bash_profile && source ~/.bash_profile"

      #install ruby
      queue  "#{ruby_exists} || RUBY_CONFIGURE_OPTS=--with-openssl-dir=/usr/bin rbenv install #{ruby_version}"

      #install bundle
      queue  "#{ruby_exists} || rbenv local 2.0.0-p247"
      queue  "#{exists('gem')} || gem install bundle --no-ri --no-rdoc"

      #set up deploy to
      queue "sudo mkdir -p #{deploy_to}"
      queue "sudo chown -R #{user} #{deploy_to}"

    end

    #helper method
    def ruby_exists
    "rbenv versions | grep #{ruby_version} >/dev/null 2>&1"
    end

    def exists cmd
      "command -v #{cmd}>/dev/null 2>&1"
    end

to be able run this taks multi times, I create some helper method detecting whether an executable exists or not. e.g. ruby_exists, exits('gem') these helper method will return if the executable exits, otherwise, it will run next command to install the executable.

With this task, you can get a server ready for deployment in seveural minutes.

Deploy

Once the server is provisioned, you can deploy your application with mina deploy, here is a typical deploy task:

    desc "Deploys the current version to the server."
    task :deploy => :environment do
      deploy do
        invoke :'git:clone'   #clone code from github
        invoke :'deploy:link_shared_paths' #linking shared file with latest file we just cloned
        invoke :'bundle:install' #install bundle
        invoke :'rails:db_migrate' #run database migration
        invoke :'rails:assets_precompile' #compile assets
        invoke :'unicorn_and_nginx' #setup nginx and unicorn config
        to :launch do
          queue '/etc/init.d/unicorn_myapp.sh reload' #reload unicorn after deployment succeed
        end
      end
    end

Unicorn and nginx

to run our application on unicorn and nginx, we need to create our own unicorn and nginx configuration file and start nginx and unicorn with our configuration. here is the task:

    desc "Setup unicorn and nginx config"
    task :unicorn_and_nginx do
      queue! "#{file_exists('/etc/nginx/nginx.conf.save')} || sudo mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.save"

      queue! "#{file_exists('/etc/nginx/nginx.conf')} || sudo ln -nfs #{deploy_to}/current/config/nginx.conf /etc/nginx/nginx.conf"

      queue! "#{file_exists('/etc/init.d/unicorn_avalon.sh')} || sudo ln -nfs #{deploy_to}/current/scripts/unicorn.sh /etc/init.d/unicorn_myapp.sh"
    end

Conclusion

With these tasks: mina init, mina provision, mina deploy, mina helps you deploying easily with less mistake, have fun with mina!

Recently I’m working a RoR stack RESTful API project, I was involved in proposing tech stack, architecture, deployment pipeline. There was many thoughts in this progress, so I wrote down our thoughts here, in case it may help you when you met a similar question.

Tech Stack

There’re bunch of api framework out there in ruby stack, such as Grape, rails-api, Sinatra. I’ll share my understanding here:

Sinatra

Sinatra providing a lightweight, simple DSL to creating web application. we can create a web/api application with sinatra within 15 seconds! the downside is, it is not a full stack framework, it requires us to combine sinatra with other frameworks. for example, we have a backend database for storing and retrieving information, you need to interating sinatra with a orm framework(e.g. ActivateRecrod or DataMapper).if we want to render info on a web page, we need to integrate a view template engine.

Grape

Grape is a ruby framework, which is designed for creating RESTful api service. it have sevural great features for creating RESTFul api. for example, api verisoning, automatic api doc generation, etc. Similar to sinatra, it is not a full stack framework, it requires some integration work. BTW, Grape can be easily integrated into any Rack application(e.g. sinatra, rails).

Rails::API

Rails::API is a subset of a normal Rails application, created for applications that don’t require all functionality that a complete Rails application provides. It is a bit more lightweight, and consequently a bit faster than a normal Rails application.

In the end, we choose Rails::API as our tech stack for the following reason:

  • it is a fullstack framework, including ORM, db migration, validation, etc all in one place.
  • we can leveage some rails’s feature, e.g. generator, db migration.
  • it is a subset of rails, is designed for creating a API application.
  • rails’s REST conversion.

API Design

Content Type Negotiation

A most important part of designing RESTful API is content-type negotiation. the content type negotiation can be both in request/response header and url suffix:

in request header, Content-Type indicating content type of data in request body, Accept telling server what kind of content type the client expected to accept.

in response header, Content-Type indicating content type of data in the response body.

Also, request to /users/2.json expecting the server return user info in JSON format, but request to /users/2.xml expecting get a XML response.

there’re several kind of standard content type, e.g. application/json, application/xml. People can define their own content type, e.g. application/vnd.google-apps.script+json
My feeling is, if your api is a public endpoint, you’d better define your own content type.

let’s take a example, a authentication api expecting get the following request data:

{
  email: "sample@gmail.com",
 password: "my_password"
}

you have two content type choices: application/json and application/vnd.mycompany.credential+json, if this is a public api, I’ll chose the customized content type - application/vnd.mycompany.credential+json, or this is an internal api, I’ll chose the standard content type - application/json. I made this choice by considering the following reasons:

  Pros Cons
Customized content type Could define a json schema, api server and client could use this json schema ensure request is processable. adding complexity
Standard content type Simple and straightforward no validation to request data, any unexpected message could be send to server

Code conversion

I struggled with workflow management when I play the very first story in this project, the problem is, it is very common and business workflow have more then two exit points. e.g. a login workflow, the possiable exit points are:

  1. username password matched, login succeed
  2. username password mimatched, login failed
  3. user is inactived, login failed
  4. user is locked because of too many login failures, login failed.

in a rails project, it is very important to keep your controllers clean, controller’s only responsibility is passing parameters and routating, so it is better is to put these business logic into Model or Service. here comes the problem: how can we let the controller konw the extact reason of failure without put the business logic in controller? return value can not fufill this requirement, so here come out our solution: modeling these exception exit point with ruby exception, handing different exceptions in controller with different exception handler. and we found it makes the controller and model much more cleaner. let’s have a look at this example:

Before, the controller was messy:

      #in controller
      def create
        user = User.authorize(params[:email], params[:password])
        if user.nil?
          render :status => 401, :json => {:error => "Unauthorized"}
        elsif !user.activated?
          render :status => 403, :json => {:error => "user hasn't been activated"}
        else
          response.headers["Token"]= Session.generate_token(user)
          render :status => 201, :nothing => true
        end
      end

After, controller is much cleaner

      #in controller
      rescue_from InactiveUserException, with: :user_is_inactive
      rescue_from InvalidCredentialException, with: :invaild_credential
      rescue_from UserNotFoundException, with: :user_not_found

      def create
        login_session = User.login(params[:email], params[:password])
        response.headers["Token"]= login_session.token
        render :status => :created, :nothing => true
      end

Error code

It is very common to return failure reason when API call failed. Even we can return failure reason in plian english, as an API provider, we shouldn’t assume that API client will use error message we provided, it’s better to return a structured failure reason which can be parsed by API client. let’s take a look at example from Github API:

    {
      "message": "Validation Failed",
        "errors": [
        {
          "resource": "Issue",
          "field": "title",
          "code": "missing_field"
        }
      ]
    }

failure reason was returned as an JSON object, resource representing what kind of resource is requested, field indicates which field fails api call, code indicating the exact faliure reason.

Another thing I want to highlight is - do not define numeric error code, it will be a nightmare for you and your client. a better solution is define meanningful error code, like missing_field, too_long, etc.

Documentation

RESTful api don’t have frontend, so it is very important to make your document friendly. Aslo, it is very common that a developer changing code but forgot to change api doc, so it would be great if we can generating api document automaticly. considering we have a well formed test suite(or spec) for the api, why cann’t we just extract information from these tests/specs and generating document automaticly. Acturally there’re some gems trying to solve this problem: apipie-rails, swagger-ui. we’re using apipie-rails, but we’ve found some missing features in apipie-rails. e.g. it can not record extract request and response headers, while headers play a important rule in a RESTful api.

Testing

We have two kind of tests in this project: integration test and unit test.

integration tests test the api from end point, it is a end to end test. we use requesting rspec define this test.

unit tests test different layer. hints: only stub method on the boundary of each layer.

integration test

Make tests less flakey by using contract-based testing instead of hitting live endpoints

Versioning

we could specify api version in either url or http request header. In theory, verion number in url is not restful, any thoughts, please let me know

Deployment

We deploy our api on amazon EC2.

  • Provision: creating new node from a customized AMI (with many required dependence installed).

  • Build pipeline:

    the CI will build a rpm package one all test passed, then this package will be pushed to S3, after that, this package will be installed on the provisioned node.

本文译自这里。

注:翻译尚未完成,标记为WIP的章节还未开始。

####Delimited Continuations

Scala 2.8中引入的Delimited Continuation,可以被用来实现一些有趣的流程控制结构。

这是一篇很长很长的博客,我花了很长时间才理解了scala的resetshift操作符,为了不让别人再掉进我遇到的坑里,我会从最基本概念开始介绍,如果你需要一个更短的介绍,请到文章的最后的“资源”部分查看其他的博客。

####目录

  • 技术实现
  • Continuation Passing Style
  • 嵌套CPS
  • 全部CPS和有界限的Continuations
  • 如何使用
  • CPS with Return
  • Reset和Shift
  • 注解(WIP)
  • 嵌套shift(WIP)
  • 控制结构的限制(WIP)
  • 建议(WIP)
  • 资源(WIP)

####技术实现 为了使用scala的delimited continuations,你的scala版本必须高于2.8版本(译者注:目前的scala的最新版本是2.10.2),并且你还需要使用scala的continuation编译器插件。你可以通过在命令行制定一个参数来在编译时(compiling)和运行时(runtime)启用此插件:

scalac -P:continuations:enable ${sourcefiles}
scala -P:continuations:enable ${classname} 在代码中,你还需要引入scala中对continuation的支持。

import scala.util.continuations._

如何你忘记了引入上述包,你可能会得到类似下面的错误信息:

<console>:6 error:not found value reset
		reset{		
		^ ####Continuation Passing Style 为了理解scala的delimited continuations,首先你需要理解“continuation passing style”这个术语。

来看一下下面这段包含了方法调用的代码:

def main{
	pre
	sub()
	post
}

def sub(){
	substuff
} `pre`和`post`表示在main方法中在调用sub方法之前、之后的代码,`substuff`表示sub方法中的所有代码。

sub方法被调用时,系统会调度处理器执行sub中的代码,等sub中的代码执行完毕后,再继续执行main方法中其余代码。

我们可以把上面的代码进行些许重构,所有pre部分的代码可以被抽取到一个方法中,所有post部分的代码可以被抽取到另外一个方法中。甚至我们可以把每部分所需要的输入抽取为方法的参数,把每部分的的输出返回出来。经过以上修改,我们得到如下的代码:

def main(m: M): Z = {
	val x: X = pre(m)
	val y: Y = sub(m, x)
	val z: Z = post(m, x, y)
	return z
}

def sub(m: M, x: X): Y {
	val y: Y = substuff(m, x)
	return y
}

现在,让我们做点改进,我们不让系统在执行sub完毕后执行自动执行post,而是显式地把要执行的代码作为sub额外的一个参数传递给sub。然后我们修改sub,在完成原有的计算逻辑,计算出要返回给main的值y以后,调用这个额外的参数,计算出它的返回值z,然后把这个z返回给main

def main(m: M) {
	val x: X = pre(m)
	val z: Z = sub(m, x, {post(m, x, _)})
	return z
}

def sub(m: M, x: X, subCont: Y => Z) {
	val y: Y = substuff(m, x)
	val z: Z = subCont(y)
	return z
} 当把包含`post`代码段传递给`sub`时,Scala生成一个记录了包含`post`执行上下文(当时的m,x值)的闭包,当这个闭包将来被执行时,可以获得的当时的执行上下文。

我们注意到,main方法中已无法看到sub方法原来的返回值y了,因此我们没法把y传递给post,我们用了一个占位符_来表示,这个值会在sub内部传递给post。我们可以把这个方法调用重写成一个更加明确的形式:

val z: Z = sub(m, x, {y: Y => post(m, x, y)}) CPS的要点是“不要用`return`”。不像`direct style`那样调用一个子函数,待子函数执行完毕后返回给主函数,我们传递一个子函数执行结束后要被执行的continuation给子函数。

####嵌套CPS 在上面的例子里,我们已经迈出了变换成CPS的第一步。为了能够使用CPS的一些更高级的特性,我们需要继续完成上面的变换。

在方法调用的最顶层,main方法仍然有返回值,但是,在CPS中没有return,我们如何处理这种情况?答案是,最高层的方法不能有返回值,我们来再来增加一个顶级的wrapper:

def prog(m: M) {
	val z: Z = main(m)
	println(z)
	System.exit(z.exitValue)		
} 现在,我们可以对`prog`和`main`做同样的CPS变换:

def prog(m: M) {
	main(m, {z: Z => {
		println(z)
		System.exit(z.exitValue)
	}})
}

def main(m: M, mainCont: Z => Unit): Unit = {
	val x: X = pre(m)
	val z: Z = sub(m, x, post(m, x, _))
	mainCont(z)
} 我们仍然在`sub`中使用了`return`,并且在`main`中使用`sub`的返回值。为了解决这个问题,我们需要把`mainCont`做为一个`continuation`传递给`sub`,修改后的`main`和`sub`如下:

def main(m: M, mainCont: Z => Unit): Unit = {
	val x: X = pre(m)
	sub(m, x, y: Y => {
		val z: Z = post(m, x, y)
		mainCont(z)
	})
}

def sub(m:M,x:X, subCont: (Y) => Unit) {
	val y:Y = substuff(m,x)
	subCont(y)
}

我们已经有了个自顶向下的continuation(包含了System.exit),因此,当我们执行在sub中执行subCont时,它会先执行post,再执行main后面的代码,即printlnSystem.exit

如果我们想把stubstuff也转换成CPS,我们需要利用同样的转换机制转换subsubstuff。把subsubstuff之后所有的代码做为一个额外的参数传递给substuff,其中包含了从main传递到sub的continuation,也包含了从prog传递到main的continuation。

我们可以看到,每个continuation都会包含它之前所有的caller的continuation,换句话说,每个continuation都包含了这个方法调用结束后需要被执行的所有代码。另外一个很重要的一点是,在每个调用CPS的子方法的地方,这CPS方法调用总是这个方法的最后一行。

####Full versus Delimited Continuations 在上面的讨论中,我们假设整个程序都被转换成CPS,这就是传统的CPS,我们也称之为Full Continutaitons。然而, 在原生不支持CPS的编程语言(例如scala)中使用CPS可能会使代码变得比较混乱, 因此如果能够把CPS的使用限定在特定的范围内是最好不过了。

这就是delimited continuation被发明出来的原因。仅仅留存部分后续要执行的程序,而不是尝试保存整个后续执行的程序。

我们再来看下上面的例子程序,那个prog方法与其他方法唯一的不同就是我们不能返回一个Direct Style value。如果我们移除掉那个System.exit ,我们可以在一个普通的Direct Style代码中调用prog,同时在prog和其子方法中使用CPS,每个方法以传递一个continuation给下一个方法结尾,直到最后一个continuaion被执行,CPS代码结束,控制返回到调用者prog中。

def prog(m: M) {
	main(m, {z: Z => 
		println(z)
	})
}

####Use

我们已经通过很大努力把我们的代码重写成CPS并且保持功能不变。现在我们再来看看如何修改代码只能使用CPS。

CPS的核心能力是:它能够提供一个显式的对象(continuation)来表示后续要执行的代码(或者,在Delimited continuation的情况下,后续要执行的部分代码)。在上面的例子中,我们在sub方的最后执行那个continuation。但是,如果我们不执行这个continuation,而是把它保存起来,例如一个singleton,会发生什么有意思的事情呢?

object ContinuationSaver{
	var savedContinuation: Option[() => Unit] = None
	def save(saveCont: => Unit) = {savedContinuation = Some(saveCont _)}
}

def sub(m: M, x: X, subCont: Y => Unit) {
	val y: Y = substuff(m, x)
	ContinuationSaver.save {subCont(y)}
} `sub`保存了continuation后,`sub`就执行完毕,实际上整个delimited continuation已经执行完毕;控制已经返回到调用者`prog`中。但是在`ContinuationSaver`中我们仍然保存着那个记录着后续代码的continuation。实际上,我们已经把这些后续代码放到了挂起状态,并且在以后的任意时间重新执行。

我们不仅可以在将来执行这段代码,并且可以多次执行。我们甚至可以实现一个更加复杂的ContinuationSaver,能够保存多个continuation,并且记录哪个continuation应该被被执行,以什么样的顺序,执行多少次等。我们甚至可以把这些continuation持久化起来,或者发送到其他计算机上,如Swarm.

####CPS with Return

在纯粹的CPS中是没有return的,但是Scala中有return,甚至在使用CPS的时候。在上一章节中我用了“控制回到了调用者prog”这样的术语。这跟普通的方法调用一样 —— 所有的中间方法都返回到自己的调用者,直到方法调用栈pop到第一个CPS方法调用。我假设所有的CPS方法都不返回值(即返回Unit),但是没有任何规则规定我们不能从CPS方法中返回值。

If we add a return value to the transformed code, this is not something we can get as a result of using the above transformation technique.

上面的例子展示了从一个Direct Style代码到CPS代码的转换过程,上述的转换方式总会生产一个返回值为Unit的代码。如果我们给上面转换后的代码加入返回值,通过上述的转换方式是做不到的。

如果给我们的CPS代码加入返回值,会发生什么样的事情呢?在上面的例子里, 子方法的最后一步总是在执行continuaiton,如果我们把这做为默认行为,当我们给CPS代码增加返回值时,这个返回值会沿着CPS方法调用链一直返回到最顶端的CPS方法,在Direct Style代码中表现为最顶端的CPS方法的返回值。当然,任何一个中间的CPS方法都有可能修改或者替换这个返回值。

举个例子,我们让最新版本的sub方法(把continuation保存在ContinuationSaver中的那个)返回一个Int值。

object ContinuationSaver {
	var numberOfSavedContinuations = 0
	var savedContinuation: Option[() => Unit] = None
	def save(saveCont: => Unit): Int = {
		savedContinuation = Some(saveCont _)
		numberOfSavedContinuations = numberOfSavedContinuations + 1
		
		numberOfSavedContinuations
	}
}

def sub(m: M, x: X, subCont: Y => Unit):Int = {
	val y: Y = substuff(m, x)
	ContinuationSaver.save {subCont(y)}
}

并且我们也修改了调用链的其他方法以便把这个值返回出去。由于对sub的调用是main方法的最后一条语句,我们只需要修改main的返回值类型使之和sub的返回值类型一致即可。同样地,对main的调用是prog方法的最后一条语句,只需习惯prog的返回值类型使之和main的返回值类型一致即可。

def prog(m: M):Int = {
	main(m, { (z: Z) => 
		println(z)
	})
}

def main(m: M, mainCont: Z => Unit):Int => {
	val x: X = pre(m)
	sub(m, x, 
	{ (y: Y) => 
		{
			val z: Z = post(m, x, y)
			mainCont(z)
		}
	})
} 如果我们想的话,我们可以在`main`中修改从`sub`中返回的值做为`main`自己的返回值,甚至我们可以从`main`中返回一个与`sub`的返回值没有任何关系的值。

我们来回顾一下上面转换后的CPS代码。我们可以发现,未转换的代码拥有原始的返回值类型,转换后的CPS代码有转换后(可能完全不同)的返回值。

####Reset and Shift 终于,我们有了足够的背景知识来理解Scala的 resetshift关键字。

Scala中的delimited continuation是由EPFL的Tiark Rompf创建的。在他和Ingo Maier以及Martin Odersky合作的论文Delimited Continuations in Scala有详细描述。下面的资源部分也有Tiark的关于Delimited Continuations的博客。

reset被用于标识delimited contiuation上界,只有reset内部代码才是CPS代码,reset的返回值不是CPS代码。

shift被用于标识delimited continuation的下界,shift内部的代码不是CPS代码,(but it’s untransformed return value is CPS)但是它的未转换的返回值是CPS。当shift执行时,会被传入一个传入一个从它的调用者开始到一个闭合的reset的continuation做为参数。

所以,resetshift是从Direct Style到CPS,从CPS带Direct Style的转换器。所有在resetshift之间的代码都是CPS。所有包含了shift的方法需要被标识为CPS,所有调用CPS方法的方法也需要被标识为CPS,直到遇到一个闭合的reset

当你使用resetshift时,continuation编译器插件就是对你的代码做类似我们上面的例子里的CPS转换操作。从shift块结束处开始到方法结束或者reset块结束的代码会被打包成一个closure,做为一个continuation传递给shift块。

我们来仔细研究下Scala中resetshift的例子。

reset {
	shift{ k: (Int => Int) =>  k(7) } + 1
}

shift语句告诉编译器插件重组代码,通过把从shift调用之后的代码转换成一个continuation,并且把这个continuation做为参数传递给shift语句。为了让这个例子更好懂一点,我们来做一下这个转换。

首先,我们把shift调用的结果赋值给一个变量,并且使用这个变量。

reset {
	var r = shift{k: (Int => Int) => k(7)}
	r + 1
}

接下来,我们把shift块之后的代码转换成一个函数,并且调用之。

reset {
	var r = shift{k: (Int => Int) => k(7)}
	
	def f(x: Int) => x + 1
	f(r)
} 这个函数`f`就是我们的continuation,它包含了从`shift`块结束到`reset`块结束的所有代码。

最后我们就像编译器插件那样,把我们的continuation函数f绑定到shift的参数k上。把转换后的代码的返回值做为shfit块的返回值。

reset{
	def f(x: Int) = x + 1
	f(7)
}

现在我们可以直接看到,返回值是8。

我们也可以把同样的转换方式应用到

reset {
	shift { k: (Int=>Int) => k(k(k(7)))} + 1
} 会得到

reset {
  def f(x:Int) = x + 1
  f(f(f(7)))
} 我们可以很容易地看出结果是10。

我们所有的转换都对reset外部的代码没有影响,例如

reset {
  shift { k: (Int=>Int) => k(7) } + 1
} * 2

给reset表达式的返回值乘以2,结果会是16。

Tiark的论文中展示了一个有趣的例子:

reset {
  shift { k: (Int=>Int) => k(k(k(7))); "done" } + 1
} 并指出这个代码的返回值是“done”。continuation函数被调用了三次,但是其返回值被抛弃了。对这段代码应用我们的代码转换过程,我们会得到下面的结果:

reset {
  def f(x:Int) = x + 1
  f(f(f(7))); "done"
} 从这个代码中很容易看出来为什么返回是“done”。

值得注意的一点是,reset块的运算结果不像大多数代码那样,是这个块的最后一行。实际上,reset块的运算结果是它内部的shift块的最后一行。执行shift块永远是reset块内部最后做的一件事。

当你看到一个shift块,并且它的返回值被应用在一个表达式中时,比如上面的那个“shift + 1”的例子,请记住一点,由于代码的转换,这个从shift块中“return”从来没有真正地return过。实际上,当执行到shift块时,shfit块之后的代码被转换成一个continuation并做为参数传递给这个shift块;如果shift块中的代码执行了这个continuation,这个continuation的执行结果就表现为shfit块的返回值。the value which is passed as an argument to the continuation appears as the value being returned from the shift block.因此,传递给shift块中continuation 函数的参数的类型和代码中shfit块的返回值类型是一致的。 continuation 函数的返回值类型和这个shift块外围的reset块的返回值类型是一致的Thus the type of the argument passed to the shift block’s continuation function is the same as the type of the return value of the shift in the source code, and the type of the return value of that continuation function is the same as the type of the return value of the original last value in the reset block that encloses the shift block.

这里有三种类型与shift相关连:

  • 传递给continuation函数的参数类型,和shfit块的语义返回值类型一致。
  • The type of the argument to pass to the continuation, which is the same as the syntactic return type of the shift in the source code.

  • continuation函数的返回值类型,与从shfit块结束开始的代码的返回值类型一致。(例如,从shift块结束到函数结束或者reset块结束之间的代码的返回值类型)这被叫做untransformaed return type

  • The type of the return from the continuation, which is the same as the return type of all of the code that follows the shift block in the source code (i.e. the type of the last value in the block of code between the shift block and the end of the function or reset block containing the shift block). This is called the untransformed return type.

  • shift块中最后一条语句的值,被做为整个函数或者reset块的返回值的类型。这个被称为transformed return type
  • The type of the last value in the shift block, which becomes the type of the return value of the enclosing function or return block. This is called the transformed return type.

shift的方法签名中,上面的三个类型标记为A,B,C

def shift[A, B, C](func: ((A => B) => C)): A @scala.util.continuations.cpsParam[B, C]

cpsParam注解中两个类型为未转换的返回值和已转换的返回值。关于注解cpsParam下面有更详细的描述。

reset的方法签名包含了两个类型:第一个类型是传给reset的代码块的未转换类型,和shiftB类型匹配;第二个类型是该代码块的已转换类型,和shfitC类型匹配。同时也是reset块的真正返回类型。reset的scaladoc中用AC来表示这两种类型,但是这里我使用BC,因此这个ctx参数类型和shift的返回类型一致。

def reset[B, C](ctx: => B @scala.util.continuations.cpsParam[B, C]): C

下面展示了这些类型都是在哪儿出现的:

C = reset{…; A = shift{k: A => B => …; C}…;B}

在下面这个例子里,A=Int, B =String, C=Boolean:

def is123(n: Int): Boolean = {
	reset{
		shift{k: (Int => String) =>
		 (k(n) == "123")
		}.toString
	}
}

####注解(WIP) ####嵌套shift(WIP) ####控制结构的限制(WIP) ####建议(WIP) ####资源(WIP)

###声明 目前spring只支持MRI 1.9.3, MRI 2.0.0, Rails 3.2,没有达到要求的人赶紧升级你们的ruby,rails版本吧

###问题 想必采用TDD/BDD方式进行开发的rails开发者都有着这样类似的经历:

  1. pair写了一个测试
  2. 运行测试
  3. 等待
  4. 该我来编写产品代码
  5. 运行测试
  6. 等待
  7. 代码有bug
  8. 测试失败
  9. 修复测试
  10. 运行测试
  11. 等待
  12. 测试通过,yeah!

再回过头来想想,我享受这段pair的过程吗?

  • pair很给力,很快就把一个taks实现成一个测试用例
  • 桌子上的水果也很好吃。
  • 。。。

可是,我总觉得有点不爽快,原来是那么多的等待,每运行一次测试,就需要等待十几秒甚至几十秒,每天我会运行上千次测试,这是多大的浪费?做为一个有追求的程序员,我当然不愿意把宝贵的工作时间浪费在这无谓的等待中去 :-)。

###现有方案 有追求的程序员还是大多数,google之后才发现已经有人尝试解决这个问题,如sporkzeus。他们的原理都是预先把rails环境启动起来,后面在运行测试,执行rake task时从这个启动好的进程fork一个进程,在这个进程中执行操作。然而,spork需要修改spec_helper.rb,并且需要单独启动一个server进程,zeus虽然不需要修改项目代码但仍然需要单独启动一个server进程,用起来还不是很爽快。 spring带来了更加易用的方案。

###安装 建议把spring安装到rvm的global gemset中去,这样就可以在多个project使用spring

安装命令非常简单:

gem install spring

###使用

执行测试的命令也非常简单:

spring rspec

当第一次使用spring运行测试,rake taks, db migration时,spring会自动在后台load rails 环境,因此执行速度也很慢,但是当再次执行时,spring会从先前的进程中fork出load好的rails环境,执行速度就变得飞快!

###已知问题 把 require 'rspec/autorun'从spec_helper中删掉,否则,spec会被执行两次,而且第二次会由于找不到url helper method而失败。

Failure/Error: visit posts_path
 NameError:
   undefined local variable or method `posts_path' for #<RSpec::Core::ExampleGroup::Nested_2:0x007fcf650718e0>
 # ./spec/integration/posts/posts_spec.rb:10:in `block (2 levels) in <top (required)>'
 # -e:1:in `<main>'

感谢Stefan Wienert的分享。

###总结 spring把对项目代码的影响减少到了没有,并且能够去掉加载rails环境的时间,极大地提升rails开发者的效率,是现有rails开发者必不可少的利器。enjoy coding!!!