笔记:RubyDSL实验

导航关联

Ruby特性和实验分析汇总

我觉得这篇比我弄的要详细,详细参考这篇

《baya-一步一步DSL》


DSL形态

常见的DSL形态,Rails风格

常见的DSL形式如下,在Rails中很常见:

something *args do |*items|
  ...
end

具体代码实现:


# 这种以 yield、block.call 为载体的DSL 会伴随着把自己传给外部的块

# 情况1. 带参数

class A

  attr_accessor :name

  def initialize(name, &block)
    @name = name

    if block_given?
      block.call(self)
    end
  end

  def work(arg)
    puts arg
  end
end


A.new "Atom" do |s|

  s.work "hello from dsl"

end


# 情况 2. 不带参数

class B
  attr_accessor :name

  def initialize(&block)
    @name = name

    if block_given?
      block.call(self)
    end
  end

  def work(arg)
    puts arg
  end
end

B.new do |s|
  s.work "hello from btom dsl"
end

上下文中的DSL,Rack中间件风格

其实只要没有显式的参数传递,那么就可以理解为其实是在一个上下文中进行。

DSL总是通过块传递。所以这里的关键就是 block 在那里执行。

这里其实就是 block 在 *eval 中执行。

比如 Rack

app = Rack::Builder.new do
  use Rack::CommonLogger
  use Rack::ShowExceptions
  map "/lobster" do
    use Rack::Lint
    run Rack::Lobster.new
  end
end

# 效果等价于

app = Rack::Builder.new
app.use Rack::CommonLogger
app.use Rack::ShowExceptions
app.map "/lobster" do
  use Rack::Lint
run Rack::Lobster.new
end

模拟实现


class A

  def initialize(&block)
    @name = "Atom"
    instance_eval(&block) if block_given?
  end

  def work(arg)
    puts "work call: #{arg}"
  end
end


a = A.new do
  work "FFFFF" #执行了实例内部的方法
end

写一个菜狗语言


#!/usr/bin/env ruby

script_file = ARGF.argv[0]
script_code = File.read(script_file)


module DSL
  def (text)
    @count.times do
      puts text
    end
  end

  def 循环(count)
    @count = count
  end

  def 计算(expr)
    puts expr
  end
end


extend DSL

instance_eval(script_code)


循环 10

说 "我是菜狗"

计算 1+10+12

计算 22*15

这段文本可以被真的执行

$ sudo chmod +x ./dsl.rb

$ ./dsl.rb demo.xlang 
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
我是菜狗
23
330

我最后的感觉就是,DSL 其实就是一段Ruby代码(比如这里的表达式计算,其实就是Ruby)。我们要做的就是把 我们设计的目标 DSL 放入一段我们自己的上下文中执行。

我们自己能做的只是增加了各别方法罢了。

归根结底是利用Ruby的语法特点写程序而已。

Rake风格,缓存并集中执行

rake的风格,模拟如下。

主要是以 block的形式把任务缓存起来,然后集中执行。

把task委托在top level可以方便书写任务。执行的时候交给工作类。

module FakeRake
  module DSL
    def task(*args, &block)
      FakeRake::Task.define_task(*args, &block)
    end
  end

  class Task
    @task_queue = []

    # 这是以Proc的形式把任务给缓存起来,然后集中执行
    def self.define_task(*args, &block)
      @task_queue << block
    end

    def self.execute
      @task_queue.each do |task|
        task.call
      end
    end
  end
end


# 这里是为了
self.extend FakeRake::DSL

task :wakeup do
  puts "wakeup"
end

task :eat_breafast do
  puts "eat_breafast"
end

# 最后一起执行
FakeRake::Task.execute

# output >>>>>
# ➜  ruby-test ruby test.rb
# wakeup
# eat_breafast

Rspec 风格

describe Bowling, "#score" do
  it "returns 0 for all gutter game" do
    bowling = Bowling.new
    20.times { bowling.hit(0) }
    bowling.score.should eq(0)
  end
end

评论

  1. 神奇的搜索 —— 是便利链表指针

看到Rspec的风格,复杂的嵌套产生疑问:为什么Ruby可以把作用域来回串联,方法又可以随意绑定到main等对象上?

我想归根结底的原因是 —— 指针

extend 之类方法的作用就是 把指针和对象关联起来。

而块这种语法,也是在背后记录了指针。

函数可以调用函数自己是因为 指针。函数可以循环是以为它记住了自己的指针。而这里,事物之间能够建立联系执行,也是因为指针。

只要关联上指针,那么寻找方法的过程看起来就是在指针链条上搜索罢了。

这种“魔法”,并非并非什么魔法,而是在一种约定中遍历特殊结构但是本质依然是链表。

  1. DSL和元编程

元编程 在Ruby里面侧重的是 运行时修改 语言构建。Ruby的几乎所有构件都可以通过内部提供的方法和自省来控制。

DSL的特点是建立在 元编程的基础之上, 把一段 Ruby风格的代码(主要由 方法+参数)暴露在外部成为 看起来如文本一般的语言 当成一种方言。

DSL的组成是定义和预先准备好方法。然后把 这段DSL方言,放在实现定义好方法的比如某个工作类的 上下文中 eval 执行。

我们写的归根结底是Ruby罢了。


TODO

链式风格DSL

比如ORM的形态

Person.where(category: categories, name: params[:name]).order('created_at DESC').limit(50)
module FakeActiveRecord
  class Model 
    def self.where(*arg)
      puts "where call#{arg}"
      self
    end

    def self.order(*arg)
      puts "order call:#{arg}"
      self
    end

    def self.limit(*arg)
      puts "limit call:#{arg}"
      self
    end
    
  end
end


class Person < FakeActiveRecord::Model

end

Person.where(category: 'categories', name: 'name').order("created_at DESC").limit(50)


# output >>>>>
# ➜  ruby-test ruby chain.rb
# where call[{:category=>"categories", :name=>"name"}]
# order call:["created_at DESC"]
# limit call:[50]

类宏风格

例如

  
# Rails AR的

class Contest < ActivieRecord::Base

  has_many :votes
  has_many :entites

  validate_presence_of :title
  
end

# Ruby自己的
class Book
  attr_reader :name
end


模拟 Ruby的 attr_accesstor


module Macro
  def fake_attr_accessor *method_names
    method_names.each do |m_name|
      puts m_name
      instance_eval(<<-DEF
        define_method :#{m_name} do
          @#{m_name}
        end

        define_method :#{m_name}= do |name|
          @#{m_name} = name
        end
      DEF
    )

    end
  end
end

# 想把 类宏方法捆绑到 MyClass
class MyClass
  def initialize
    @name = 'I;m MyClass'
    @age = 100
  end
  class_eval do
    # 在类作用域上下文中执行,并且编辑 self
    # 使用 self.extend 给 self增加方法
    # 这个方法即 可以动态生存方法的类宏
    # *_eval 传入字符串执行、或者传入 块 执行。
    self.extend(Macro)
  end
  fake_attr_accessor :name, :age
end


# 验证我们的类宏是否其作用

demo = MyClass.new
puts demo.name

demo.name = 'AAAA'


puts demo.name

puts demo.age


# output >>>>

# ➜  ruby-test ruby macro.rb
# name
# age
# I;m MyClass
# AAAA
# 100

补丁风格,执行预留方法 ActiveRecord::Migration


 
class CreateCategories < ActiveRecord::Migration
  def change
    create_table :categories do |t|
      t.string      :name
      t.timestamps
    end
  end
end

class AddRoleToUsers < ActiveRecord::Migration
  def change
    add_column :users, :role, :string, default: 'user' # 账号角色, user 普通用户, admin 管理员, sponsor 赞助商
  end
end

模拟 Migration风格



module ActiveRecord
  class Migration

    def change

    end


    def create_table(table_name, &block)
        puts "create_table call :#{table_name}"
        block.call(self)
    end
  end

end


class CreateTable < ActiveRecord::Migration

  def change
    create_table "user_table" do |t|
         
    end
  end
end


CreateTable.new.change


# output >>>
# ➜  ruby-test ruby macro.rb
# create_table call :user_table


我的原始笔记


补充

DSL主要用了元编程技巧

绑定到全局(或者叫顶层 top level)的DSL

比如我想要制作一个 备份的 DSL

DSL的脚本看起来像这样

# backup.dsl

backup '/path/to/my/document'

to '/path/to/my/mobile-disk'

interval 60

非常简单, 而且直接可以阅读。

想让Ruby读取这样的DSL,看怎么进行

# backup_robot.rb

def backup(dir)
  # 做读取目录的逻辑
end

def to(dir)
  # 做写入目录的逻辑
end

def interval(minutes)
  # 相关间隔时间的逻辑
end


eval(File.read('backup.dsl'))

这就是一个挂载在 顶层(Top Level)的DSL最简单的实现模板。

多个工作实例的DSL

上面假设我们做了备份这件事,只能去做一个备份实例。如果我们希望定义多个备份实例怎么办?

看起来DSL也可以写成下面的目标,新建三个Backup任务,每个任务内部独立的进行前面的备份逻辑。


Backup.new do |b|
  b.backup '/path/to/my/document/A'

  b.to '/path/to/my/mobile-disk/A'
end


Backup.new do |b|
  b.backup '/path/to/my/document/B'

  b.to '/path/to/my/mobile-disk/B'
end

紧接着看如果想要Ruby支持读取这样的DSL,我们应该如何写Ruby

class Backup
  def initialize
    yield(self) if block_given?
  end

  def backup(dir)
    puts "backup method : #{dir}"
  end

  def to(dir)
    puts "to method :#{dir}"
  end
end

Backup.new do |b|
  puts '---Backup Instance---'
  pp self

  b.backup 'call backup in dsl'

  b.to 'call to in dsl'
end


# output >>>>

# ---Backup Instance---
# main
# backup method : call backup in dsl
# to method :call to in dsl

我们的实例中开放实例属性允许读取。通过块的方式读取DSL,然后把注册完的实例,注入真正工作的实例。传入self是为了方便,按照约定 工作实例 读取需要的约定接口就可以开始工作了。

不传入 |b| 会如何

另外,其实这里的 b 不传入将会无法工作。裸词标识符调用,会被默认当成 self 调用。而块中的 self 指向了 main,而main上没有方法会报错。

Rails风格 DSL 模拟

# 模拟一个 Rails的 ActiveRecord
module ActiveRecord
  class Base
    def belongs_to(who)
      puts "Base.belongs_to call :#{who}"
    end

    def has_one(who)
      puts "Base.has_one call :#{who}"
    end
  end
end


class Book < ActiveRecord::Base
  belongs_to :user
  has_one :author
end


Sinatra风格模拟

Sinatra 把方法委托到 top level 并且执行的时候又是一个工作实例在响应。这是怎么做到的呢?

可以看下面实现的一个代理器。

class App
  def get(ctx)
    puts "get call :#{ctx}"
  end

  def post(ctx)
    puts "post call :#{ctx}"
  end
end

BaseApp = App.new


module Delegator
  def self.delegate(baseApp, *methods)
    Array(methods).each do |method_name|
      define_method(method_name) do |*args, &block|
        baseApp.send(method_name, *args, &block)
      end

      private method_name
    end
  end

  delegate BaseApp, :get, :post # :patch, :put, :delete, :head
end

extend Delegator


# --- DSL -----

get "hello world"

这里主要使用 extend

extend 是裸词调用,默认的对象self是top level的 main 所以 这里是给 main 增加了方法。而这个方法就是动态定义了 :get :post 并且使用了BaseApp去 send执行。

变相的给 main 增加了委托方法。

备注

Ruby的工作特点是动态性 —— 我们更需要关注的是 “意图”。 我们可以使用Ruby的动态方法去逼近我们的意图。 因为Ruby提供的方法特别多,并且都允许修改。

Ruby的方法并不应该死记硬背、而是灵活理解 搜索使用。

用Ruby的原则去思考,而不是记忆他的形态。

Sinatra的例子里

  1. extend可以给具体实例增加方法
  2. define_method 嵌套 #send 就可以定义 转发方法

Rake风格模拟

MiniTest风格模拟

Mark24

Everything can Mix.