有没有一个内置的Ruby方法可以“重塑”哈希?

3

我有一个从数据库中检索到的哈希值:

original_hash = {
  :name => "Luka",
  :school => {
    :id => "123",
    :name => "Ieperman"
  },
  :testScores => [0.8, 0.5, 0.4, 0.9]
}

我正在编写一个API,并希望向客户端返回一个稍微不同的哈希值:
result = {
  :name => "Luka",
  :schoolName => "Ieperman",
  :averageScore => 0.65
}

无法使用reshape方法,因为该方法不存在。但是它是否以另一个名称存在呢?

result = original_hash.reshape do |hash|
  {
    :name => hash[:name],
    :school => hash[:school][:name],
    :averageScore => hash[:testScores].reduce(:+).to_f / hash[:testScores].count
  }
end

我刚学习Ruby,所以在重写核心类之前想问一下。我相信这种方法肯定存在,因为编写API时,我经常需要重新塑造哈希。或者我完全错过了什么吗?

实现非常简单,但是正如我说的,如果不需要,我不想覆盖Hash:

class Hash
  def reshape
    yield(self)
  end
end

顺便说一下,我知道这个:

result = {
  :name => original_hash[:name],
  :school => original_hash[:school][:name],
  :averageScore => original_hash[:testScores].reduce(:+).to_f / original_hash[:testScores].count
}

但有时我没有一个original_hash变量,而是直接在返回值上操作,或者我在一行代码中,使用块式方法可能更方便。

实际例子:

#get the relevant user settings from the database, and reshape the hash into the form we want
settings = users.find_one({:_id => oid(a[:userID])}, {:emailNotifications => 1, :newsletter => 1, :defaultSocialNetwork => 1}).reshape do |hash|
  {
    :emailNotifications => hash[:emailNotifications] == 1,
    :newsletter => hash[:newsletter] == 1,
    :defaultSocialNetwork => hash[:defaultSocialNetwork]
  }
end rescue fail

1
“tap”是你问题的答案,但更好的问题是为什么?只需将返回值分配给一个变量即可。你试图编写的那种可怕的大量代码行应该被避免,而不是鼓励。仅仅因为你可以将程序转换为一行代码并不意味着你应该这样做。 - user229044
@maeger 我本来就期待有人会说“大型单行代码是邪恶的”之类的话。在我看来(每个人都有自己的看法,这只是我的),只要意图明确,一个大型单行代码有时比多个分开的(更短)代码行和中间变量更容易阅读。想想函数式编程风格的方法链与旧式C风格循环或jQuery选择器链的区别。 - lmirosevic
1
@lms:有一个普遍的误解,认为FP是关于避免变量赋值,这是不正确的。只要你不在原地修改它们,中间变量是使代码可读的好方法。别误会,我完全支持一行代码,但前提是结果可读。话说回来,你用reshape做的...就是正确的方式! :-) intoaschainapply,它有许多名称,但它是一个常见的抽象。http://stackoverflow.com/questions/5010828/something-like-let-in-ruby。https://dev59.com/-2nWa4cB1Zd3GeqP1H6a - tokland
7个回答

4
如果您使用的是 Ruby >= 1.9 版本,可以尝试使用 Object#tapHash#replace 的组合。
def foo(); { foo: "bar" }; end

foo().tap { |h| h.replace({original_foo: h[:foo]}) }
# => { :original_foo => "bar" }

由于 Hash#replace 是原地操作,你可能觉得以下代码更加安全:

foo().clone.tap { |h| h.replace({original_foo: h[:foo]}) }

但这变得有些嘈杂了。我可能会继续对Hash进行猴子补丁。


1
根据OP的说法,似乎他(非常明智地)想要一个非就地操作(FP)。 - tokland
但有时我没有一个original_hash变量,而是直接操作返回值。我理解这意味着在我的例子中我们不关心foo()返回的值——如果我们关心,那么肯定需要一个original_hash变量。 - hdgarrood
更新变量可能会产生意想不到的结果。显然,您可能不关心 foo() 的特定结果,但是这些数据可能会在其他地方使用(而您将更改它们)。这就是命令式编程的烦恼。这只是提醒人们的一个警告,并不是说这很糟糕,只是原地更新是危险的。 - tokland
@tokland 你说得对。我会用更安全的替代方案更新答案。 - hdgarrood

3

从 API 的角度来看,您可能正在寻找一个代表对象,它位于您的内部模型和 API 表示之间(在基于格式的序列化之前)。这不能使用最短、方便的 Ruby 语法内联为哈希而工作,但是这是一种不错的声明性方法。

例如,Grape gem (还有其他 API 框架可用!) 可能解决与以下相同的实际问题:

# In a route
get :user_settings do
  settings = users.find_one({:_id => oid(a[:userID])}, {:emailNotifications => 1, :newsletter => 1, :defaultSocialNetwork => 1})
  present settings, :with => SettingsEntity
end

# Wherever you define your entities:
class SettingsEntity < Grape::Entity
  expose( :emailNotifications ) { |hash,options| hash[:emailNotifications] == 1 }
  expose( :newsletter ) { |hash,options| hash[:newsletter] == 1 }
  expose( :defaultSocialNetwork ) { |hash,options| hash[:defaultSocialNetwork] }
end

这种语法更适合处理ActiveRecord或类似的模型,而不是哈希表。所以这并不是你问题的一个直接答案,但我认为你正在构建一个API。如果你现在加入某种代表层(不一定是grape-entity),以后会感激不已,因为当模型到API数据映射需要更改时,你将能够更好地管理它们。


这很棒。虽然并没有直接回答问题,所以我不能打勾,但是我肯定会在将来使用类似的东西。谢谢你。 - lmirosevic

1
你可以使用内置方法 Object#instance_eval 替换对 "reshape" 的调用,它将完全按照此类方式工作。但是请注意,由于在接收对象的上下文中评估代码(例如,如果使用 "self"),可能会出现一些意外的行为。
result = original_hash.instance_eval do |hash|
  # ...

0

我想不出任何神奇的方法可以做到这一点,因为你实际上想要重新映射一个任意的数据结构。

你可能能够做的是:

require 'pp'

original_hash = {
    :name=>'abc',
    :school => {
        :name=>'school name'
    },
    :testScores => [1,2,3,4,5]
}

result  = {}

original_hash.each {|k,v| v.is_a?(Hash) ? v.each {|k1,v1| result[ [k.to_s, k1.to_s].join('_') ] = v1 } : result[k] = v}

result # {:name=>"abc", "school_name"=>"school name", :testScores=>[1, 2, 3, 4, 5]}

但是这个方法非常混乱,我个人对此感到不满意。手动转换已知键可能更好、更易于维护。


0
如果你把哈希放在一个数组里,你可以使用map函数来转换条目。

0

0

这种抽象在核心中不存在,但人们使用它(使用不同的名称,如pipeintoaspegchain等)。请注意,这个let抽象不仅对哈希有用,所以将其添加到Object类中。

Ruby中是否有`pipe`等效物?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接