Mark24
笔记:RubyDSL实验
导航关联
我觉得这篇比我弄的要详细,详细参考这篇
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
评论
- 神奇的搜索 —— 是便利链表指针
看到Rspec的风格,复杂的嵌套产生疑问:为什么Ruby可以把作用域来回串联,方法又可以随意绑定到main等对象上?
我想归根结底的原因是 —— 指针
extend 之类方法的作用就是 把指针和对象关联起来。
而块这种语法,也是在背后记录了指针。
函数可以调用函数自己是因为 指针。函数可以循环是以为它记住了自己的指针。而这里,事物之间能够建立联系执行,也是因为指针。
只要关联上指针,那么寻找方法的过程看起来就是在指针链条上搜索罢了。
这种“魔法”,并非并非什么魔法,而是在一种约定中遍历特殊结构但是本质依然是链表。
- 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的例子里
- extend可以给具体实例增加方法
- define_method 嵌套 #send 就可以定义 转发方法