Fantom

Login | Register

Ordered Map litteral

tcolar
14 Mar 2012

Is there a way to define a map litteral (not empty) that is ordered.

say:

Str:Str map := ["a":"1", "b":"2", "c":"3", ...... ]

Since "ordered=true" can only be applied to an empty map and I can't think of a way to apply that "ordered=true" before hand (it block would do it after) ... then I have no choice but to do something like:

Str:Str map := [,] {ordered=true}
// then add all the items one at a time
....

Or am I missing something ? I have a feeling I am :)

brian
14 Mar 2012

You are correct, you haven't use a literal for creating an ordered map. Simplest code to achieve that might be something like:

[:] { ordered = true }.addAll([...])

tcolar
14 Mar 2012

Well I actually had tried that but that doesn't help either because the Litteral you pass to addAll is itself not sorted either

KevinKelley
14 Mar 2012

Hey, I remember reading it that way too -- that "ordered" might mean "sorted" which would have to mean that the map was based on a tree-map, rather than hash-based...

But the docs says explicitly that it's a hash-map, so I guess that "insertion order" is about all that ordered could mean.

I think you're probably wishing for something like a red-black tree, something that maintains order and still gives fast insert/delete. Some time back Ivan put out a Fantom collections package, which I'm pretty sure has TreeMap in it.

@Ivan, by the way, thank you for that.

tcolar
15 Mar 2012

Thanks, but in this case i was not looking for it to be sorted, just to keep the order of insertion.

ivan
15 Mar 2012

I wanted something like this long time ago. The compromise I could find was like this:

fansh> class Foo {
> static Obj:Obj? orderedMap(Obj?[][] list) 
> {
>   result := [:] { ordered = true }
>   list.each { result[it.first] = it.last }
>   return result
> }
> }
sh17::Foo

fansh> Foo.orderedMap(
> [
>   ["a", "b"],
>   ["c", "d"]
> ])
[a:b, c:d]

fansh>   

ivan
15 Mar 2012

Thanks, Kevin!

Now this package in fact is moved under xored account here – https://bitbucket.org/xored/collections

Mostly I use it now for json objects:

fansh>  constJson := Json.makeWith {
> it->foo = [1,2,3,5]
> it->bar = ["a": 43]
> }
{
  "foo": [1,2,3,5],
  "bar": {
    "a": 43
  }
}

fansh> persistentCopy := constJson.mutate {
> it->bar->a = 42
> }
{
  "foo": [1,2,3,5],
  "bar": {
    "a": 42
  }
}
fansh> 

KevinKelley
15 Mar 2012

I need to use that stuff more -- const everywhere, and the persistent types where modified instances share memory.

Actors are great, and make for solid code. But they've got that "pervasiveness" thing going -- where everything they touch ends up needing to deal with constness. So if you weren't thinking about that up front, then you have issues like lots of naive copying. Since I often bang out a quick test implementation of something, just to get it clear in my head what I'm trying to do, I often find myself coming back and reworking everything to be const-safe.

It's a lot nicer to have good cheap persistent types in the first place.

Ivan, I was just looking again at your multiple dispatch example -- https://bitbucket.org/ivan_inozemtsev/mdispatch. (There's some neat stuff I'm playing around with, PEG parsers like in OMeta and in vpri's Ian Piumarta's Maru, that wants multiple-dispatch). Anyway I don't know enough about how MD ought to work... the mdispatch DSL's call implementation seems to just take the first-matching-definition. Is that a useful scheme, or is it just that the example is intended to be about DSLs, not MD?

I'm trying to get my head around the "best-fit" idea: if you've got defined a draw(shape, canvas), draw(circle, printer), draw(ellipse, screen)... what should the algorithm look like, to find and call the most-specific method.

  • object
    • shape
      • circle
        • ellipse
      • square...
    • canvas
      • printer
      • screen

I guess you'd probably want to do the "exclude methods that don't fit actual-args" as in the DSL example, and then do a left-to-right selection of the most-specific matching method...

Anyway, not really a question, just thinking out loud about this interesting code!

brian
15 Mar 2012

Well if anyone has a way we could improve the situation, suggestions welcome. I've often wanted to use ordered/case insensitive with literals myself.

tcolar
15 Mar 2012

Maybe a facet might help (for literals like list/map):

@Ordered Str:Str map := ["a":"1"]

Then the compiler could make it "ordered" right at construction time before adding the values

Maybe the facet could be more generic

@LiteralOption(ordered=true) // to set that option on the object at construction before setting the values
Str:Str map := ["a":"1"]

Or something among those lines ... can't really think of much else.

jstaudemeyer
15 Mar 2012

The simplest solution is to let the map literal always generate something like the Java LinkedHashMap. Groovy went that way, it was a nonbreaking change that didn't harm anybody but proved to be very useful.

StephenViles
15 Mar 2012

+1 for jstaudemeyer's idea: maps instantiated by a literal are ordered.

DanielFath
15 Mar 2012

I think what people would like most (at least people that I've talked to on other blogs) is that Map could easily be replaceable with a different implementation. In other words ideally you could choose or plug in different Map types depending on the need. It's probably too broad but I think Map litteral syntax should be just a syntax sugar over a Map class or a mixin, which you could extend and changed as you wish. It's probably way too broad for this particular solution, but it should be one solution we strive for.

dobesv
16 Mar 2012

jstaudemeyer and StephenViles seem to be on to something - the map is separated by commas, it might be intuitive that it would be ordered by default. People are already accustomed to this in javascript and apparently Groovy. For the few performance hotspots where using an ordered map is a problem, we could have a way to make an unordered map, for example by using the less friendly syntax Map().add().add().add()

Another approach that comes to mind is to have an it-block like approach where you can put a map literal after something and it adds the keys and values into the map in a standard way. Perhaps

MyMap() ["foo":"bar", "baz":"quux"]
// =>
MyMap().add("foo","bar").add("baz", "quux")
// or
Map() { ordered = true } ["foo":"bar", "baz":"quux"]
// =>
Map() { ordered = true }.add("foo","bar").add("baz", "quux")

This would work for lists as well:

LinkedList() ["foo", "bar", "buzz"]
// =>
LinkedList().add("foo").add("bar").add("buzz") }

This could also support a syntax where you choose a different method to call besides add, for example:

MyQueue.enqueue ["foo", "bar", "baz"]
// =>
MyQueue.enqueue(foo).enqueue(bar).enqueue(baz)

Thoughts?

Akcelisto
16 Mar 2012

I think we may extend current conventions. Let's that custom classes use ":"-syntax as class Map like custom classes use ","-syntax as class List.

OrderedMap{"a":"1", "b":"2", "c":"3", ......}

We already have

Class definition:

@Serializable { collection = true }
class Person
{
  Void add(Person kid) { kids.add(kid) }
  Void each(|Person kid| f) { kids.each(f) }
  Str name
  @transient private Person[] kids := Person[,]
}

Creating and serialization:

Person
{
  name = "Homer Simson"
  Person { name = "Bart" },
  Person { name = "Lisa" },
  Person { name = "Maggie" },
}

Compiles to:

Person
{
  name = "Homer Simson"
  this.add(Person { name = "Bart" })
  this.add(Person { name = "Lisa" })
  this.add(Person { name = "Maggie" })
}

I suggest

Class definition:

@Serializable { map = true }
class Person
{
  Void set(Str key, Person kid) { kids[key]=kid }
  Void each(|Str key, Person kid| f) { kids.each(f) }
  Str name
  @transient private Str:Person kids := Str:Person[:]
}

Creating and serialization:

Person
{
  name = "Homer Simson"
  "key1":Person { name = "Bart" },
  "key2":Person { name = "Lisa" },
  "key3":Person { name = "Maggie" }
}

Compiles to:

Person
{
  name = "Homer Simson"
  this["key1"] = Person { name = "Bart" })
  this["key1"] = Person { name = "Lisa" })
  this["key1"] = Person { name = "Maggie" })
}

brian
16 Mar 2012

I've actually been thinking along lines of @Akcelisto myself. One thing I did our product's scripting language was use just a simple colon instead of ":=" to declare new variables and I've found a prefer that, so been thinking of myself if/how just colon gets used in Fantom, since we don't have labels like Java its actually a free operator.

The other benefit of maybe using colon is that in that case we could say that identifier keys are treated as strings:

Str:Int[:] { one: 1, two: 2, three: 3 }

Akcelisto
16 Mar 2012

Str:Int[:] { one: 1, two: 2, three: 3 }

But this may clash with named params of methods. Or named params is not important?

KevinKelley
16 Mar 2012

Add named params workalike with

sys::Func.callMap({one: 1, two: 2, three: 3})

Get rid of <map> literal from grammar; add

<def>  ::= <id> ":" <expr>

with <def> appearing somewhere in <expr> tree,

then use it-block syntax to construct a map...

andy
16 Mar 2012

{ one: 1, two: 2, three: 3 }

I think about this more as the anonymous object syntax. Maybe just using Maps is our poor mans implementation for anonymous objects. But then we lose out on the consumption side (which is really what we want it for):

obj := { msg:"Foo", result:12 }

echo("$obj.msg: $obj.result")             // this is alot nicer...
echo(obj["foo"] + ": " + obj["result"])   // ...than this

This probably remains my #1 feature missing from Fantom.

dobesv
18 Mar 2012

Ah I didn't know about that existing syntax for calling add() implicitly. Extending that syntax to support a map version seems like a good idea.

dobesv
18 Mar 2012

andy: I wonder if Map could override trap() so that you can do:

obj := [ "msg":"Foo", "result":12 ]

echo("$obj->msg: $obj->result")             // this is alot nicer...
echo(obj["foo"] + ": " + obj["result"])   // ...than this

Not as big a saving in characters as your version, but a smaller delta from what's already there.

andy
19 Mar 2012

I wonder if Map could override trap()

We do that for a similar construct in our commercial software. Main reason we haven't added it to Map, is because it conflicts with reflection: map.size vs map["size"]

Login or Register to Reply

Back | All Topics