6 ms·
Ruby's hash is a Swiss-army knife
- block_dagger 3y agoRuby’s Hash is probably the handiest data structure I’ve ever encountered. Thanks Matz.
- thibaut_barrere 3y agoDefinitely! As a matter of fact, this is the default data structure I use when writing Ruby ETL code (e.g. https://github.com/thbar/kiba/wiki https://github.com/thbar/kiba/wiki). Methods like "except" (https://docs.ruby-lang.org/en/3.2/Hash.html#method-i-except https://docs.ruby-lang.org/en/3.2/Hash.html#method-i-except) or "fetch" (raising an error on missing key) are very convenient to write defensive data processing code! Similarly, in Elixir, I use Maps a lot for the same type of jobs (https://hexdocs.pm/elixir/1.15.4/Map.html https://hexdocs.pm/elixir/1.15.4/Map.html), with similar properties.
- code_biologist 3y agoKiba looks like a really cool framework, thanks for posting it!
- Gigachad 3y agoI wish it was typed though. So many times I’ve seen a function that takes a hash of “options” or “config” and have no idea what that actually contains. Even for official rails methods it’s often complex to know what the possible options are. Some of them seem almost internal with how obscure they are.
- lloeki 3y agoRBS+steep to the rescue! We typed our configuration this way for ddtrace Ruby. On the external (set) side it makes it very easy to explore configuration in an editor, on the internal (get) side it makes us ensure we don't make mistakes.
- j_crick 3y agoIt should be noted that ** operator works like .merge, and also accounts for the order of key definition in a given hash declaration (whatever is declared earlier gets overwritten if a key with same name is used in the same hash declaration later).
- drbojingle 3y agoPHP's associative array is very similar yea?
- software_writer 3y agoYes, hashes in Ruby, associative arrays in PHP, maps in Go[1], dictionaries in Python[2] and C#[3] represent the same concept, a collection of key-value pairs. [1]: https://gobyexample.com/maps https://gobyexample.com/maps [2]: https://docs.python.org/3/tutorial/datastructures.html#dictionaries https://docs.python.org/3/tutorial/datastructures.html#dicti... [3]: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2?view=net-7.0 https://learn.microsoft.com/en-us/dotnet/api/system.collecti...
- my_new_account0 3y agoYes. The OP's post is basically "baby's first API." The are a very handy API but relying on them at scale is much less efficient than structs or arrays. This post doesn't even go into the different performance characters of these APIs, which can be a major foot gun. I've noticed a lot of novice-level posts on here lately. Although I admit the post was very well written.
- ceautery 3y agoNovice level posts are great. Someone found something exciting to them, not new to us, but we should encourage them to keep exploring and writing.
- schrodinger 3y agoAlso, the votes imply interest!
- dikei 3y agoI find it funny that he said he didn't know what to pick between 5 types of IDictionary in C#. * Hashtable * SortedList * SortedList<TKey, TValue> * Dictionary<TKey, TValue> * ConcurrentDictionary<TKey, TValue> I don't even know C# (just Java), yet I know that you'd probably want to use Dictionary<TKey, TValue> most of the time.
- andolanra 3y agoSince Ruby 3, the automatic coercion of keywords to a hash—the second example underneath "Passing Hash to Functions" in this post—is considered a legacy style and is generally frowned upon in new code. That is to say, code like the second call to `foo` here: def foo(kwargs = {}) kwargs end foo({k: 1}) # ok: passing hash argument foo(k: 1) # ok: keywords coerced to hash One of the strongest arguments for avoiding this sugar is that it makes the code more brittle in the face of future changes. In particular, in the example above, if we add a new keyword argument to `foo`, then any call which omitted the curly braces will break, while calls which used them will keep working fine: # added a new keyword arg here def foo(kwargs = {}, frob: false) kwargs end foo({k: 1}) # still ok: `frob` defaults to false foo(k: 1) # ArgumentError: no keyword: :k This is touched on in the blog post describing the extensive changes made to keywords in Ruby 3: https://www.ruby-lang.org/en/news/2019/12/12/separation-of-positional-and-keyword-arguments-in-ruby-3-0/ https://www.ruby-lang.org/en/news/2019/12/12/separation-of-p...
- Klonoar 3y agoThis might be me having huffed too many types lately, but I feel like I would want that to break.
- mcv 3y agoAdding a parameter that has a sensible default value? I don't think I'd want that to break anything.
- Klonoar 3y agoThe inverse here - specifying {} directly - does not at a glance imply a default value is being constructed. (Or perhaps it does and this is just some Ruby-ism I'm not exposed to)
- andolanra 3y agoThat's kind of the point of default values, though. If you wanted to make a change to the method and you want it to break to alert you to all the calls, then you can add a keyword without adding a default value for it. def foo(kwargs = {}, frob:) kwargs end
- sottol 3y agoIn my opinion Lua's equivalent tables or more of a Swiss army knife. It's been a while since I've used them but afair they combine hash/dict, array and objects in one data structure. They're hashes as usual. With a special constructor or int keys they turn into arrays, last I looked the Lua interpreter even optimizes contiguous ranges of keys as arrays. And by adding metatables and metamethods they also emulate JS-style objects.
- omoikane 3y agoMy feeling with Lua is that it's lacking arrays due to minimalism, as in "we don't need arrays because tables will do". Probably the same reason why there is no "continue" in a for-loop. In comparison, Ruby has arrays and hashes, and gives me the feeling "yes these do exactly what you expected, but wait there is more!" In that sense it does feel more like a Swiss army knife.
- astrobe_ 3y agoOn the contrary, they acknowledged that hash tables only was pushing it a bit too far, so they implemented arrays sort of "on the side" of tables.
- q7xvh97o2pDhNrh 3y agoThis is a lovely overview. Hash is a great example of how delightful it can be to program in Ruby. One more technique worth noting is the chained functional style of using Hash, which you can do in Ruby because Hash inherits from Enumerable. If you're prototyping a script to do some data-cleaning, this makes it easy to build up your pipeline and iterate on it. For example: foobar = { ...your data here... } foobar.map do |k, v| # ... # do some transformation here # ... # and then return the replacement key/value for this entry [key, new_value] end.select do |k, v| # do some filtering here, e.g.: is_foobarable?(k, v) end.map do |k, v| # ... # do some expensive transformation here on the smaller data set # ... [key, newer_value] end.to_h (Note that you have to call #to_h at the end since the Enumerable functions will coerce the hash into an array of arrays.) Now your code literally shows the pipeline that your data is falling through — and each of these steps can be side-effect-free, with no mutations to the original foobar structure.
- my_new_account0 3y agoUnless things have changed and Ruby has stream fusion now, this is bad advice for scale. You are iterating over a fat object multiple times. Even if its uglier its much better in this case to create an empty array/hash, iterate over with #each and #<< to the hash. I worked at the largest Rails shop in the world and this would be rejected in code review. Edited to add more detail: the only method you need to write to implement Enumerable is #each. Every step of your pipeline here is _another_ call to #each. Just do it once.
- lloeki 3y agoThere's #lazy to turn things into a lazy enumerator, to be iterated over when you so desire with e.g #force. If you're going to iterate over an accumulator variable, use the for keyword instead of each, it's faster. Alternatively, one can use .reduce({}) { |h, (k, v)| ... h } or .each.with_object({}) { |(k, v), h| ... } which makes the block not close over an external variable, and makes the assignment to that variable "atomic" (wrt the hash construction, the variable will only contain the final result, that is if a final variable is needed at all, which it may not with implicit return of the last value)
- faitswulff 3y agoMy favorite little-known fact about Ruby hashes is that they respond to `to_proc` and can be used as procs. For example, you can do this: a = { 1 => 'a', 2 => 'b' } [1, 2, 3].map(&a) #=> ['a', 'b', nil]
- software_writer 3y agoI don't quite understand how this code works. Where does the `nil` come from? What operation are we performing on 3 that causes it to return `nil`?
- breckenedge 3y ago1, 2, and 3 are being passed as lookups to the a hash. 3 is undefined on the hash, hence nil.
- software_writer 3y agoAh, that makes sense. Thank you!
- deleted 3y ago[deleted]
- nix-zarathustra 3y agoOne of the most beautiful things in Ruby that I have ever seen is this fibonacci code. fib = Hash.new do |k, v| next 1 if v == 0 || v == 1 k[v-1] + k[v-2] end
- hit8run 3y agoLet’s make it a one liner :D fib = Hash.new {|hash, key| hash[key] = key < 2 ? key : hash[key-1] + hash[key-2] } Example: fib[123] # => 22698374052006863956975682 Makes use of memoization.
- nix-zarathustra 3y ago
- gilfoyle 3y agoAlso that it maintains insertion order (since Ruby 1.9) opens up more use cases.
- software_writer 3y agoDo you know any use cases, other than iteration? Would love to know any interesting ones. Thanks!
- byroot 3y agoYeah, you can use it to implement a simple (but not necessarily very performant) LRU: https://github.com/SamSaffron/lru_redux/blob/037ee594aded59764487d4a1a5b4077fa710ae3b/lib/lru_redux/cache.rb https://github.com/SamSaffron/lru_redux/blob/037ee594aded597...
- software_writer 3y agoVery cool, thanks for sharing!
- irjustin 3y agoI've been in Python/Django for about a year now and I really miss Ruby's hash vs the dict. `.dig(:key, :key, :etc)` is so nice to find deeply nested data without blowing up. One thing I don't miss is knowing whether a hash's keys are strings vs symbols. While it's easily solvable, I've definitely lost time only to smack myself that I need to use a str but was always feeding a sym and swore that this should be a sym based hash.
- code_biologist 3y agoI don't know ruby and if it's the same, but if you're not using a library like funcy or toolz for a nested get helper, you can do `dict.get('key', {}).get('key2', {}).get('key3')`. Not the prettiest, but can do in a pinch.
- irjustin 3y agoThanks! i didn't know about these and will check them out for sure. Tired of `if key in hash:` nested layers.
- nicolaslem 3y agoIn my opinion, a better alternative to nested `if key in hash`: try: value = data["foo"]["bar"]["baz"] except KeyError: value = None
- irjustin 3y agoI know it's been almost a year but I still haven't accepted this pattern. Using errors as general flow control makes me uncomfortable. It shouldn't be an error or exception except in.... Actual problems.
- defanor 3y agoIt is just a hash map with a few common functions defined; hash maps are occasionally useful, but what is all the praise about? The mentioned "simple" bit is arguable: as a language's building block, a hash map is relatively complex and specific, since those can be built out of lists (or trees, though lists/arrays may be preferable for efficiency), which can be built out of pairs (product types, cons cells, tuples; unless going for that efficiency, though it can still be pretty efficient with trees). Maybe it is one of those "simple versus easy" mix-ups.
- rob74 3y agoWell yeah, it's "simple" (or easy) to use, definitely not simple to implement. But having an easy-to-use hashmap is par for the course for most newer languages - not only Ruby, but also PHP (associative arrays), JS (objects), Go (built-in map type) etc.
- lloeki 3y agoFor me the most useful bit about #fetch is not that it throws, it's that it allows setting a default value while distinguishing between a missing key and a key with nil as a value. If I want to handle key presence without a default I just use #key? as exceptions come with additional performance cost (building the stacktrace)
- blarnopre 3y agoThe only thing I really miss in the default Ruby hash is property access for keys like JavaScript has. I know, I know, OStruct does this but it's not the thing that gets built from hash literals so it's less convenient. It's a minor quibble
- lloeki 3y agoOpenStructs are ungodly slow anyway. If you want such accessors you probably want a plain Struct anyway and not mix hashness and method accessors. You can even define additional methods on structs: Foo = Struct.new(:a, :b, keyword_init: true) do def frobz ... end end Structs are quicker to instantiate than classes but slower to call methods on, so if you instantiate a lot but seldom call they're great. Otherwise a class is better.
- ljm 3y agoThe new Data class in Ruby 3.2 also does the job (although it's intended for immutable data) https://docs.ruby-lang.org/en/3.2/Data.html https://docs.ruby-lang.org/en/3.2/Data.html
- thriftwy 3y agoReminds me of https://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html https://steve-yegge.blogspot.com/2008/10/universal-design-pa... with its observation that since hash table is O(1), it may mock almost any other data structure.
- Draiken 3y agoHash is so powerful in Ruby that people often overuse them. One of the most common issues I found on Ruby code-bases is to not create classes to represent their domain and simply use hashes everywhere. The downside is that a hash has no shape. It can (and will) be anything you want it to be, often causing havoc once the system grows. Checks for keys everywhere. Almost all statements use the safe navigation because you never know what shape you're dealing with. Multiple places performing the same map/reduce/filter/etc. All because people stick to hashes a bit too long.
- cies 3y agoRuby newly added type system can also help here. For starters, it'd be nice to know what type(s) the keys and values can be.
- Draiken 3y agoThat's what POROs are there for. I truly don't want a faux type system only to then keep using hashes for everything...
- arooaroo 3y agoAmen. This is an issue at the company I work at. Common typos when looking up has keys will return nil - this has a tendency to silently keep working and blow up with a runtime error further down the chain. I am trying to insist on using .fetch to force an exception. When the company switches to 3.2 I will insist on everyone using the new Data class for value objects rather than hashes.
- mcv 3y agoThis is perhaps the biggest killer feature of typescript: that your object literals (which are basically hashes) can have types. Or possibly interfaces.
- tinnet 3y agoWhile is share the praise for Ruby's hash I find the argument that it might be daunting for a developer (of any persuasion) to pick between a `SortedList` and `Dictionary` worrying.
- mcv 3y ago> as a programmer building run-of-the-mill CRUD web applications, having so many choices can be really daunting and confusing. When do you choose which type? I feel similarly after getting back into Java after years of Javascript. Java has a ridiculous number of arrays and lists: Object[], List, Iterator, Iterable, Stream, and I think I'm forgetting a few. Some functions want or return one, some want or return another. I'm constantly convertings these from one to the other, and it's never pretty. (What the hell is a Spliterator?!) Meanwhile, in Javascript, there's just the Array. (It's not even really an array, but a hashmap that only takes integers as index, but nobody cares; it works.) And, like Ruby, Javascript also has easy object literals that is basically the exact same as the hash in Ruby. If there are any meaningful differences, I'd love for someone to enlighten me. The only difference I'm aware of, is that the preferred type for keys in Ruby is the Symbol, which is a very lightweight string, whereas in JS it's just a regular string. But, like the author, I strongly prefer to work in a language that just has a couple of sensible data types instead of making me choose from thousands that have no meaningful differences beyond their incompatibility.