HomeUser Control Panel (unavailable in archive)ForumsTutorialsArt GalleryResourcesMaps

[script] Dictionary

11-25-2009, 04:19 AM#1
weaaddar
Updated::
Dictionary is an associative array, like Vexorian's table. Unlike Table however, Dictionary keeps track of elements stored in it and provides a means to iterate through the collection. This is basically a linked list and a hashtable combined. There is a readonly array struct that will inline, and doesn't allow mutation.
The macro is instanced currently for 3 types
Dict_integer
Dict_string
Dict_agent
Thankfully it is a macro so you can instance your own. just at the bottom of Dictionary library instance to your hearts content. The macro takes 3 parameters
//! runtextmacro DictTemplate (T,HashFunc,DefaultVal)
where T is the type, HashFunc is a function to generate a hash, and defaultVal is the default for the type.

Another included type is Set. Set is like a unitgroup but for any type T. Set provides a readonly node as well, and a fast contain operator. It too is all constant time operations except for its clear op.
More text in the script header.
Expand Zinc:

Here is fibonacci. You can try running the Fib method which has exponential runtime, and quickly blow out your op limit, or you can use the faster MemoizedFib which can compute any value in a worse case of o(n).
Also demonstrated is how to enumerate through the values of the dictionary. Using the for loop feature of zinc. Its slightly miserable and a bit long, but it will compile cleanly to some nice jass.
You'll note that you don't have to destroy the entry at the end, you are done as it is an array struct and isn't "instanced".

Alternatively, a slower fashion of generating a loop through all entries is the forEach method. You can use it for debug purposes or whatever. Its not terribly less code to use the callback, but maybe you can use it do something interesting.
Expand Zinc:

UPDATED again::
This time I switched Dict_handle/Set_handle->Dict_agent/Set_agent as the fogstate return bug is unsupported and is the only way to make handles useful. Agent typecasting while still a grey arey is more supported.
11-27-2009, 02:36 AM#2
weaaddar
I also quickly wrote an example HashSet for demonstration purposes to show that its easy to wrap a hashset around it.
Collapse Zinc:
//! zinc
library HashSet requires Dictionary
{
    //! textmacro HashSetTemplate takes T
    public struct Set_$T$
    {
        Dict_$T$ m_dict;
        
        method operator Count()->integer
        {
            return m_dict.Count;
        }
        
        method Add($T$ object)->boolean
        {
            return m_dict.Add(object,-1);
        }
        
        method Remove($T$ object)->boolean
        {
            return m_dict.Remove(object) == -1;
        }
        
        method Contains($T$ object)->boolean
        {
            return m_dict.Contains(object);
        }
        
        method Clear()
        {
            m_dict.Clear();
        }
        
        private method onDestroy()
        {
            m_dict.destroy();
        }
        
        method GetEnumerator()->DictEnumerator_$T$
        {
            return m_dict.GetEnumerator();
        }

        static method create()->thistype
        {
            thistype s = thistype.allocate();
            s.m_dict = m_dict.create();
            return s;
        }
    }
        //! endtextmacro        
        //! runtextmacro HashSetTemplate("integer")
        //! runtextmacro HashSetTemplate("handle")
        //! runtextmacro HashSetTemplate("string")
}
//! endzinc
A hashset can basically be used like a group but handle types that don't support it. Like Triggers or Effects. The dictionary can be thought of as a group with a built in struct attach system.
11-29-2009, 12:18 AM#3
grim001
  • Documentation is bad, it needs to be above the code, not inside of it.
  • The documentation needs to be more verbose and understandable, even for people who may not be familiar with other programming languages.
  • Don't capitalize the first word in method name, seriously.
  • The Add method is really redundant except for technical details that are none of the user's concern.
  • TryGetValue is really useless with the way it's implemented; why not just use a Contains check first?
  • Do you want Collections graveyarded since this seems pretty similar?
11-29-2009, 04:39 AM#4
weaaddar
1. I can move the documentation up. That's not a concern. It was simply easier to write the documentation above the code in my Ide, as I could peer down a little bit.
2. I'm not sure what you'd like. I'm not going to document Node, as you can't create a node. I won't document constructors as Node or Enumerator's constructor can't be called by the end user. And the constructor for dictionary doesn't do anything.
3. I really don't like camel case starting with a lower case method names.I program in C# as my day job and the convention is capital method names. :/ I can try to fix that, but I really hate it.
4. The add method is not redundant, as it reports its insertion or failure. I can see a possible usage in something like::
Expand Zinc:
5. TryGetValue is not useless, it is the correct way to fetch a value when 0 is a possible result. If 0 isn't a result then its not a concern, but sometimes 0 can be an acceptable answer and so you want to test first. Without it you would have to use a contains and then an index fetch. Which is 2 ht load calls vs the 1 ht load call of TryGetValue. We don't have out parameters in Jass, and so this seems like a pretty nifty way to emulate the result. As the Add method stores a Node which is an integer type, so if you got back a node of 0 (nil) there is nothing stored there, and value gets set to 0 as TryGetValue failed.

6. I'd rather it wasn't. I can't really think of any improvements, but this and collections (which probably will just be renamed List) are going to be used in my fixed version of ItemMenu as I want to change the ItemMenus list on the hero to a hashset, and add a Hero's dictionary which will contain all the heroes instantiated as a unit-struct key-value collection.

My question: Is the convention for properties lower case letters too? Because that is ugly. I really do not like the idea of seeing
Dict_integer myDict = Dict_integer.create();
return myDict.count;
That's horrible :/

Ps: Grim you're the guy who made me paranoid about HT calls so part of the goal of this dictionary is to avoid excess HT calls. I'm actually thinking about make a private insert method, so that Add and operator[]= can call that internally so as to avoid the double fetch that []= does when it doesn't find a node.
11-29-2009, 10:40 AM#5
Anitarf
5. You could use some obscure integer that nobody will ever use instead of 0 as the empty return.
11-29-2009, 12:49 PM#6
weaaddar
That's true, but [] currently inlines to the hashtable native, and 0 is a better return value imo. As this should be used mostly wtih structs, where 0 is nil. If you aren't using structs then that's why TryGetValue exists.
11-29-2009, 02:35 PM#7
weaaddar
Actually Grim, I just realized you probably need a typecast library to use this with handle types as the Node_handle isn't upcastable without it. So its probably best to make it instancable. If only there was some way to pass what Node type to use in the dictionary, then you'd only need one copy of it (you'd still have a generic handle taking key method like [], but when it compiled it would error out on you if you tried to use something like trigger instead of unit).
12-05-2009, 04:18 AM#8
weaaddar
I've updated the code and the documentation. Its more efficient now.

I've included HashSet as its basically free. If there is desire I can probably throw in a switch in the macro so that you only have to instance the type you want.

I've removed the enumerators as it seems silly to create a heavy object, when you want a light-safe way to enumerate a collection. The replacement is the RO_Dict/SetNode_T types. The enumeration pattern is described in the first post and displayed in my fib library.
12-09-2009, 11:40 PM#9
weaaddar
I've updated once more, and removed trygetvalue testing showed it was slower then if(contains) and another fetch.

Is there any other criticism of the library? It performs nearly as fast as table for retrievable, it enumerates as quick as a linked list, updating or removing elements is lightening fast. The only slow point is the add method which still is reasonable. 1000 elements added in 20 ms, vs the about 11 seconds of a linked list.
12-10-2009, 04:14 AM#10
grim001
  • It is a little disorienting to not have the library name at the very top of the code.
  • You need to mention somewhere in your documentation that "T" is referring to the type, and that integer/handle/string are available by default.
  • There's a typo in the description of operator[T key], "reutrns"
  • Maybe "Add" would be more clear if it were named "TryAddValue," it matches well with "TryGetValue"
  • RO_DictNode_T and RO_SetNode_T are kind of ugly names, possibly difficult for the user to remember. Can you think of something more eloquent for those?
  • Wouldn't it be simpler to rename Object in RO_SetNode_T to Value, so that it shares the same syntax?
  • Aren't you missing a .GetReadOnlyNode() call in the vjass enumeration example?
  • Can you include a simpler method of enumeration (such as a callback that takes the value) for when speed isn't a concern or for debug purposes?

Anyway, you have done a lot of work on this, it is looking pretty useful now.
12-12-2009, 04:52 AM#11
weaaddar
1,2,3 fixable.
TryGetValue is gone because it was an awful performer (function space is slower then 2 natives in Jass).
Add is prety standard naming convention, I'd like Set and Dict to share the same name.

RO_DictNode_T is an awful name I agree, But I can't think of a better one. ReadOnlyDictNodeT is really long. RODN_T/ROSN_T is shorter but pretty bad too. DictNode_T and SetNode_T might be better.

Value is of type integer always, where as Object is of type T. I don't want to cause confusion.

Yes, I am missing the call. Good catch!
I'll fix the documentation. I'll probably introduce a ForEach(func) method, and an enumerator type (usable for any dictionary or set, just requires casting). I'll also be making a modification so that all the Node_T inherit from some interface so as to avoid extra array creation.
12-15-2009, 01:43 AM#12
weaaddar
Updated again.
RO_DictNode/RO_SetNode->DictEntry/SetEntry

Fixed the documentation, added the ForEach method for set and dictionary (from here I'll refer to them as maps as saying dict/set is really annoying).

Made Node_T implement an interface called INode so that less arrays are made. It should be noted that this means in theory your got smaller amount of maps possible as all types here secretly inherit from Nodes.
What do you think?
04-16-2010, 03:52 PM#13
Rising_Dusk
Grim, final verdict on this? I haven't really been looking it over like you have, but I feel like I can at least see its use in some sparse cases, so I wouldn't be averse to approving it with your go ahead.