论坛首页 编程语言技术论坛

Try() 和 Maybe Monad

浏览 6641 次
精华帖 (14) :: 良好帖 (1) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2008-03-07  
在Ruby里用到对象图导航(Object Graph Navigation)的时候,有时候需要判断对象是否为nil,很常见这样的代码:
#显示某个产品的分类名称
product.category ? product.category.name : nil

ozmm.org的chris最近介绍了一个好方法,他给这个方法起名叫try(),给Object添加一个try方法:
class Object
  def try(method)
    send method if respond_to? method
  end
end

这样上面的代码就可以简化成
product.category.try(:name)


这个try的实际用途很多,比如:
#删除某个可能存在的用户
User.find_by_name("JavaEye").try(:destroy)
#找出最后一个未激活用户的名字
User.find_all_by_active(false).last.try(:name)


但是这个简单的try()有很多限制,比如原先这样的代码就不能解决:
#默认值
product.category ? product.category.name : "N/A"
#多层对象图导航
product.category.owner.name


对此Anders Engström提供了一个Improved 'try()',上面的代码可以简化成
product.category.try(:name, :default => "N/A")
product.category.try(:owner, :name)


但是又有人觉得这样不够直观了,提供了一个类似Groovy做法的: A better “try()” for Ruby, why not do the Groovy way?
Groovy语言本身提供了内置的?.操作: person?.name,而上面这篇文章则通过在方法名后面添加"_"来实现相同的目的
product.category.owner_.name_


Q1: 和方法2相比,它少了一个默认值的处理,大家觉得添加这样的特性如何?
product.category.name_(:default => "N/A")


上面这3种try()方法都是通过method missing实现的,这篇文章提到的Maybe Monad也可以解决这个问题:
Maybe.new(product).category.name.value("N/A")


Q2: 这些方法你更喜欢用哪一种呢?或者你有其他更好的方法?欢迎讨论。
我比较喜欢方法2,感觉代码侵入比较小,缺点是多层导航的时候不够直观。而方法4Maybe Monad的优缺点正好与之相反,如果能综合这2种方法就好了。
   发表时间:2008-03-07  
我还看到过一个方法,就是给NilClass定义一个method_missing方法..
0 请登录后投票
   发表时间:2008-03-07  
可以这样
class NilClass
	def default(r=nil)
		r
	end
	def method_missing(sym)
		nil #这个不写也可以
	end
end

category.name.default()
category.name.default("N/A")
0 请登录后投票
   发表时间:2008-03-07  
lllyq 写道
可以这样
class NilClass
	def default(r=nil)
		r
	end
	def method_missing(sym)
		nil #这个不写也可以
	end
end

category.name.default()
category.name.default("N/A")

不过这样也不好,仅最后一级nil没问题,但如果是多级nil,则屏蔽了该有的信息,应该在method_missing里面trace,如果最终调用的是default才做这个操作,等会再改改
0 请登录后投票
   发表时间:2008-03-07  
x = a.b.c.d rescue "N/A"
0 请登录后投票
   发表时间:2008-03-07  
a.b.c.d rescue "N/A" 好像只能用到块里,例如 puts a.b.c.d rescue "N/A"就不行了
我修正了一下代码,应该没什么副作用了
class Object
  def default(r)
    self
  end
end
    
class NilClass
  @@caller_cache = {}
  @@file_cache = {}
  alias old_method_missing method_missing
  def default(r=nil)
    r
  end
  def method_missing(sym)
    @@caller_cache[caller.first] ||= begin
      x, y = caller.first.split(":")
      @@file_cache[x] ||= File.read(x).split("\n")
      @@file_cache[x][y.to_i-1].index(".default").nil?
    end
    @@caller_cache[caller.first] ? old_method_missing(sym) : nil
  end
end

a.b.c.d.default()
0 请登录后投票
   发表时间:2008-03-07  
 puts a.b.c.d rescue "N/A"

等价于
 (puts a.b.c.d) rescue "N/A"



 puts (a.b.c.d rescue "N/A")

会出错:
引用
syntax error, unexpected kRESCUE_MOD, expecting ')'
puts (a.b.c.d rescue "N/A")
                    ^
(irb):21: syntax error, unexpected ')', expecting $end


貌似是ruby解析器或语法定义的bug,它不认为a.b.c rescue "N/A"整体作为puts的唯一一个参数。
这种用法民间好多称之为 inline rescue。
groovy的机制是叫safe navigation吧,用来特意处理变量为null时的情况,很早以前记得它是这样用的:
a->b->c

现在成了
a?.b?.c


我还是喜欢老的表示方式。

另种形式:
 
a && a.b && a.b.c


0 请登录后投票
   发表时间:2008-03-10  
try或rescue都防不住rails的一个陷阱:nil.id的问题。
假设有这样的代码:
User.find_by_name("sliu").id

当User.find_by_name是nil时,整个表达式并不会引发异常,而是返回怪异的数字4,并附带打印一个warning。
在纯ruby环境中打印的warning信息是:
引用
Object#id will be deprecated; use Object#object_id

在rails环境中打印出的warning信息是:
引用
RuntimeError: Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil,
use object_id


这是因为id方法是在Object中定义的,返回值大致可以理解为对象的内存地址,nil也是一个Object,所以。。。
貌似因为rails把id方法改写成 代表数据库记录的主键 后,ruby语言为了防止二义性就把Object#id deprecated掉了,取而代之的是Object#object_id。而rails多数情况下能接收id的时候也能接收一个ActiveRecord对象,尽可能避免错误的返回4而不是nil。总之因为现在是warning而不是error,所以如果不检查log的话,有可能系统正常运行,却在某些情况下出现诡异的逻辑问题。


0 请登录后投票
   发表时间:2008-03-10  
仔细看了下,rails在处理nil.id时不是输出warning,而是明明白白的RuntimeError,所以

User.find_by_name("sliu").id  rescue nil

因为用了rescue会log出error但依然返回4。


User.find_by_name("sliu").try(:id) 

也不会像预期返回nil,而是报RuntimeError。

现象不同,可都不对啊。
0 请登录后投票
   发表时间:2008-03-10  
liusong1111 写道
仔细看了下,rails在处理nil.id时不是输出warning,而是明明白白的RuntimeError,所以

User.find_by_name("sliu").id  rescue nil

因为用了rescue会log出error但依然返回4。


User.find_by_name("sliu").try(:id) 

也不会像预期返回nil,而是报RuntimeError。

现象不同,可都不对啊。


这是经典的Null Object Pattern问题嘛,貌似在一篇讨论patterns in ruby的BLOG里有见过讨论

try()里可加上先判断自身是否是nil,然后再respond_to?
0 请登录后投票
论坛首页 编程语言技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics