【AICon】探索RAG 技术在实际应用中遇到的挑战及应对策略!AICon精华内容已上线73%>>> 了解详情
写点什么

在 Ruby 中对字符串和 block 求解

  • 2007-08-24
  • 本文字数:7800 字

    阅读完需:约 26 分钟

介绍

对包含代码的字符串和 block 求解,是我最钟爱的 Ruby 特性之一。Ruby 提供了多种不同类型的求解方式;不过我最常用的是下面这些:eval、instance_eval 和 class_eval。

Module.class_eval

使用 Module 类的 class_eval(及其别名 module_eval)方法,可以在一个类的定义或者 module 定义的上下文中对给定字符串或 block 进行求解。我们常常用 class_eval 来向类的定义中加入方法,或是包含其他的 module。

klass = Class.new<br></br> klass.class_eval do<br></br> include ERB::Util<p> def encoded_hello</p><br></br> htnl_escape "Hello World"<br></br> end<br></br> end<p> klass.new.encoded_hello #=> <strong>Hello World</strong></p>不使用 class_eval 也可以达到上面的效果,但是要牺牲代码的可读性。

klass = Class.new<br></br> klass.send :include, ERB::Util<br></br> klass.send :define_method, :encoded_hello do<br></br> html_escape "Hello World"<br></br> end<br></br> klass.send :public, :encoded_hello<p> klass.new.encoded_hello #=> <strong>Hello World</strong></p>### Object.instance_eval

使用 Object 的 instance_eval 方法,可以在一个类实例的上下文中对给定字符串或 block 进行求解。这是个功能强大的概念:你可以先在任何上下文中创建一块代码,然后在一个单独的对象实例的上下文中对这块代码进行求解。为了设定代码执行的上下文,self 变量要设置为执行代码时所在的对象实例,以使得代码可以访问对象实例的变量。

class Navigator<br></br> def initialize<br></br> @page_index = 0<br></br> end<br></br> def next<br></br> @page_index += 1<br></br> end<br></br> end<br></br> navigator = Navigator.new<br></br> navigator.next<br></br> navigator.next<br></br> navigator.instance_eval "@page_index" #=> 2<br></br> navigator.instance_eval { @page_index } #=> 2与使用 class_eval 的示例类似,实例变量的值可以通过其他的方式获取,不过使用 instance_eval 是一种非常直观的做法。

Kernel.eval

使用 Kernel 的 eval 方法可以在当前上下文中对一个字符串求解。可以选择为 eval 方法制定一个 binding 对象。如果给定了一个 binding 对象,求解的过程会在 binding 对象的上下文中执行。

hello = "hello world"<br></br> puts eval("hello") #=> "hello world"<p> proc = lambda { hello = "goodbye world"; binding }</p><br></br> eval("hello", proc.call) #=> "goodbye world"### 扩展 eval 的上下文

第一次使用 eval,我用它来创建了 attr_init 这个类方法。当时我发现我总是在重复下面代码中的模式:

def some_attribute<br></br> @some_attribute || = SomeClass.new<br></br> end因此我决定创建一个类方法来封装上面的行为:

class << Object<br></br> def attr_init(name, klass)<br></br> define_method(name) { eval "@#{name} ||= #{klass}.new" }<br></br> end<br></br> end记得当时我觉得这样调用 eval 是非常丑陋的做法,但那会儿我想不出更好的方式来实现这样的效果;因此我把代码贴到了博客中,等待别人的指摘;他们很快就做出了回应,并给出下面的做法。一开始我并没有觉察这样做的好处,但是后来我意识到这个解法是非常出色的:它只需要调用一次 eval 方法,而不是在每次进行方法定义时都去重新调用 eval。

class << Object<br></br> def attr_init(name, klass)<br></br> eval "define_method(name) { @#{name} ||= #{klass}.new }"<br></br> end<br></br> end这样优化的有趣之处在于:它需要求解更多的内容, 以达到提升运行效率的目的。从那时开始,我只在必要的时候才使用 eval,而且我非常注意如何以更有效率的方式来使用 eval。

在不同上下文中使用 instance_eval

在不同上下文中,对 block 或是以字符串形式出现的代码进行求解是很有价值的一种做法,也是设计领域特定语言(Domain Specific Language,DSL)时很常用的一种技术。实际上,在多种上下文环境中进行求解的能力是使用 DSL 的一个关键因素。请看下面的代码:

class SqlGenerator<br></br> class << self<br></br> def evaluate(&script)<br></br> self.new.instance_eval(&script)<br></br> end<br></br> end<p> def multiply(arg)</p><br></br> "select #{arg}"<br></br> end<p> def two(arg=nil)</p><br></br> "2#{arg}"<br></br> end<p> def times(arg)</p><br></br> " * #{arg}"<br></br> end<br></br> end使用上面的代码,调用 SqlGenerator.evaluate 方法并给定一个 block 参数,便可以生成一条 SQL 语句:

SqlGenerator.evaluate { multiply two times two }<br></br> => "select 2 * 2"然而,你还可以在一个 calculator 类的上下文中执行同样的代码来获得结果:

class Calculator<br></br> class << self<br></br> def evaluate(&script)<br></br> self.new.instance_eval(&script)<br></br> end<br></br> end<p> def multiply(arg)</p><br></br> eval arg<br></br> end<p> def two(arg=nil)</p><br></br> "2#{arg}"<br></br> end<p> def times(arg)</p><br></br> " * #{arg}"<br></br> end<br></br> end执行结果:

Calculator.evaluate { multiply two times two }<br></br> => 4上述代码展示了如何使用 instance_eval 来指出 block 执行的作用范围。我在前面提到过,instance_eval 方法在接受者的上下文中对字符串或 block 展开求解。例子中的接收者是 SqlGenerator 的一个实例和 Calculator 的一个实例。同时要保证使用 self.new.instance_eval 这样的方式来调用。如果不调用 self 的 new 方法,会将 block 作为类的一部分进行求解,而不是在类的实例中完成。

上述代码同样展示了开始定义 DSL 所需的一些步骤。创建 DSL 是很有挑战性的工作,但同时会带来很多好处。通过 DSL 来表达业务规则,所带来的好处是可以在多种上下文中执行这些业务规则。如上述示例所展示的,通过在不同上下文中执行 DSL,可以从同一个业务规则产生多种不同的行为。当业务规则随着时间推移而改变时,系统中所有引用该业务规则的构成部分都会随之发生变化。而对 Ruby 求解方法的利用,就是成功实现这种效果的关键。

关于赌场中扑克牌桌的示例

Ruby 提供的不同的求解方法,让我们可以很方便的在不同上下文中执行代码。举例来说,假设你为一个赌场工作,分派给你的任务是设计一个系统。当需要开一张新的扑克牌桌,或是需要知道等多久才能开新牌桌时,这个系统负责通知扑克牌室的工作人员。新开牌桌的业务规则,根据牌桌上的赌注大小和等待列表中的人数多少而不同。例如,对于一个赌注不封顶的牌局来说,牌桌边等待的人数多一些也无妨,因为人们更有可能在一手牌中输光他们所有的钱;如果贸然开新的牌桌,由于没有足够的玩家,该牌桌可能很快就要关闭。规则在 DSL 中可能以下面的方式表示: if the '$5-$10 Limit' list is more than 12 then notify the floor to open<br></br> if the $1-$2 No Limit' list is more than 15 then notify the floor to open<br></br> if the '$5-$10 Limit' list is more than 8 then notify the brush to announce<br></br> if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce第一个执行 DSL 的上下文被用来通知赌场雇员。代码如下:

class ContextOne < DslContext<p> bubble :than, :is, :list, :the, :to</p><p> def more(value)</p><br></br> '> ' + value.to_s<br></br> end<p> def method_missing(sym, *args)</p><br></br> @stakes = sym<br></br> eval "List.size_for(sym) #{args.first}"<br></br> end<p> def floor(value)</p><br></br> __position(value, :floor)<br></br> end<p> def brush(value)</p><br></br> __position(value, :brush)<br></br> end<p> def open</p><br></br> __action(:open)<br></br> end<p> def announce</p><br></br> __action(:announce)<br></br> end<br></br> def __action(to)<br></br> { :action => to }<br></br> end<p> def __position(value, title)</p><br></br> value[:position] = title<br></br> value<br></br> end<p> def notify(value)</p><br></br> [@stakes, value]<br></br> end<p> end</p>ContextOne 通过下面的代码执行。

script = <<-eos if the '$5-$10 Limit' list is more than 12 then notify the floor to open<br></br> if the '$1-$2 No Limit' list is more than 15 then notify the floor to open<br></br> if the '$5-$10 Limit' list is more than 8 then notify the brush to announce<br></br> if the '$1-$2 No Limit' list is more than 10 then notify the brush to announce eos
class Broadcast<br></br> def self.notify(stakes, options)<br></br> puts DslContext.sym_to_stakes(stakes)<br></br> options.each_pair do |name, value|<br></br> puts " #{name} #{value}"<br></br> end<br></br> end<br></br> end<p> ContextOne.execute(script) do |notification|</p><br></br> Broadcast.notify(*notification)<br></br> endContextOne 继承自 DslContext。DslContext 的定义如下。

class DslContext<br></br> def self.execute(text)<br></br> rules = polish_text(text)<br></br> rules.each do |rule|<br></br> result = self.new.instance_eval(rule)<br></br> yield result if block_given?<br></br> end<br></br> end<p> def self.bubble(*methods)</p><br></br> methods.each do |method|<br></br> define_method(method) { |args| args }<br></br> end<br></br> end<p> def self.polish_text(text)</p><br></br> rules = text.split("\n")<br></br> rules.collect do |rule|<br></br> rule.gsub!(/'.+'/,extract_stakes(rule))<br></br> rule << " end"<br></br> end<br></br> end<p> def self.extract_stakes(rule)</p><br></br> stakes = rule.scan(/'.+'/).first<br></br> stakes.delete!("'").gsub!(%q{$},'dollar').gsub!('-','dash').gsub!(' ','space')<br></br> end<p> def self.sym_to_stakes(sym)</p><br></br> sym.to_s.gsub!('dollar',%q{$}).gsub!('dash','-').gsub!('space',' ')<br></br> end<br></br> end<ContextOne 的 method_missing 方法中使用了 List 类,List 类代码如下。

class List<br></br> def self.size_for(stakes)<br></br> 20<br></br> end<br></br> endContextOne 使用 DSL 检查每张牌桌的 List 大小,并在必要的时候发送通知。当然,这只是演示代码,List 对象也只不过是 stub,以验证 ContextOne 和 DslContext 所有的功能都没有问题。这里要重点注意:方法的执行被委托给了 instance_eval,这样才能在 ContextOne 的上下文中对代码进行求解。

同样的脚本,可以在第二个上下文中执行;这个上下文返回当前正在散播的不同类型的赌博游戏。

class ContextTwo < DslContext<p> bubble :than, :is, :list, :the, :to, :more, :notify, :floor, :open, :brush</p><p> def announce</p><br></br> @stakes<br></br> end<p> alias open announce</p><p> def method_missing(sym, *args)</p><br></br> @stakes = sym<br></br> end<p> end</p>正像我们看到的,添加新的上下文是非常方便的。由于 DslContext 的 execute 方法调用 instance_eval 方法,上面的代码可以如下的方式执行。

ContextTwo.execute(script) do |stakes|<br></br> puts ContextTwo.sym_to_stakes(stakes)<br></br> end为了使我们的示例更加完整,我们创建另外一个例子,显示所有接收通知的位置。

class ContextThree < DslContext<p> bubble :than, :is, :list, :the, :to, :more, :notify, :announce, :open, :open</p><p> def announce; end</p><br></br> def open; end<p> def brush(value)</p><br></br> :brush<br></br> end<p> def floor(value)</p><br></br> :floor<br></br> end<p> def method_missing(sym, *args)</p><br></br> true<br></br> end<p> end</p>同样的,这个上下文也继承自使用了 instance_eval 的 DslContext,因此,只要运行下面的代码来执行即可。

ContextThree.execute(script) do |positions|<br></br> puts positions<br></br> end在多个上下文中对 DSL 进行求解的能力,模糊了代码和数据之间的界线。可以对脚本‘代码’进行求解来生成报表(比如关于系统中已联系雇员的报表)。在展示需要多久才会新开扑克牌桌这样的上下文中,也可以对脚本进行求解(比如,业务规则说明需要 15 个人才能新开一张牌桌,系统知道在等待列表中有 10 个人,因此显示“5 more people needed before the game can start”)。使用 instance_eval,我们可以在系统需要的任何上下文中,对同样的代码进行求解。

同样具有魔法的 eval

上述代码展示的是:如何在不同的作用范围中,使用 instance_eval 对 block 进行求解。不过,eval 方法同样可以在不同的上下文中进行求解操作。下面我来展示如何在 block 的作用范围中对 ruby 代码构成的字符串进行求解。

先让我们从一个简单的例子开始,不过让我们先回顾一下如何根据 block 的 binding 使用 eval。我们需要一个能够帮我们创建 block 的类。

class ProcFactory<br></br> def create<br></br> Proc.new {}<br></br> end<br></br> end在示例中,ProcFactory 类有一个方法:create;它的功能只是简单地创建并返回了一个 proc 对象。尽管这看起来似乎没什么特别之处,但我们可以在 proc 对象的作用范围中,使用它对任何包含 ruby 代码的字符串进行求解。这样,我们不需要直接引用某个对象,便可以在这个对象的上下文中求解 ruby 代码。

proc = ProcFactory.new.create<br></br> eval "self.class", proc.binding #=> ProcFactory什么时候会用到这样的功能呢?我最近在开发表示 SQL 的 DSL 时用到了它。我开始使用类似下面代码的语法:

Select[:column1, :column2].from[:table1, :table2].where do<br></br> equal table1.id, table2.table1_id<br></br> end上述代码被求解时,跟在 from 后面的 [] 实例方法将所有的表名保存在一个数组中。接下来,当执行 where 方法时,传递给 where 的 block 会执行。此时,method_missing 方法会被调用两次,第一次针对:table1,第二次针对:table2。在 method_missing 的调用中,对之前提到过的、用 [] 方法创建的表名数组进行检查,以查看标识符参数(:table1 和:table2)是否为合法的表名。如果表名在数组中,我们返回一个知道如何应对字段名称的对象;如果表名非法,我们会调用 super 并抛出 NameError。

应对一般的简单查询,上面的做法不存在问题;但如果涉及到子查询的话,就另当别论了。前述实现对下面示例中的代码是无效的。

Delete.from[:table1].where do<br></br> exists(Select[:column2].from[:table2].where do<br></br> equal table1.column1, table2.column2<br></br> end)<br></br> end不过我们可以使用 eval 与指定的 binding 一起,让上面的代码正常工作。此处的技巧是:将表名数组从外部的 block 隐式地传递到内部的 block 中。用显式方式传递会让 DSL 看起来很丑陋。

在 Select 类的 where 方法中,我们使用 block 的 binding 对象来得到 Delete 实例的 tables 集合。我们能够这样做,在于 Delete 实例的 where 方法被作为上下文(亦即 block 的 binding)传递给了 select 实例的 where 方法。binding 对象(或上下文)是 block 被创建时的作用范围。下面的代码是对 where 方法的完整实现。

def where(&block)<br></br> @text += " where "<br></br> tables.concat(eval("respond_to?(:tables) ? tables : []", <br></br>block.binding)).inspect<br></br> instance_eval &block<br></br> end我们把 eval 所在的语句拆开看看它都干了什么。它做的第一件事情是:

eval "respond_to?(:tables) ? tables : []", block.binding它的作用是“在 block 的作用范围中对语句进行求解”。在当前例子中,block 的作用范围是:

Delete.from[:table1].where do .. end这个范围是一个 Delete 类的实例,Delete 类中确实有 tables 方法,其作用是暴露表名数组(tables#=>[:table1])。因此,语句被求解后会返回表名数组。剩余的语句就可以看作:

tables.concat([:table1])此句只是将所有的表名加入到 tables 数组中,并且可以被内部的 block 访问。有了这一行代码的处理,我们就可以让子查询产生正确的结果了。

delete from table1 where exists (select column2 from table2 where table1.column1 = table2.column2)下面的代码可以产生上述结果,并且能够作为参考,以了解如何与 binding 一起使用 eval。

class Delete<br></br> def self.from<br></br> Delete.new<br></br> end<p> def [](*args)</p><br></br> @text = "delete from "<br></br> @text += args.join ","<br></br> @tables = args<br></br> self<br></br> end<p> attr_reader :tables</p><p> def where(&block)</p><br></br> @text += " where "<br></br> instance_eval &block<br></br> end<p> def exists(statement)</p><br></br> @text += "exists "<br></br> @text += statement<br></br> end<br></br> end<p> class Select</p><br></br> def self.[](*args)<br></br> self.new(*args)<br></br> end<p> def initialize(*columns)</p><br></br> @text = "select "<br></br> @text += columns.join ","<br></br> end<p> def from</p><br></br> @text += " from "<br></br> self<br></br> end<br></br> def [](*args)<br></br> @text += args.join ","<br></br> @tables = args<br></br> self<br></br> end<p> def tables</p><br></br> @tables<br></br> end<p> def where(&block)</p><br></br> @text += " where "<br></br> tables.concat(eval("respond_to?(:tables) ? tables : []", block.binding)).inspect<br></br> instance_eval &block<br></br> end<p> def method_missing(sym, *args)</p><br></br> super unless @tables.include? sym<br></br> klass = Class.new<br></br> klass.class_eval do<br></br> def initialize(table)<br></br> @table = table<br></br> end<p> def method_missing(sym, *args)</p><br></br> @table.to_s + "." + sym.to_s<br></br> end<br></br> end<br></br> klass.new(sym)<br></br> end<p> def equal(*args)</p><br></br> @text += args.join "="<br></br> end<br></br> end### 结语

正如我们所看到的那样,使用 Ruby 提供的多种求解方法,我们可以创建简练、可读的代码;这些求解方法同时提供了创建诸如领域特定语言之类强大工具的能力。

关于作者

Jay Fields 是 ThoughtWorks 的一位开发人员。他总是在寻找令人兴奋的新技术,并愿意马上采用这些技术。他最近一段时间的工作中心放在领域特定语言(DSL)上面,所交付的应用为特定业务领域专家使用 DSL 撰写应用业务规则提供了强大的支持。

查看英文原文: Evaluation Options in Ruby - - - - - -

译者简介:郑柯,目前就职于一家医药电子商务公司,从事医用耗材电子商务平台的开发与维护。有志于在中国的软件开发业界推广 Agile 的理念和方法论,笃信以人为本,关注 Ruby,关注敏捷,关注人。

2007-08-24 04:231519
用户头像

发布了 479 篇内容, 共 151.8 次阅读, 收获喜欢 47 次。

关注

评论

发布
暂无评论
发现更多内容

知道Python中的字符串是什么吗?

华为云开发者联盟

Python 编程语言 字符串 字符

融云集成之避坑指南-Android推送篇

融云 RongCloud

音视频

OpenKruise v0.8.0 版本发布:K8s 社区首个规模化镜像预热能力

阿里巴巴云原生

容器 云原生 k8s 安全 应用服务中间件

【数独问题】经典面试题:解数独 ...

宫水三叶的刷题日记

面试 LeetCode 数据结构与算法

与前端训练营的日子 -- Week19

SamGo

学习

叹服!微软自爆虐心405页程序员面试通关手册,Github上已获赞75.6K

Java架构之路

Java 程序员 架构 面试 编程语言

写作的意义

ES_her0

28天写作 3月日更

融云即时通讯SDK集成 -- 国内厂商推送集成踩坑篇(Android平台)

融云 RongCloud

即时通讯

寻找被遗忘的勇气(十二)

Changing Lin

3月日更

安卓系统开发架构!5214页PDF的进阶架构师学习笔记,成功入职腾讯

欢喜学安卓

android 程序员 面试 移动开发

Linux 高并发服务器 select/poll实现

赖猫

Linux linux编程 linux开发 Linux服务器开发

细粒度授权在安全领域的重要性

龙归科技

安全 iam 细粒度 ABAC PBAC

安卓应用程序开发理论!免费Android高级工程师学习资源,附面试题答案

欢喜学安卓

android 程序员 面试 移动开发

你的终端从未如此优雅

Kareza

终端工具 3月日更 Hyper

技术债是什么、怎么还?你想知道的都在这一篇文章里了!

禅道项目管理

技术 技术债 问题

对标阿里P7Java架构师面试题,已助我拿下字节、蚂蚁、滴滴三家Offer

Java架构之路

Java 程序员 架构 面试 编程语言

DataPipeline亮相“2021科技助力湾区数字金融发展峰会”,解锁“实时数据管理”密码

DataPipeline数见科技

嵌入式技术与人工智能有什么关系?

cdhqyj

人工智能 嵌入式 系统 科技

对标阿里P9Java架构师面试题,已助我拿下字节、蚂蚁、滴滴三家Offer

Java架构追梦

Java 阿里巴巴 架构 面试 滴滴

Redis和Memcached的区别

赖猫

redis memcached 服务器开发 Linux服务器开发

助我拿到37KOffer,这份阿里巴巴890页Redis笔记可谓功不可没

Java架构之路

Java 程序员 架构 面试 编程语言

为啥你一入场就开始跌呢?聊聊长期主义

池建强

长期主义

WebRTC服务器模型

赖猫

音视频 WebRTC

你的终端从未如此高效

Kareza

3月日更 Oh My Zsh

San CLI 的实现原理

百度Geek说

cli service san command

高质量、高并发的实时通信架构设计与探索

融云 RongCloud

架构 通信

谷歌大脑团队官方推荐,用浏览器实现深度学习的「黑科技」教程来了!

图灵社区

JavaScript 人工智能 机器学习 深度学习 大前端

融云即时通讯SDK集成 -- FCM推送集成指南(Android平台)

融云 RongCloud

即时通讯

融云 IM SDK 转 AndroidX

融云 RongCloud

IM

并发编程-原子操作CAS

赖猫

c++ 高并发 并发 CAS Linux服务器开发

数字孪生技术如何实现复制世界?关键的关键是…

华为云开发者联盟

数据中心 数字孪生 节能 仿真 数据中心网图服务

在Ruby中对字符串和block求解_Ruby_Jay Fields_InfoQ精选文章