#1076 Map.trap

brian Wed 14 Apr 2010

There was a fairly long discussion a while back #848 on maybe replacing Context with a Dict class. We decided to just remove Context and just use Str:Obj.

However, the issue of "anonymous objects" or records of String name/Obj value pairs is something that I spent a great deal of time working with when slinging around data.

The biggest issue I have right now is that it would be really nice to use -> to access a named value and get a meaningful error such as "The key foo is not found" rather than just a NullErr if the key is missing. Andy and I talked about this at length today, and wish to propose a change to Map where trap performs a key lookup instead of a reflection call on Map. Of all the solutions this seems to be the simplest rule to remember, and enables Maps to be used easily as anonymous objects:

rec := ["name": "Brian"]
echo(rec->name)
ehco(rec->foo) // throws NameErr instead of returning null

The only downside is that you can't use any duck typing with Map. This is actually something I occasionally do for things like each or size. But compared to how often I use Map as an anonymous object, I think I can live without it.

Any votes against this change or better ideas?

Another thing I'd love to do is avoid use of quoting the keys in map literals:

rec := ["name": "Brian"]
rec := [name: "Brian"]

However, that gets ugly if name is also a local variable or method on the enclosing class. So I don't think we can really do it without some special syntax annotation on the map syntax which I don't really like either (think I'd rather just live with quoting the keys).

tactics Thu 15 Apr 2010

I could go either way on the trap method change.

I'd rather not change the keys in map literals. It's already confusing enough trying to tell locals from this slots from it slots. Let's not add even more rules for figuring out what an identifier means in a code context.

jodastephen Thu 15 Apr 2010

Every special rule that is added is something to remember. This one seems too magical.

map->size             // I'd expect this to be reflective lookup

Most developers will learn that -> is a reflective lookup, rather than learning it calls trap which may do anything. Thus, from everything else in Fan, its obvious what this should do. The proposal would change that, which I see as unhelpful.

One simple option is to allow the trap target to be a quoted string:

map->size             // reflective trap()
map->"name"           // get()
map->"name" = ...     // put()

A second option is an new operator, such as:

map->size             // reflective trap()
map[->]name           // get()
map[->]name = ...     // put()

or

map->size             // reflective trap()
map[]name             // get()
map[]name = ...       // put()

A third option is the Dyn/'Dict' class, but I'd suggest easy conversion to and from a map:

d := Dyn()
d.map->size           // reflective trap()
d.map["name"]         // get()
d->name               // get()

m := Str:Obj[:]
m->size               // reflective trap()
m["name"]             // get()
m.dyn->name           // get()

In reality, if developers choose the right type, then they would rarely use the map() or dyn() conversions.

.

On the second issue, please don't make map keys clever just because they are a string. It confuses the namespace.

I believe that this is partly a problem caused by using strings for keys - if I was coding I'd want to use enums wherever possible for the extra type safety (bigger enterprise development needs that extra safety).

Looked at in that light, whatever is supported for strings should also be supported for enums as keys. Which rules out the first option above.

More generally, Fantom is still not making best use of enums. In most locations, the enum type can be inferred. Doing so, would simplify many cases like this:

map := Weekday:Str[:]
map[Weekday.mon] = "MN"
map[Weekday.tue] = "TD"
map.contains(Weekday.wed)

// with inference:
map := Weekday:Str[:]
map[mon] = "MN"
map[tue] = "TD"
map.contains(wed)

With this inference and proper use of enums, the issue of map keys needing to have quotes disappears.

Finally, I'd note that for actors, I've already said that there should be an Actor mixin like Enum that allows the developer to define "local variables", but it compiles as a map of locals.

ivan Thu 15 Apr 2010

I really like the idea with map.dyn and dyn.map from the post above, but there are still some unclear moments, for example:

personMap := [ "name" : "Ivan",
               "address" : ["city" : Novosibirsk]
             ]
personDyn := personMap.dyn
personDyn->address //Is this Map or Dyn? Can I call personDyn->address->city?

Another moment - immutability, I assume that dyn.toImmutable should return immutable dyn, and dyn.map/'map.dyn' should return projections with same immutability state.

So I think another option would be to say that -> is always call to trap and have a special syntax for reflective calls, like map#size.

brian Thu 15 Apr 2010

All great comments. Let me take it from another perspective...

I think it is important that we easily support the concept of anonymous or dynamic objects. These are essentially name/value maps. In fact they are so close to the existing Map class that there is really only two problems:

  • you can't use dynamic call on them
  • a little awkward syntax for the names being quoted

I agree we should keep the keys quoted in map literals, it would be too confusing otherwise. I bring up the point to illustrate the two main differences so that we can evaluate trade-offs in various solutions.

The most basic two options are:

  1. make Map do the duty, only issue is how to use dynamic call
  2. create new Dict, Dyn, Anom class that does the duty (but shares virtually the same APIs as map)

Option 1 has a couple different paths:

  • 1.A: just swizzle -> to be a key lookup instead of reflection call (my proposal)
  • 1.B: make -> behavior configurable; problem is how to this without adding even more confusion (so I don't think this is viable)
  • 1.C: add yet another operator

Option 2 is to embrace a new first class collection which will require a new literal syntax for the language.

So it sort of boils down to this for me: is making -> on Map a anomoly to the standard rules justified versus the complexity of adding a new operator or literal syntax to the language?

qualidafial Thu 15 Apr 2010

Option 3: Make the Dyn class a wrapper over the Map, but with none of Map's API, eliminating any confusion over slot resolution in the trap method.

brian Thu 15 Apr 2010

Option 3: Make the Dyn class a wrapper over the Map, but with none of Map's API, eliminating any confusion over slot resolution in the trap method.

That is what we do today, and it really sucks because all your map methods like findAll return Map which you then have to convert back to a Dict. So I think we would end up creating convenience methods to mimic the Map API:

// to Map and back to Dict ...
Dict(["a":1, "b":2]).map.findAll(|v| { v.isOdd }).toDict

// versus duplicating map API into Dict
Dict(["a":1, "b":2]).findAll(|v| { v.isOdd })

// versus just using map
["a":1, "b":2].findAll(|v| { v.isOdd })

qualidafial Thu 15 Apr 2010

I see now that we're talking about Dyn / Dict for two different purposes. Dict is clearly a data structure, whereas Dyn is more about object prototyping. My earlier comments were geared toward the Dyn case and so are not really relevant to this discussion.

brian Wed 21 Apr 2010

Just an update, Andy and I have beat this horse to death trying to figure out the best way to tackle it either in Fantom or elegantly as a library. No suitable solution has emerged, so we're just going to punt on it for now. But the notion of a anonymous or dynamic object is something I think we really need to add to Fantom eventually.

DanielFath Wed 21 Apr 2010

I'm probably stating the obvious, but have you guys considered making Dict : Map ?

It solves nearly all conversion problems and reduces the need for toMap method.

brian Wed 21 Apr 2010

I'm probably stating the obvious, but have you guys considered making Dict : Map ?

yes, but it introduces all sorts of really complicated problems:

  • subclassing of a generic
  • opening up the implementation of map (but still providing a concrete implementation for map literals)

DanielFath Wed 21 Apr 2010

* subclassing of a generic

Why is this a problem? Can't you lock the subclassing by making the constuctor private or internal? Or are there other problems with subclassing generic.

Speaking of, being a Java developer I do miss the ability to specify or extend implementation of Map, List, etc. What Map implementation did you guys make? Will it be optimal in most cases if not all?

Login or Signup to reply.