3

I have this hash which I retrieve from a database:

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

I'm writing an API and want to return a slightly different hash to the client:

result = {
  :name => "Luka",
  :schoolName => "Ieperman",
  :averageScore => 0.65
}

This doesn't work because the method reshape doesn't exist. Does it exist by another name though?

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

I'm new to Ruby so thought I'd ask before I go off overriding core classes. I'm sure it must exist as I always find myself reshaping hashes when writing an API. Or am I totally missing something?

The implementation is dead simple but, like I said, I don't want to override Hash if I don't need to:

class Hash
  def reshape
    yield(self)
  end
end

BTW, I know about this:

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

But sometimes I don't have an original_hash variable and instead I'm operating straight off a return value, or I'm inside a one liner where this block based approach would be convenient.

Real World example:

#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
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
lmirosevic
  • 15,787
  • 13
  • 70
  • 116
  • 1
    `tap` is the answer to *your* question, but the better question is **why**? Just assign your return value to a variable. The kind of awful massive lines you're trying to write are to be *avoided*, not encouraged. Just because you can turn your program into a single line of code doesn't mean you should. – user229044 Jun 04 '13 at 13:53
  • @maeger I was expecting someone to say something along the lines of "big one liners are evil". In my opinion (and everyone is entitled to one, this just happens to be mine) as long as intent is clear, a big one liner is sometimes easier to read than multiple separate (shorter) lines with intermediate vars. Think FP style method chaining for stuff like map reduce vs old-school C-style loops, or jQuery selector chaining. – lmirosevic Jun 04 '13 at 14:04
  • 1
    @lms: there is a common misunderstanding that FP is about avoiding variable assignments, that's not true. As far as you don't modify them in-place, intermediate vars are a good way to make code readable. Don't get me wrong, I am all for one-liners, but only when the result is readable. That said, what you did with `reshape`... is the way to go! :-) `into`, `as`, `chain`, `apply`, it has many names, but it's a common abstraction. http://stackoverflow.com/questions/5010828/something-like-let-in-ruby. http://stackoverflow.com/questions/12848967/is-there-a-pipe-equivalent-in-ruby – tokland Jun 04 '13 at 14:32

7 Answers7

4

If you're using Ruby >= 1.9, try a combination of Object#tap and Hash#replace

def foo(); { foo: "bar" }; end

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

Since Hash#replace works in-place, you might find this a bit safer:

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

But this is getting a bit noisy. I'd probably go ahead and monkey-patch Hash at this stage.

hdgarrood
  • 2,141
  • 16
  • 23
  • 1
    that's inplace, from what the OP said it seems he (very wisely) wanted a non-inplace operation (FP). – tokland Jun 04 '13 at 14:37
  • _"But sometimes I don't have an original_hash variable and instead I'm operating straight off a return value"_ I took this to mean that we don't care about the value that `foo()` returns in my example -- if we did, surely we would need an original_hash variable? – hdgarrood Jun 04 '13 at 14:48
  • Updating a variable may have unexpected results. Apparently you may not care about what the particular result of `foo()`, but this data may be in turn used somewhere else (and you'll be changing that). Those are the woes of imperative programming. Just a caveat for people, not that this is bad, simply that in-place updates are dangerous. – tokland Jun 04 '13 at 15:03
  • @tokland you're correct. I'll update the answer with a safer alternative – hdgarrood Jun 04 '13 at 15:12
3

From an API perspective, you may be looking for a representer object to sit between your internal model, and the API representation (prior to format-based serialisation). This doesn't work using the shortest, convenient Ruby syntax inline for a hash, but is a nice declarative approach.

For instance, the Grape gem (other API frameworks are available!) might solve the same real-world problem as:

# 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

This syntax is more geared towards handling ActiveRecord, or similar models, and not hashes though. So not a direct answer to your question, but I think implied by you building up an API. If you put in a representer layer of some kind now (not necessarily grape-entity), you will be thankful for it later, as you'll be better able to manage your model-to-API data mappings when they need to change.

Neil Slater
  • 26,512
  • 6
  • 76
  • 94
  • This is great. Doesn't directly answer the question so I can't give you a tick, but I'll definitely use something like this in the future. Thank you. – lmirosevic Jun 04 '13 at 13:59
1

You can replace the call to "reshape" with the builtin method Object#instance_eval and it will work exactly as such. Note however that there may be some unexpected behavior since you evaluating code in the context of the receiving object (e.g. if using "self").

result = original_hash.instance_eval do |hash|
  # ...
maerics
  • 151,642
  • 46
  • 269
  • 291
0

if you put your hashes in a array you could use the map function to convert the entries

Nikolaus Gradwohl
  • 19,708
  • 3
  • 45
  • 61
0

I can't think of anything that will do this magically, since you're essentially wanting to remap an arbitrary data structure.

Something you may be able to do is:

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]}

but this is incredibly messy and I'd personally be unhappy with it. Performing a manual transform on known keys is probaby better and more maintainable.

mcfinnigan
  • 11,442
  • 35
  • 28
0

Check Facets Hash extensions.

Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
0

This abstraction does not exist in the core but people uses it (with different names, pipe, into, as, peg, chain, ...). Note that this let-abstraction is useful not only for hashes, so add it to the class Object.

Is there a `pipe` equivalent in ruby?

Community
  • 1
  • 1
tokland
  • 66,169
  • 13
  • 144
  • 170