NamedTuple isn't Enumerable

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 Strings…

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 NamedTuples 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 Hashes at all because the authors just work with NamedTuples. Then in order to support hashes they have to rework everything to delegate to hashes from NamedTuples 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.