Consider this example:
def foo(bar : Enumerable(String))
pp! bar
end
foo({"baz"})
foo(["baz"])
It works nice, because Tuple
includes Enumerable
.
But this code:
def foo(bar : Enumerable({String, String}))
pp! bar
end
foo({"baz" => "qux"})
foo({"baz": "qux"})
Doesn’t compile with
no overload matches 'foo' with type NamedTuple(baz: String)
Shouldn’t this behavior change? NamedTuple
already has #each
methods and its keys could be considered String
s…
3 Likes
I think I’ve had the same question before…FWIW :)
I’ve wanted this very same behavior. In the Neo4j shard, you pass query params as either a hash or named tuple (to protect against query injection attacks):
query = <<-CYPHER
MATCH (user:User { id: $id })
RETURN user
CYPHER
connection.execute query, id: params["id"]
connection.execute query, { "id" => params["id"] }
The hash is significantly more cumbersome to type, so I’ve added convenience methods to take **params
, convert it to a hash, and then call the Hash
version of the method. Eventually I’ll probably add direct serialization for the NamedTuple
to avoid allocations for the Hash
and each String
key, but I’ll need good benchmarks for that. :-)
The thing is, though, that NamedTuple
s aren’t intended to be just a more convenient hash. They’re more of a special case. Including Enumerable
into it would probably encourage using it in places that don’t make sense, which seems fine until shards aren’t handling Hash
es at all because the authors just work with NamedTuple
s. Then in order to support hashes they have to rework everything to delegate to hashes from NamedTuple
s because you can’t go the other way and we’re right back to where we are now. And that sounds like a lot of effort to spend not to go anywhere. :-)
One thing I find amazing about how Crystal is being designed is that the team have learned a lot from how Ruby’s convenience features are abused in the wild and are trying hard to guide the Crystal ecosystem by showing restraint in the stdlib.