Hash and type restriction on extracted sub-hash

Given the following hash:

h = {"abc" => {32 => "xyz"}, 37 => :hello}

if I extract the value of key "abc", I get:

h["abc"] # => {32 => "xyz"}
typeof(h["abc"]) # => (Hash(Int32, String) | Symbol)

I’d like to convert h["abc"] type to :

Hash(Int32, String)

which is the type of {32 => "xyz"}

I’m sure it’s obvious, but I just can’t do it!

You can straight up convert it using h["abc"].as(String), but I’d suggest using a better data type instead, like a Tuple or a record.

h["abc"].as(String) ends in an error

Error: can’t cast (Hash(Int32, String) | Symbol) to String

I could, of course, use the syntax

h["abc"].as(Hash(Int32, String))

in this example, but that presupposes that I know the exact contents of the hash.

I’m actually looking for a way to obtain a new hash, the type of which is based solely on its content, and not on its origin.

But perhaps this isn’t possible with the current Hash API and a low-level approach involving a few magic tricks would be in order?

I think so.

Ok, then something like

case v = h["abc"]
when Hash
  # do something hashy with v
else
  # do something else
end

could perhaps work.

But really, the less you know about what the content is, the less you can do with it.

It could maybe help to give some context why you end up with a nested hash of unknown structure and then want to import it into strict types in the first place.

Not easy to explain!
The hash is used to parameterize the operation of an application and is in the user’s hands.
The hash structure is not really unknown, but can be more or less complete, depending on the user’s use.

The original idea was to facilitate programming by taking into account only the values provided, but this was probably a bad idea. I’ve therefore abandoned it and considered taking into account a hash containing all possible data.
Who can do more can do less!

Hm, how does the user configuration get into the hash, though?

In fact, the application in question is a library, still in the development phase, designed for Crystal developers to format tables in Terminal mode (a primitive version of Tablo was released in 2021).

To use this library, the developer builds and fills the hash as part of his own application.

To come back to the original question, adding a method (Hash.from ?) meeting my initial wish would not, in my opinion, be uninteresting.
So, in the example given:

h.from("abc") # => {32 => "xyz"}  
typeof(h.from("abc")) # => Hash(Int32, String)
h.from(37) # => :hello
typeof(h.from(37)) # => Symbol

What do you think?

1 Like

It seems like a configuration class could be a much better option for this… Bringing type-safety, easy discoverability and documentation.

Nested hashes are a bit frowned upon in Crystal (and I guess in any statially typed langauge). Especially the extensive use for configuration as in Ruby doesn’t translate well. It’s usually much better to define a type or configuration.

Hence I don’t see much reason for Hash#from.

2 Likes

Much better, indeed !
Thanks for the advice.