Rails 4 源码阅读之timestamps

Published on:
Tags: Rails, Rails4

最近开始看 Rails 4 的源码,打算写一系列的 Rails 4 源码阅读的文章,这是第一篇.

我是因为想知道 Rails 4 里面的 timestamps 是在什么时候赋值或者更新的,然后我翻看了下 Rails 4 的 ActiveRecord 代码.

https://github.com/rails/rails/blob/master/activerecord/lib/active_record/timestamp.rb里面有create_recordupdate_record方法,更新这些timestamps的操作就在这个里面了.

恩,应该是这样.update_recordcreate_record分别是创建和更新必掉的方法,所以如果有 timestamps 并且开启了record_timestamps就会去赋值或者更新这些字段了.

其实也可以从save开始阅读,save的过程可以简化成这样.

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
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record//connection_adapters/abstract/transaction.rb#L270
def save(*) #:nodoc:
  rollback_active_record_state! do
    with_transaction_returning_status { super }
  end
end
.
.
.
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/attribute_methods/dirty.rb#L31
def save(*)
  if status = super
    @previously_changed = changes
    @changed_attributes.clear
  end
  status
end
.
.
.
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/validations.rb#L56
def save(options={})
  perform_validations(options) ? super : false
end
.
.
.
# https://github.com/rails/rails/tree/master/activerecord/lib/activerecord/persistence.rb#L105
def save(*)
  create_or_update
rescue ActiveRecord::RecordInvalid
  false
end
.
.
.
# https://github.com/rails/rails/tree/master/activerecord/lib/activerecord/callbacks.rb#L298
def create_or_update #:nodoc:
  run_callbacks(:save) { super }
end
.
.
.
# https://github.com/rails/rails/tree/master/activerecord/lib/activerecord/persistence.rb#L464
def create_or_update
  raise ReadOnlyRecord if readonly?
  result = new_record? ? create_record : update_record
  result != false
end
.
.
.
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/timestamp.rb#L46
def create_record
  if self.record_timestamps
    current_time = current_time_from_proper_timezone

    all_timestamp_attributes.each do |column|
      if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil?
        write_attribute(column.to_s, current_time)
      end
    end
  end

  super
end
.
.
.
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/callbacks.rb#L302
def create_record #:nodoc:
  run_callbacks(:create) { super }
end
.
.
.
# https://github.com/rails/rails/blob/master/activerecord/lib/active_record/persistence.rb#L483
def create_record(attribute_names = @attributes.keys)
  attributes_values = arel_attributes_with_values_for_create(attribute_names)

  new_id = self.class.unscoped.insert attributes_values
  self.id ||= new_id if self.class.primary_key

  @new_record = false
  id
end

哇,就是这样的顺序,但是感觉有点复杂啊,save到处都是,通过super关键字逐层调用,就像一个rack stack一样!最主要的是我们要知道save的调用链是怎么样的,这个可以看ActiveRecord::Base.ancestors.

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
puts ActiveRecord::Base.ancestors.join("\n")
ActiveRecord::Base
ActiveRecord::Core
ActiveRecord::Store
ActiveRecord::Serialization
ActiveModel::Serializers::Xml
ActiveModel::Serializers::JSON
ActiveModel::Serialization
ActiveRecord::Reflection
ActiveRecord::Transactions
ActiveRecord::Aggregations
ActiveRecord::NestedAttributes
ActiveRecord::AutosaveAssociation
ActiveModel::SecurePassword
ActiveRecord::Associations
ActiveRecord::Timestamp
ActiveModel::Validations::Callbacks
ActiveRecord::Callbacks
ActiveRecord::AttributeMethods::Serialization
ActiveRecord::AttributeMethods::Dirty
ActiveModel::Dirty
ActiveRecord::AttributeMethods::TimeZoneConversion
ActiveRecord::AttributeMethods::PrimaryKey
ActiveRecord::AttributeMethods::Query
ActiveRecord::AttributeMethods::BeforeTypeCast
ActiveRecord::AttributeMethods::Write
ActiveRecord::AttributeMethods::Read
ActiveRecord::AttributeMethods
ActiveModel::AttributeMethods
ActiveRecord::Locking::Pessimistic
ActiveRecord::Locking::Optimistic
ActiveRecord::CounterCache
ActiveRecord::Validations
ActiveModel::Validations::HelperMethods
ActiveSupport::Callbacks
ActiveModel::Validations
ActiveRecord::Integration
ActiveModel::Conversion
ActiveRecord::AttributeAssignment
ActiveModel::ForbiddenAttributesProtection
ActiveModel::DeprecatedMassAssignmentSecurity
ActiveRecord::Sanitization
ActiveRecord::Scoping::Named
ActiveRecord::Scoping::Default
ActiveRecord::Scoping
ActiveRecord::Inheritance
ActiveRecord::ModelSchema
ActiveRecord::ReadonlyAttributes
ActiveRecord::Persistence
Object
JSON::Ext::Generator::GeneratorMethods::Object
ActiveSupport::Dependencies::Loadable
PP::ObjectMixin
Kernel
BasicObject

这样就能知道调用链的情况了.当然还可以去debugger.

恩,save就是这样!timestamps又明白了,太棒了~!

Rails 技巧之 Safe Name

Published on:
Tags: rails, ruby

当使用元编程动态生成代码时,可能会有一些不符合命名规范的名称,那么就可以用到下面的技巧,姑且称之为safe name.

以下出自 Rails 源码 define_method_attribute

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
# We want to generate the methods via module_eval rather than
# define_method, because define_method is slower on dispatch and
# uses more memory (because it creates a closure).
#
# But sometimes the database might return columns with
# characters that are not allowed in normal method names (like
# 'my_column(omg)'. So to work around this we first define with
# the __temp__ identifier, and then use alias method to rename
# it to what we want.
#
# We are also defining a constant to hold the frozen string of
# the attribute name. Using a constant means that we do not have
# to allocate an object on each call to the attribute method.
# Making it frozen means that it doesn't get duped when used to
# key the @attributes_cache in read_attribute.
def define_method_attribute(name)
  safe_name = name.unpack('h*').first
  generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
  def __temp__#{safe_name}
  read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
  end
  alias_method #{name.inspect}, :__temp__#{safe_name}
  undef_method :__temp__#{safe_name}
  STR
end

safe name技巧的原理就是构造符合命名规范的名称(通过一些算法,像unpack,hash都可以), 像这里的话是通过unpack('h*')生成一个字串safe_name.

这个技巧真心很赞!!!

Resque 同步执行

Published on:
Tags: Resque

背景

测试的时候,有个方法里面调用了Resque.enqueue,我觉得不好测试. 可能有人会说只需要测试是否调用了Resque.enqueue,或者只需要测试redis里面是否有这个队列. 但是我觉得最好还是要测试到后台任务的执行结果,因为后台任务的最终结果可能就是这个方法的关键之处,是这个方法的主体.所以最好还是能去测试后台任务的结果(当然,视代价而定)!~

好了,回到正题.怎么去测试Resque任务的执行结果呢?额,开着后台worker?然后在测试代码里面sleep 10等待后台执行?我擦,太二了!

翻看了下Resque的源码,发现了个好东西inline.

# If 'inline' is true Resque will call #perform method inline
# without queuing it into Redis and without any Resque callbacks.
# The 'inline' is false Resque jobs will be put in queue regularly.

大意就是

如果 inline 为 true, Resque 任务会直接执行不需要放到 redis 队列中,并且不执行任何 callbacks。 inline 是 false, Resque
任务将放在 redis 队列里面等待执行。

恩,nice!这个就是我想要的,同步执行!

源码分析 inline

首先要看压队列的方法 Resque#enqueue.

1
2
3
def enqueue(klass, *args)
  enqueue_to(queue_from_class(klass), klass, *args)
end

这个方法是将job压入redis队列.再看看 queue_from_class方法.

1
2
3
4
5
6
7
def queue_from_class(klass)
  if klass.instance_variable_defined?(:@queue)
    klass.instance_variable_get(:@queue)
  else
    (klass.respond_to?(:queue) and klass.queue)
  end
end

这个方法是用来获取job对应的队列名的.

再回到enqueue方法,看看 enqueue_to 方法.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def enqueue_to(queue, klass, *args)
  validate(klass, queue)
  # Perform before_enqueue hooks. Don't perform enqueue if any hook returns false
  before_hooks = Plugin.before_enqueue_hooks(klass).collect do |hook|
    klass.send(hook, *args)
  end
  return nil if before_hooks.any? { |result| result == false }

  Job.create(queue, klass, *args)

  Plugin.after_enqueue_hooks(klass).each do |hook|
    klass.send(hook, *args)
  end

  return true
end

这个方法是去验证job,并且执行hooks,创建job. 先看看 validate.

1
2
3
4
5
6
7
8
9
10
11
def validate(klass, queue = nil)
  queue ||= queue_from_class(klass)

  unless queue
    raise NoQueueError.new("Jobs must be placed onto a queue.")
  end

  if klass.to_s.empty?
    raise NoClassError.new("Jobs must be given a class.")
  end
end

这个方法是去校验是否有队列名以及job class.

再看看 Plugin.before_enqueue_hooks.

1
2
3
4
5
# Given an object, returns a list `before_enqueue` hook names.
def before_enqueue_hooks(job)
  get_hook_names(job, 'before_enqueue')
end
return nil if before_hooks.any? { |result| result == false }

这个方法是获取before_enqueue hook的列表.然后

1
2
3
before_hooks = Plugin.before_enqueue_hooks(klass).collect do |hook|
  klass.send(hook, *args)
end

这段代码会去执行所有的before_enqueue hook,并且校验所有before_enqueue hook执行结果,如果有为false,则直接return,否则创建job,并且执行所有的after_enqueue hook.

我们再仔细看看 Job.create.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Creates a job by placing it on a queue. Expects a string queue
# name, a string class name, and an optional array of arguments to
# pass to the class' `perform` method.
#
# Raises an exception if no queue or class is given.
def self.create(queue, klass, *args)
  coder = Resque.coderhttps://github.com/resque/resque/blob/master/lib/resque/json_coder.rb
  Resque.validate(klass, queue)

  if Resque.inline?
    # Instantiating a Resque::Job and calling perform on it so callbacks run
    # decode(encode(args)) to ensure that args are normalized in the same
    # manner as a non-inline job
    payload = {'class' => klass, 'args' => coder.decode(coder.encode(args))}

    new(:inline, payload).perform
  else
    Resque.push(queue, 'class' => klass.to_s, 'args' => args)
  end
end

关键的地方来了!!!
额,先稍等,一步一步来.

coder
1
2
3
4
5
6
#https://github.com/resque/resque/blob/master/lib/resque.rb#L84
# Encapsulation of encode/decode. Overwrite this to use it across Resque.
# This defaults to JSON for backwards compatibility.
def coder
  @coder ||= JsonCoder.new
end

不难看出,是用来解析队列参数args的,这里默认是 JsonCoder, 用JSON来解析参数.

Resque.validate

前文已经讲过,用来校验队列名以及job class.

Resque.push

如果非inline,将job压入队列,等待异步执行.

perform

如果inline,直接执行job.可以具体看看是怎么执行的.
先获取job参数,再用参数new一个job出来,然后执行perform方法.关键就在perform方法,这个方法就是去真正执行job了.
下面再来具体看看到底是怎么perform的.

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
# https://github.com/resque/resque/blob/master/lib/resque/job.rb#L134
# Attempts to perform the work represented by this job instance.
# Calls #perform on the class given in the payload with the
# arguments given in the payload.
def perform
  begin
    hooks = {
      :before => before_hooks,
      :around => around_hooks,
      :after => after_hooks
    }
    JobPerformer.new.perform(payload_class, args, hooks)
    # If an exception occurs during the job execution, look for an
    # on_failure hook then re-raise.
  rescue Object => e
    run_failure_hooks(e)
    raise e
  end
end

#  https://github.com/resque/resque/blob/master/lib/resque/job_performer.rb#L12
# This is the actual performer for a single unit of work.  It's called
# by Resque::Job#perform
# Args:
#   palyoad_class: The class to call ::perform on
#   args: An array of args to pass to the payload_class::perform
#   hook: A hash with keys :before, :after and :around, all arrays of
#         methods to call on the payload class with args
def perform(payload_class, args, hooks)
  @job      = payload_class
  @job_args = args || []
  @hooks    = hooks

  # before_hooks can raise a Resque::DontPerform exception
  # in which case we exit this method, returning false (because
  # the job was never performed)
  return false unless call_before_hooks
  execute_job
  call_hooks(:after)

  performed?
end

# https://github.com/resque/resque/blob/master/lib/resque/job_performer.rb#L28
def call_before_hooks
  begin
    call_hooks(:before)
    true
  rescue Resque::DontPerform
    false
  end
end

# https://github.com/resque/resque/blob/master/lib/resque/job_performer.rb#L37
def execute_job
  # Execute the job. Do it in an around_perform hook if available.
  if hooks[:around].empty?
    perform_job
  else
    call_around_hooks
  end
end

# https://github.com/resque/resque/blob/master/lib/resque/job_performer.rb#L67
def perform_job
  result = job.perform(*job_args)
  job_performed
  result
end

由上述代码可以看出,其实真正执行的方法还是job classperform方法,并且会去执行诸如before_perform around_perform after_perform等为前缀的job class方法. 整个Resque同步执行的过程就是这样!

但是

看完代码之后,我直接提了个issue, 不知道是不是没开发完还是怎么回事!inline的描述和真实的行为竟然不一样!!!inlinetrue,还是会去执行callbacks!

最后

看了这篇文章后,脑子里应该能有一个大概的流程图,知道Resque大概的执行过程,这样我的目标就算达到了.后面我会再写一篇Resque 异步执行的文章,其实都是一样的,这个留到下次文章再分析.

Let Let! Subject Subject! 的区别

Published on:
Tags: rails, rspec, test

当我遇到不明白的事物的时候,我会先看表象,了解到表象后,再深入了解其内部。不能说这种方法好,但是对我来说还是挺有效的(年轻的coder)。当然,最好还是直接深入内部去了解,这样更省事省时。
到正题,let 与 let的说明如下:

Use let to define a memoized helper method. The value will be cached
across multiple calls in the same example but not across examples.

Note that let is lazy-evaluated: it is not evaluated until the first time
the method it defines is invoked. You can use let! to force the method's
invocation before each example.

其实 let let! 以及 subject 都是 memoized_helpers,都是通过委托来定义一个消息的接收方!好处就是可以让代码更精炼,提高可读性,减少重复。
要比较三者间的区别,必须先聊聊let

1
2
3
4
5
6
7
8
9
10
11
def let(name, &block)
  # We have to pass the block directly to `define_method` to
  # allow it to use method constructs like `super` and `return`.
  MemoizedHelpers.module_for(self).define_method(name, &block)

  # Apply the memoization. The method has been defined in an ancestor
  # module so we can use `super` here to get the value.
  define_method(name) do
    __memoized.fetch(name) { |k| __memoized[k] = super(&nil) }
  end
end

首先定义一个MemoizedHelpers,就是memoize method,只要调用过,就会被缓存起来,下次调用还是返回上次相同的对象。 验证起来也很简单,可以看看两次调用结果的 object_id

1
2
3
4
5
6
7
8
describe User do
  let(:user) { User.new }

  it 'test user' do
    puts user.object_id
    puts user.object_id
  end
end

输出结果:

70186054302440
70186054302440

四者间的区别

首先上源码吧! let 源码上面已经贴了, 接下来看看 let! subject subject!的源码。

let!

1
2
3
4
def let!(name, &block)
  let(name, &block)
    before { __send__(name) }
  end

subject

1
2
3
4
5
6
7
8
9
10
11
12
def subject(name=nil, &block)
  if name
    let(name, &block)
    subject { __send__ name }

    self::NamedSubjectPreventSuper.define_method(name) do
      raise NotImplementedError, "`super` in named subjects is not supported"
    end
  else
    let(:subject, &block)
  end
end

可以看得出,let!其实也是调用let,并且在before each调用,区别就是let!会在before each调用,而let不会,区别仅此而已! subject就稍微的复杂了一点,首先如果subject指定了 name ,则会先用let生成一个叫 name 的 memoize method,然后再次用let生成一个叫 subject 的memoize method。这样当调用 subject 的时候,其实最终调用的是 name 这个memoize method。当subject没有指定 name 的时候,则生成用let直接生成一个名叫 subject 的memoize method。额,是不是有点乱?其实细细体味一下这个方法,哇塞,真的很棒,还这么精炼!用到了递归,哈哈!但是我又有点奇怪,为什么要多此一聚呢,而不直接用let。恩,看源码。

1
2
3
def should(matcher=nil, message=nil)
  RSpec::Expectations::PositiveExpectationHandler.handle_matcher(subject, matcher, message)
end

原来,subject 是用来配合 should 进行隐式调用的,在这里何为隐式调用?看源码里面的例子:

1
2
3
4
5
describe CheckingAccount, "with $50" do
  subject { CheckingAccount.new(Money.new(50, :USD)) }
  it { should have_a_balance_of(Money.new(50, :USD)) }
  it { should_not be_overdrawn }
end

恩,rspec设计的就是这么巧妙!

subject!

这个就不需要多说了,也就是比subject多了句before each

1
2
3
4
def subject!(name=nil, &block)
  subject(name, &block)
  before { subject }
end

总结

其实什么时候用let subject,这个可以根据字面意思来,如果是测试主题,并且需要多次调用,就可有写成一个subject。而非测试主题,又需要多次调用,则可以用let

Ruby 函数式编程 by Arnau Sanchez

Published on:

本文档翻译自 Arnau Sanchez (tokland)所编译的这份文档 RubyFunctionalProgramming

同时也有日文版本

目录

简介

命令式编程比较牛吗? 不!不!不!只是比较快,比较简单,比较诱人而已。

x = x + 1

在以前上小学的美好回忆裡,我们可能都曾对上面这行感到疑惑。这个 x 到底是什么呢?为什么加了一之后,x 仍然还是 x

不知道为什么,我们就开始写程序了,也就不在乎这是为什么了。心想:“嗯”,“这不是什么大问题,编程就是事情做完最重要,没有必要去挑剔数学的纯粹性 (让大学裡的大鬍子教兽们去烦恼就好)” 。但我们错了,也因此付出极高的代价,只因我们不了解它。

理论部分

维基百科的解释:“函数式编程是一种写程序的范式,将计算视为对数学函数的求值,并避免使用状态及可变的数据” 换句话说,函数式编程提倡没有副作用的代码,不改变变量的值。这与命令式编程相反,命令式编程强调改变状态。

令人惊讶的是,函数式编程就这样而已。那…有什么好处呢?

  • 更简洁的代码:“变量”一旦定义之后就不再改动,所以我们不需要追踪变量的状态,就可以理解一个函数、方法、类别、甚至是整个项目是怎么工作的。

  • 引用透明:表达式可以用本身的值换掉。如果我们用同样的参数调用一个函数,我们确信输出会是一样的结果(没有其它的状态可改变它的值)。这也是为什么爱因斯坦说:“重复做一样的事却期望不同的结果”是疯狂的理由。

引用透明打开了前往某些美妙事物的大门

  • 并行化:如果调用函数是各自独立的,则他们可以在不同的进程甚至是机器裡执行,而不会有竞态条件的问题。“平常” 写并发程序讨厌的细节(锁、semaphore…等)在函数式编程裡面通通消失不见了。

  • 记忆化:由于函数调用的结果等于它的返回值,我们可以把这些值缓存起来。

  • 模组化:代码裡不存有状态,所以我们可以将项目用小的黑箱连结起来,函数式编程提倡自底向上的编程风格。

  • 容易调试:函数彼此互相隔离,只依赖输入与输出,所以很容易调试。

Ruby的函数式编程

一切都是这么美好,但怎样才能将函数式编程,应用到每天写 Ruby(Ruby 不是个函数式语言)的程序开发裡呢?函数式编程广义来说,是一种风格,可以用在任何语言。当然啦,用在特别为这种范式打造的语言裡显得更自然,但某种程度上来说,可以应用到任何语言。

让我们先釐清这一点:本文没有要提倡古怪的风格,比如仅仅为了要延续理论函数式编程的纯粹性所带来的古怪风格。反之,我想说的重点是,我们应该 当可以提昇代码的品质的时候,才使用函数式编程 ,不然这只不过是个糟糕的解决办法。

不要更新变量

别更新它们,创造新的变量。

不要对数组或字串做 append

No:

Ruby indexes = [1, 2, 3] indexes << 4 indexes # [1, 2, 3, 4]

Yes:

Ruby indexes = [1, 2, 3] all_indexes = indexes + [4] # [1, 2, 3, 4]

不要更新 hash

No:

Ruby hash = {:a => 1, :b => 2} hash[:c] = 3 hash

Yes:

Ruby hash = {:a => 1, :b => 2} new_hash = hash.merge(:c => 3)

牵扯到内存位置的地方,不要使用破坏性方法。

No:

Ruby string = "hello" string.gsub!(/l/, 'z') string # "hezzo"

Yes:

Ruby string = "hello" new_string = string.gsub(/l/, 'z') # "hezzo"

如何累积值

No:

Ruby output = [] output << 1 output << 2 if i_have_to_add_two output << 3

Yes:

Ruby output = [1, (2 if i_have_to_add_two), 3].compact

用 Blocks 作为高阶函数

如果一个语言要搞函数式,会需要高阶函数。高阶函数是什么?函数可以接受别的函数作为参数,并可以返回函数,就这么简单。

Ruby (与 Smalltalk 还有其它语言)在这个方面上非常特别,语言本身就内置这个功能: blocks 区块。区块是一段匿名的代码,你可以随意的传来传去或是执行它。让我们看区块的典型用途,来构造函数式编程的构造子。

init-empty + each + push = map

  No:

  ```Ruby
  dogs = []
  ["milu", "rantanplan"].each do |name|
  dogs << name.upcase
  end
  dogs # => ["MILU", "RANTANPLAN"]
  ```

  Yes:

  ```Ruby
  dogs = ["milu", "rantanplan"].map do |name|
  name.upcase
  end # => ["MILU", "RANTANPLAN"]
  ```

init-empty + each + conditional push -> select/reject

No:

Ruby dogs = [] ["milu", "rantanplan"].each do |name| if name.size == 4 dogs << name end end dogs # => ["milu"]

Yes:

Ruby dogs = ["milu", "rantanplan"].select do |name| name.size == 4 end # => ["milu"]

initialize + each + accumulate -> inject

No:

Ruby length = 0 ["milu", "rantanplan"].each do |dog_name| length += dog_name.length end length # => 15

Yes:

Ruby length = ["milu", "rantanplan"].inject(0) do |accumulator, dog_name| accumulator + dog_name.length end # => 15

在这个特殊情况下,当累积器与元素之间有操作进行时,我们不需要区块,只要将操作传给符号即可。

Ruby length = ["milu", "rantanplan"].map(&:length).inject(0, :+) # 15

empty + each + accumulate + push -> scan

想像一下,你不仅想要摺迭(fold)的结果,也想要过程中产生的部分数值。用命令式编程风格,你可能会这么写:

Ruby lengths = [] total_length = 0 ["milu", "rantanplan"].each do |dog_name| lengths << total_length total_length += dog_name.length end lengths # [0, 4, 15]

在函数式的世界裡,Haskell 称之为 scan, C++ 称之为 partial_sum, Clojure 称之为 reductions

令人讶异的是,Ruby 居然没有这样的函数!让我们自己写一个。这个怎么样:

Ruby lengths = ["milu", "rantanplan"].partial_inject(0) do |dog_name| dog_name.length end # [0, 4, 15]

Enumerable#partial_inject 可以这么实现:

Ruby module Enumerable def partial_inject(initial_value, &block) self.inject([initial_value, [initial_value]]) do |(accumulated, output), element| new_value = yield(accumulated, element) [new_value, output << new_value] end[1] end end

实作的细节不重要,重要的是,当认出一个有趣的模式可以被抽象化时,我们将其写在另一个函式库,撰写文档,反覆测试。现在只要让实际的需求去完善你的扩充即可。

initial assign + conditional assign + conditional assign + …

这样的程序我们常常看到:

Ruby name = obj1.name name = obj2.name if !name name = ask_name if !name

在此时你应该觉得这样的代码使你很不自在(一个变量一下是这个值,一下是这个;变量名 name 到处都是…等)。函数式的方式更简短,也更简洁:

Ruby name = obj1.name || obj2.name || ask_name

另一个有更复杂条件的例子:

Ruby def get_best_object(obj1, obj2, obj3) return obj1 if obj1.price < 20 return obj2 if obj2.quality > 3 obj3 end

可以写成像是这样的一个表达式:

Ruby def get_best_object(obj1, obj2, obj3) if obj1.price < 20 obj1 elsif obj2.quality > 3 obj2 else obj3 end end

确实有一点囉嗦,但逻辑比一堆行内 if/unless 来得清楚。经验法则告诉我们,仅在你确定会用到副作用时,使用行内条件式,而不是在变量赋值或返回的场合使用:

“`Ruby country = Country.find(1) country.invade if country.has_oil?

more code here

“`

如何从 enumerable 创造一个 hash

Vanilla Ruby 没有从 Enumerable 转到 Hash 的直接对应(本人认为是一个遗憾的缺陷)。这也是为什么新手持续写出下面这个糟糕的模式(而你怎么能责怪他们呢?唉!):

  ```Ruby
  hash = {}
  input.each do |item|
  hash[item] = process(item)
  end
  hash
  ```

  这真的非常可怕!阿~~~!但手边有没有更好的办法呢?过去 Hash 构造子需要一个有着连续键值对的 flatten 集合 (阿,用 flatten 数组来描述映射?Lisp 曾这么做,但还是很丑陋)。幸运的是,Ruby 的最新版本也接受键值对,这样更有意义(作为 `hash.to_a` 的逆操作),现在你可以这么写:

  ```Ruby
  Hash[input.map do |item|
  [item, process(item)]
  end]
  ```

  不赖嘛,但这打破了平常的撰写顺序。在 Ruby 我们期望从左向右写,给对象调用方法。而“好的”函数式方式是使用 `inject`:

  ```Ruby

input.inject({}) do |hash, item| hash.merge(item => process(item)) end “`

我们都同意这还是很囉嗦,所以我们最好将它放在 Enumerable 模组,Facets 正是这么干的。它称之为 Enumerable#mash:

Ruby module Enumerable def mash(&block) self.inject({}) do |output, item| key, value = block_given? ? yield(item) : item output.merge(key => value) end end end

“`Ruby [“functional”, “programming”, “rules”].map { |s| [s, s.length] }.mash

{“rules”=>5, “programming”=>11, “functional”=>10}

“`

或使用 mash 及 选择性区块来一步完成:

“`Ruby [“functional”, “programming”, “rules”].mash { |s| [s, s.length] }

{“rules”=>5, “programming”=>11, “functional”=>10}

“`

面向对象与函数式编程

Joe Armstrong (Erlang 发明人) 在 “Coders At work” 谈论过面向对象编程的重用性:

“我认为缺少重用性是面向对象语言造成的,而不是函数式语言。面向对象语言的问题是,它们带着语言执行环境的所有隐含资讯四处乱窜。你想要的是香蕉,但看到的却是香蕉拿在大猩猩手裡,而大猩猩的后面是整个丛林”

公平点说,我的看法是这不是面向对象编程的本质问题。你可以写出函数式的面向对象程序,但确定的是:

  • 典型的 OOP 倾向强调改变对象的状态。
  • 典型的 OOP 倾向层与层之间紧密的耦合。
  • 典型的 OOP 将同一性(identity)与状态的概念搞溷了。
  • 数据与代码的混合物,导致了概念与实际的问题产生。

Rich Hickey,Clojure 的发明人(一个给 JVM 用的函数式 Lisp 方言),在这场出色的演讲裡谈论了状态、数值以及同一性。

万物皆表达式

可以这么写:

Ruby if found_dog == our_dog name = found_dog.name message = "We found our dog #{name}!" else message = "No luck" end

然而,控制结构(if, while, case 等)也返回表达式,所以只要这样写就好:

Ruby message = if found_dog == my_dog name = found_dog.name "We found our dog #{name}!" else "No luck" end

这样子我们不用重复变量名 message,企图也更明显:当有段长的程序(用了一堆我们不在乎的变量),我们可以专注在程序在干什么(返回讯息)。再强调一次,我们在缩小程序的作用域。

另一个函数式程序的好处是,表达式可以用来构造数据:

“`Ruby { :name => “M.Cassatt”,

:paintings => paintings.select { |p| p.author == "M.Cassatt" },
:birth => painters.detect { |p| p.name == "M.Cassatt" }.birth.year,
...

}

1
2
3
4
5
6
7
8

### 递归

纯函数式语言没有隐含的状态,大量利用了递归。要避免栈溢出,函数式使用一种称为尾递归优化(TCO)的机制。Ruby 1.9 有实作这种机制,但缺省没有打开。要是你希望你的程序,在哪都可以动的话,就不要使用它。

但是某些情况下,递归仍然是很有用的,即便是每次递归时都创建新的栈。注意!某些递归的用途可以用 foldings 来实现(像 Enumerable#inject)。

在 MRI-1.9 启用 TCO:

Ruby RubyVM::InstructionSequence.compile_option = { :tailcall_optimization => true, :trace_instruction => false, }

1
2

简单例子:

Ruby module Math def self.factorial_tco(n, acc=1) n < 1 ? acc : factorial_tco(n-1, n*acc) end end “`

在递归深度不太可能很深的情况下,你仍可以使用:

“`Ruby class Node has_many :children, :class_name => “Node”

        def all_children
        self.children.flat_map do |child|
        [child] + child.all_children
        end
        end
        end
        ```

惰性枚举器

        惰性求值延迟了表达式的求值,在真正需要时才会求值。与 eager evaluation 相反,eager evaluation 当一个变量被赋值时、函数被调用时…甚至根本没用到变量等状况,都立马对表达式求值,惰性不是函数式编程的必需品,但这是个符合函数式范式的好策略(Haskell 大概是最佳的例子,瀰漫着懒惰的语言)。

        Ruby 所採用的基本上是 eager evaluation(虽然许多其它的语言,在条件还没满足前不对表达式求值,以及短路布林运算 `&&`, `||` 等)。然而,与任何内置高阶函数的语言一样,延迟求值是隐性支援,因为程序员自己决定区块何时被调用。

        Enumerators 同样 从 Ruby 1.9 开始支援(1.8 请用 backports),它们提供了一个简单的介面来定义惰性 enumerables。经典的例子是构造一个枚举器,返回所有的自然数:

        ```Ruby
        require 'backports' # 1.8 才需要
        natural_numbers = Enumerator.new do |yielder|
        number = 1
        loop do
        yielder.yield number
        number += 1
        end
        end
        ```

        可以用更函数式的精神改写:

        ```Ruby
        natural_numbers = Enumerator.new do |yielder|
        (1..1.0/0).each do |number|
        yielder.yield number
        end
        end
        ```

“`Ruby natural_numbers.take(10)

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

“`

现在,试试给 natural_numbersmap,发生什么事?它不会停止。标准的 enumerable 方法 (map, select 等)返回一个数组,所以在输入流是无穷大时,无法正常工作。让我们扩展 Enumerator 类别,比如加入这个惰性的 Enumerator#map:

  ```Ruby
  class Enumerator
  def map(&block)
  Enumerator.new do |yielder|
  self.each do |value|
  yielder.yield(block.call(value))
  end
  end
  end
  end
  ```

  现在我们可以给所有自然数的流做 `map` 了:

  ```Ruby
  natural_numbers.map { |x| 2*x }.take(10)

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

  ```

枚举器是用来构造惰性行为的区块的好东西,但你可以使用用懒惰风格,实作了所有 enumerable 方法的函式库:

https://github.com/yhara/enumerable-lazy

“`Ruby require ‘enumerable/lazy’ (1..1.0/0).lazy.map { |x| 2*x }.take(10).to_a

[2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

“`

惰性求值的好处

  1. 显而易见的好处: 无需在不必要的情况下,构造、储存完整的结构(也许,可以更有效率的使用 CPU 及内存)

  2. 不太显而易见的好处: 惰性求值使写程序不需要了解超出你所需的范围。让我们看一个例子:你写了某种解题工具,可以提供无数种解法,但在某个时候,你只想要前十种解法。你可能会这么写:

Ruby solver(input, :max => 10)

当你与惰性结构一起工作时,不需要说什么时候该结束。调用者自己会决定他需要多少值。代码变得更简单,责任归属到对的地方,也就是调用者:

Ruby solver(input).take(10)

一个实际的例子

练习:“前十个平方可被五整除的自然数的和是多少?”

Ruby Integer::natural.select { |x| x**2 % 5 == 0 }.take(10).inject(:+) #=> 275

让我们跟等价的命令式版本来比较:

Ruby n, num_elements, sum = 1, 0, 0 while num_elements < 10 if n**2 % 5 == 0 sum += n num_elements += 1 end n += 1 end sum #=> 275

我希望这个例子展示了这个文档裡讨论的函数式编程的优点:

  1. 更简洁: 你会撰写更少的代码。函数式程序处理的是表达式,而表达式可以连锁起来;命令式程序处理的是变量的改动(叙述式),而这不能连锁。

  2. 更抽象: 你可以争论我们使用 select, inject…等等,来隐藏了一大堆代码,我很高兴你这么说,因为我们正是这么干的。将通用的、可重用的代码隐藏起来,这是所有编程的重点 –– 但函数式编程特别是关于如何撰写抽象。感到开心不是因为写了更少的代码,而是因为藉由认出可重用的模式,简化了代码的复杂性。

  3. 更有声明式的味道: 看看命令式的版本,第一眼看起来是一沱无用的代码 –– 没有注解的话 –– 它会做什么你完全没有概念。你可能会说:“好吧,从这裡开始读,草草记下 nsum 的值,进入某个迴圈,看看 nsum 的值如何变化,看看最后一次迭代的情形” 等等。函数式版本另一方面是自我解释的,函数式版本描述、声明它在干的事,而不是如何干这件事。

“函数式编程就像是将你的问题叙述给数学家一样。命令式编程像是给白痴下指令” (arcus 在 Freenode #scheme 频道所说)

结论

  更好的理解函数式编程的原理,帮助我们写出更清晰、重用性更高并更简洁的代码。Ruby 基本上是一个命令式语言,但它也有很大的函数式能力,明白什么时候用,及如何用(以及何时不该用)这些能力。将这句话当成你的座右铭吧 “状态是万恶的根源,尽可能避免它。”

简报

  Workshop at [Conferencia Rails 2011](http://conferenciarails.org/): [Functional Programming with Ruby](http://public.arnau-sanchez.com/ruby-functional/) [(slideshare)](http://www.slideshare.net/tokland/functional-programming-with-ruby-9975242)

延伸阅读

  http://en.wikipedia.org/wiki/Functional_programming

http://www.defmacro.org/ramblings/fp.html 译文

http://www.cse.chalmers.se/~rjmh/Papers/whyfp.html

http://www.khelll.com/blog/ruby/ruby-and-functional-programming/

http://www.bestechvideos.com/2008/11/30/rubyconf-2008-better-ruby-through-functional-programming

http://channel9.msdn.com/Blogs/pdc2008/TL11

http://www.infoq.com/presentations/Value-Identity-State-Rich-Hickey

授权

This document is licensed under the CC-By 3.0 License, which encourages you to share these documents. See http://creativecommons.org/licenses/by/3.0/ for more details.

CC-By 3.0 License http://creativecommons.org/licenses/by/3.0/

Resque 数据库链接错误

Published on:
Tags: Mysql, Rails, Resque

  最近项目用的 Resque 老是会有一些莫名其妙的问题,非常头疼!

1
Mysql::Error: MySQL server has gone away: SHOW FIELDS FROM `deals`

实在是没办法了,然后仔细的去阅读了下 Resquewiki,有种恍然大悟的感觉。 原来我遇到的问题大家都遇到过,并且给出了解决方案,就拿 Resque 数据库链接错误来说,Resque 原作者已经有了推荐的解决方案,原文如下:
  If your workers remain idle for too long they may lose their MySQL connection. If that happens we recommend using this Gist.
  然后我在项目 config/initializers 目录中的 resque.rb 文件中加入代码:

1
2
3
Resque.after_fork do |job|
  ActiveRecord::Base.connection_handler.verify_active_connections!
end

问题就这样解决了!但是,引入一个 Gem,我连文档都没有仔细阅读,匆匆使用了事,现在想想真的很惭愧! 以后引入 Gem 最起码要将文档通读一边,有能力更应该通读源码!
谨记!

Resque 重启命令

Published on:
Tags: God, Resque

  现在 Resque 都应该加上 god 监控,不然 Resque worker 进程很容易就死掉! god 在 worker 进程挂掉后会自动重启, 如果项目更新,需要手动重启所有的 worker,可以 kill 掉所有的 worker 进程(千万别 kill -9,最好是 kill -s QUIT,这样会在当前 job 完成后再退出), 方法如下:

1
ps -e -o pid,command | grep [r]esque-[0-9] | cut -d ' ' -f 1 | xargs  kill -s QUIT