How do I create a nested Hash type?

The following doesn’t compile:

class H < Hash(String, H)
end

p H.new

What’s the use case here? You don’t really need to create your own class as Hash already supports nested types.

class MyClass
  property name : String = "Jim"
end

hash = Hash(String, Hash(Symbol | String, Int32 | Bool | MyClass)).new

hash["foo"] = {:bar => 123, :far => true, "klass" => MyClass.new}

hash # => {"foo" => {:bar => 123, :far => true, "klass" => #<MyClass:0x55e340f73f00 @name="Jim">}}
1 Like

I need it nested to N depth. N is only known at runtime based on the data loaded.
Each Hash at each nested depth has additional properties in addition to the Hash itself.
There are also additional methods I didn’t include in the example that should not exist on the Hash class.

I mean i suppose you could do like:

class CustomHash(K, V) < Hash(String, V)
end

CustomHash{"foo" => "bar"}

Although this would of course include the methods from Hash. I’m assuming its not possible to new up objects instead of using hashes? Might be harder to work with as the types of everything is just going to be a big union.

Another option would be defining a recursive alias, then use that as the type restriction on your own class.

alias MyCustomHash = ...

class MyClass
  def initialize(@hash : MyCustomHash); end
end

But a more OO approach would be better imo.

You can use a recursive alias. But I think you don’t need to use hashes in your program.

Its easier if you tell us what you want to do, what’s your problem (not your solution).

You may also take a look at what’s going on with JSON and YAML, that both give you an arbitrary tree in runtime.

Parsing a sysctl like structure with additional data.

foo.bar.int = 1
foo.bar.str = "foo"
foo.bar.baz.int = 1

I need dig like functions and iteration at any point. A nested set of hashes with additional properties seemed like the closest data structure.

hash.dig["foo", "bar"].each

You can always do something like this: https://play.crystal-lang.org/#/r/74qu

3 Likes

Yes, that’s what JSON::Any#dig does (https://crystal-lang.org/api/0.29.0/JSON/Any.html#dig(key%3AString|Int%2C*subkeys)-instance-method)

The problem may arise if you want a key to have a value and a subtree at the same time:

foo.bar = "value"
foo.bar.baz = "another value"

One way around would be a notion of an empty key, so that becomes something like

foo.bar = Subtree.new
foo.bar.<empty> = "value"
foo.bar.baz = "another value"

Also, an often overlooked way to solve this is to flatten the tree into a single level hash, bash-style:

tree["foo.bar.int"] = 1
tree["foo.bar.str"] = "foo"
tree["foo.bar.baz.int"] = 1

As long as you are fine with the drawbacks it’s perfectly legitimate.

3 Likes

Got it. Thank you.

Example: Carcin

test = Hash(String, Hash(String, Hash(String, String))).new

test["test"] = {"one" => {"one" => "test"}}

pp test

Just replace Hash(String, String) with the last String, dependent on how many you want nested.

This is the first time Didactic.Drunk has posted — let’s welcome them to our community!

By the way, welcome to the community!! :smiley:

In Perl language code, nested Hash is very easily to create and use,like this:

#!/usr/bin/perl -w

my %h;
$h{"A"}{"B"}{"C"} = 1; 
# this is depth=3 netst hash, can use any N depth hash easily in Perl.

So sometimes I also want to use nested Hash Type easily in Crystal.

it do works! thanks~

This works for me:

$ crystal eval --time --progress --error-trace 'h=Hash{"one" => 1, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"];'   
debug: >{"one" => 1, "two" => {"foo" => 3}}< >1< >{"foo" => 3}< >{"foo" => 3}<
Execute: 00:00:00.012389834

But this fails:

$ crystal eval --time --progress --error-trace 'h=Hash{"one" => 1, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"]["foo"];'
error in line 1 (main)                   
Error: undefined method '[]' for Int32 (compile-time type is (Hash(String, Int32) | Int32))

What is the correct syntax to access the nested value of key "foo"?

In Perl this would be $h{two}{foo}.

After some playing I got this to work:

$ crystal eval --time --progress --error-trace 'h=Hash{"one" => 1, "two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s< >%s<\n], h["one"], h["two"], h["two"].as(Hash)["foo"];'
debug: >{"one" => 1, "two" => {"foo" => 3}}< >1< >{"foo" => 3}< >3<
Execute: 00:00:00.010882643

But it looks way less compact and efficient syntax like Perl. Is there a better way?

For your real usecase almost certainly. Giving good advice on such a synthetic example is very hard though unfortunately. See http://xyproblem.info/

Update: I asked in Crystal gitter chat [1] too and got answers which I’m posting here just to capture the info for other newbies:

So in Perl $h{two}{foo} is very compact and not so compact in Crystal in my above example h["two"].as(Hash)["foo"].

However, I learned that if the nested hash has consistent values then Crystal can also do the compact syntax, e.g. h["two"]["foo"]:

$ crystal eval --time --progress --error-trace 'h=Hash{"two" => Hash{"foo" => 3}}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"]["foo"];'
debug: >{"two" => {"foo" => 3}}< >{"foo" => 3}< >3<
Execute: 00:00:00.012247333

Further I discovered that Crystal Named Tuples might be used instead of the second level hash key as an alternative compact syntax h["two"].foo which is probably also faster in performance, e.g.:

$ crystal eval --time --progress --error-trace 'record A, foo : Int32; h=Hash{"two" => A.new(foo: 3), "three" => A.new(foo: 4)}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].foo; p! h;'
debug: >{"two" => A(@foo=3), "three" => A(@foo=4)}< >A(@foo=3)< >3<
h # => {"two" => A(@foo=3), "three" => A(@foo=4)}
Execute: 00:00:00.014652016

However, again, if you mix hash value types, even with named tuples, then the syntax gets fat again h["two"].as(NamedTuple)[:foo], e.g.:

$ crystal eval --time --progress --error-trace 'h=Hash{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}; printf %[debug: >#{h}< >%s< >%s<\n], h["two"], h["two"].as(NamedTuple)[:foo]; p! h;'
debug: >{"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}< >{foo: 3}< >3<
h # => {"two" => {foo: 3}, "three" => {foo: 4, bar: "baz"}, "four" => 1}
Execute: 00:00:00.010379377

[1] https://gitter.im/crystal-lang/crystal