[v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

Propose new features and changes
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

[v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

15 Aug 2023, 22:47

I've succeeded in migrating all my v1 scripts to v2, but in the process of doing so, simply to get a minimal running version, I basically did away with all Object.Property in favor of Map["Key"].

Now I'm going through my scripts once more to improve them, I find myself sometimes faced with indecision when it comes to property vs. key. And I wish there could be more guidance in the docs along the lines of: "for these usage scenarios, looking up a Map's keys is more appropriate/efficient over accessing an Object's properties (or vice versa)"

For example, say you have an HTTP server and when you handle a request, should you use Map or (mere) Object for the queries? For example say this is the type of URL you expect to handle

https://www.wikidata.org/w/api.php?action=wbgetclaims&entity=Q16850826&property=P31

Should you have request.queries["action"] or request.queries.action? Should you make "action" one of the keys in the queries Map, or should you make it one of the properties of the queries Object? *Philosophically*, I'm inclined to go with the latter approach, since I'm inclined to say that the queries in a http request are separate components of the request, much like the in representing e.g. a mouse position, x and y are separate components of the position. Clearly the more appropriate choice in the mouse position case would be position.x and position.y rather than position["x"] and position["y"]. Likewise for URL queries.

But in addition to philosophical considerations there are also technical ones. For example, other things being equal, are there any performance overhead when doing Map lookup (A["B"]) than when accessing the value of a property (A.B)? It seems that in theory the latter should be more efficient, since Map lookup necessarily involves looking up a string at (post-autoexec) runtime, whereas an Object's properties are like variable names (with the same restrictions, e.g. you can't have Object.<+j but you can have Map["<+j"]) , so there are optimization that can be done when the script is first loaded?

It'd be great if the doc could offer some guidance on this front. In moving from v1 to v2, this is one of the additional choice points one must often face when writing a script. I appreciate the additional rigor (my v2 scripts have much fewer runtime errors than my v1 scripts), but I wonder if the doc could offer a little more guidance.

(This post is inspired by seeing this comment from RaptorX, who says he's defaulting on Map most of the time. I had to do the same for the initial conversion, but I'd much rather know when to use which than to just use Map all the time
RaptorX wrote:
17 Sep 2022, 21:36
I very seldom use literal objects since I switched to v2, I use maps most of the times because it avoids using invalid names as properties.
I wish the object literal syntax was used for maps if the property names were quoted but Lexikos is against the idea or is not interested in implementing it as far as I remember. :(
)
Descolada
Posts: 1141
Joined: 23 Dec 2021, 02:30

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

16 Aug 2023, 02:33

Should you have request.queries["action"] or request.queries.action? Should you make "action" one of the keys in the queries Map, or should you make it one of the properties of the queries Object?
Which ice cream better: vanilla or chocolate? At the moment there aren't any official AHK coding style conventions, so it comes down to matter of preference. RaptorX prefers Map syntax, I on the other hand prefer object literal syntax in most cases.

My preferred style is using names as object keys whenever I can, but if I see that greater flexibility is required then I of course use Maps instead. This style also allows differentiating enumeration names and values by using object literals for names and Map keys for values. I prefer not to start names with numbers though even though this is allowed, and usually start with upper-case.
Eg. I would use Color.Black to store #000000, but Color["#000000"] to get "Black". Object literal syntax is also more terse (as you mentioned in pos["x"] vs pos.x) which I like.
Another benefit of using object literals is that they are case-insensitive by default, so you don't have to worry whether Color["Black"] returns the same value as Color["black"].
Also, Map definition syntax is ugly (especially when defining case-insensitive maps) and sometimes hard to read in my opinion. Visually separating comma-separated string-type key-value pairs in a row on one line is difficult, especially if defining a long row of them. If it were something like Map("key1":"value1", "key2":"value2") then it might be another matter...
But in addition to philosophical considerations there are also technical ones. For example, other things being equal, are there any performance overhead when doing Map lookup (A["B"]) than when accessing the value of a property (A.B)? It seems that in theory the latter should be more efficient, since Map lookup necessarily involves looking up a string at (post-autoexec) runtime, whereas an Object's properties are like variable names (with the same restrictions, e.g. you can't have Object.<+j but you can have Map["<+j"]) , so there are optimization that can be done when the script is first loaded?

Since AHK is an interpreted language and AFAIK both Map and object key lookups use binary search, I wouldn't expect much of a speed difference and wouldn't worry too much about it. I wrote a small test to try it out:

Code: Select all

a := Map()
b := {}
iterations := 1000000
Loop 26 {
    char := Chr(A_Index+64)
    a[char] := A_Index
    b.%char% := A_Index
}

start := A_TickCount
Loop iterations {
    c := a["Z"]
}
OutputDebug("Accessing Map: " (A_TickCount-start) " ms`n")
start := A_TickCount
Loop iterations {
    c := b.Z
}
OutputDebug("Accessing Object: " (A_TickCount-start) " ms`n")
In my setup a million iterations resulted in

Code: Select all

Accessing Map: 1156 ms
Accessing Object: 1094 ms
It'd be great if the doc could offer some guidance on this front. In moving from v1 to v2, this is one of the additional choice points one must often face when writing a script. I appreciate the additional rigor (my v2 scripts have much fewer runtime errors than my v1 scripts), but I wonder if the doc could offer a little more guidance.
I agree, it would be nice to have official code styling recommendations available. Not only to choose between Object vs Map (which probably will remain a matter of preference), but also standardize coding practices to make different user-defined libraries' code more uniform and readable. Eg. it would be handy if it were standard practice to prepend global variables with "g", or that class and function/method names should be PascalCase whereas variable names should be camelCase, or standardizing brace style, tabs vs spaces etc. One can dream :)
User avatar
RaptorX
Posts: 386
Joined: 06 Dec 2014, 14:27
Contact:

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

16 Aug 2023, 11:26

If we are talking "phylosophical" here, let me explain why I moved to maps and what are some considerations I take when deciding between maps and object properties:

Lets think about a dictionary... like the actual physical thing. It contains a bunch of words with their definitions. Well, those are key-value pairs. The word you are searching for is the key and the definition is its value.

Now lets think about the word languange... well this is interesting, because It can be the word language which you want the definition for (key-value), but it can also be the property of the Dictionary itself, i.e. which language is this dictionary written in.

in code:

Code: Select all

dictionary := map("language", "the principal method of human communication") ; the definition of the word language
dictionary.language := "English" ; the language the dictionary is written in.

msgbox dictionary["language"]
msgbox dictionary.language
As you can see there IS a "phylosophical" idea behind properties vs key-value pairs.

Going back to your first example with the httpQuery, I guess the actions are a property of the query... is just the things you did with it.
if you take a look at the http ComObject (WinHttp.WinHttpRequest.5.1), whenever you finish a query you can access http.ResponseText, http.status, http.ResponseStream and so on... those are the properties of the query you just performed.

---

You can set a map to case insensitive like this:

dictionary := map()
dictionary.casesense := false


Which as you can see, just modified a property of the map instead of the key-value pairs ;)

All in all, I make decisions on a case by case basis but mostly default to maps because the thing is: maps are more flexible, i can keep definitions AND properties. Objects cant do that unless you code it in with __Item and other little tricks.

BTW I personally think of Objects as a restricted type of short hand Class definition, I look at them as I look at fat arrow functions... I find that they are very similar.

Fat Arrow allow you to create a function in one line, Objects allow you to define a whole "class" in one line. You can define moethods for your object if you wanted:

Code: Select all

myObj := {manual:true, giveMe10:{call:(*)=>5+5}}
msgbox myObj.giveMe10()
There are some restrictions though.

Also maps allow for special characters in your key names... properties have a lot of restrictions on what you can use as a property name because they follow the same restrictions as variable names. So there's that too.

It's a matter of preference to be honest, but now that I understand maps better, thats the reason I default to them most of the times.
Projects:
AHK-ToolKit
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

17 Aug 2023, 07:58

Descolada wrote:
16 Aug 2023, 02:33
Which ice cream better: vanilla or chocolate? At the moment there aren't any official AHK coding style conventions, so it comes down to matter of preference. RaptorX prefers Map syntax, I on the other hand prefer object literal syntax in most cases.
I agree there are probably edge cases where the choice may really be a matter of preference, but also that in most situations only one choice is objectively superior. In particular I suspect that (for the kind of use cases I'm familiar at least) Map only should be used in very specialized situations and Object should be defaulted on most of the time.

I've gone over my scripts again, replaced all Maps with Objects except for two. One is a keymap which maps my keystrokes to commands I'd like to execute, the other is a Map (multidimensional, ht @Helgef ) that maps window contexts to entry/exit actions to execute automatically.
Also, Map definition syntax is ugly (especially when defining case-insensitive maps) and sometimes hard to read in my opinion. Visually separating comma-separated string-type key-value pairs in a row on one line is difficult, especially if defining a long row of them. If it were something like Map("key1":"value1", "key2":"value2") then it might be another matter...
I agree the Map syntax is unwiedly, but perhaps there's a hidden bonus, in that if one finds oneself inconvenienced by the syntax when hard-coding an Map object in one's source code, one should consider either that (i) Object is the more appropriate choice here or that (ii) Map is but program and data should be separated (e.g. put the Map in an external json file and load it from there)

Since AHK is an interpreted language and AFAIK both Map and object key lookups use binary search, I wouldn't expect much of a speed difference and wouldn't worry too much about it. I wrote a small test to try it out:
...
Very helpful; thanks for doing the test!
20170201225639
Posts: 144
Joined: 01 Feb 2017, 22:57

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

17 Aug 2023, 08:19

RaptorX wrote:
16 Aug 2023, 11:26
BTW I personally think of Objects as a restricted type of short hand Class definition
Yes, I find the mini-class analogy helpful. And when looked at this way, the fact that property names have similar restrictions as variable names is really as things *should* be.

One heuristic I find useful is: if I find myself often having to check whether a custom-defined object has a certain property (HasOwnProp, etc) or not, then most probably I'm doing something wrong. In most cases, I should just confidently be able to assume the object *has* that property, in the same way that you can just assume for any instance of a class that it has the properties defined in the class definition. If you are dealing with an Array, you can just assume it has the Length property (that 'length of' is well-defined for an Array) and never have to check. If you are dealing with a Map, you can just assume it has the Count property, and don't need to check. And so on. If you find yourself often having to check whether a custom-defined object has a property, then either this is a scenario where Map is more appropriate, or there's probably a problem in your code, in that you are selectively *adding* properties to your objects (and thereby changing their design!) after they've been created, when what you should do is make all the properties of the object that you ever expect to access be present in the object as soon as it's been created ...
geek
Posts: 1052
Joined: 02 Oct 2013, 22:13
Location: GeekDude
Contact:

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

17 Aug 2023, 08:33

The way I see it, object property names are fully equivalent to variable names and their usage should follow all of the same rules. As much as using double-dereference variable names to form pseudo-arrays in v1.1 was poor form, using percentage signs in object property names is a biiig no-no almost all of the time.

If your name is variable in part or whole, you should always use a map. One of the only valid reasons to use object.%name% in my mind is for meta-programming, such as manually constructing custom objects in lieu of a class where the structure cannot be known at load-time. For example, my machine code library does this to create a fake class instance of dynamically compiled C code. The syntax is intentionally ugly because it's not supposed to be sprinkled around normal every day code.

If you can achieve the effect you're going for without using obj.%...% (or its friend obj.DefineProp()) then a basic Object likely is appropriate, but the second you have to reach for dynamic property names you're probably doing something wrong.

Tangentially, I've seen a lot of people reaching for VarSetStrCapacity as a replacement for VarSetCapacity and I want to clear the air on that as often as possible. VarSetStrCapacity is not an appropriate replacement for v1's VarSetCapacity, and exists mostly as a tool for optimizing code where you construct a string using a loop. Although it can be usefully applied in conjunction with a DllCall under some circumstances, those same situations can be equally addressed by the safer and more flexible Buffer/StrGet pair.

edited to add:

I was trying to work on a guide for using objects in v2 that would address these kinds of questions (https://autohotkey.wiki/playground:objects) but I actively lose money on that project so progress has been slow. If there's more community interest in having interactive guides like that hopefully I can find more time for it...

Also edited:

Map definition syntax really is not that unwieldy. It's an extra 3 characters to mark it as a map, and some quote marks that I always included in v1 anyways. It's just different than we were used to.
User avatar
RaptorX
Posts: 386
Joined: 06 Dec 2014, 14:27
Contact:

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

17 Aug 2023, 12:56

20170201225639 wrote:
17 Aug 2023, 08:19
RaptorX wrote:
16 Aug 2023, 11:26
BTW I personally think of Objects as a restricted type of short hand Class definition
Yes, I find the mini-class analogy helpful. And when looked at this way, the fact that property names have similar restrictions as variable names is really as things *should* be.

One heuristic I find useful is: if I find myself often having to check whether a custom-defined object has a certain property (HasOwnProp, etc) or not, then most probably I'm doing something wrong. In most cases, I should just confidently be able to assume the object *has* that property, in the same way that you can just assume for any instance of a class that it has the properties defined in the class definition. If you are dealing with an Array, you can just assume it has the Length property (that 'length of' is well-defined for an Array) and never have to check. If you are dealing with a Map, you can just assume it has the Count property, and don't need to check. And so on. If you find yourself often having to check whether a custom-defined object has a property, then either this is a scenario where Map is more appropriate, or there's probably a problem in your code, in that you are selectively *adding* properties to your objects (and thereby changing their design!) after they've been created, when what you should do is make all the properties of the object that you ever expect to access be present in the object as soon as it's been created ...
I think i saw this summarized in a forum post like this:

if the properties of your data structure are known ahead of time then use an object. If the properties are dynamically created or change very often, then a Map will be easier to use.
Projects:
AHK-ToolKit
aldrinjohnom
Posts: 77
Joined: 18 Apr 2018, 08:49

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

24 Nov 2023, 21:19

I performed one-by-one Access Speed Test
Function:

Code: Select all

speedtest(iterations,Mode)
{
	; initialize to occupy space in memory
	char := 0
	c1 := 0
	c2 := 0
	c3 := 0
	c4 := 0
	c5 := 0
	c5 := 0
	c5 := 0
	; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

	if (Mode==1)
	{
		Obj := Map()
		AllocateSpeed := A_TickCount
		Loop iterations {
		    str := A_Index * 70
		    Obj[str] := [2,13,55,123,200,315,398]
		}
		AllocateSpeed := A_TickCount-AllocateSpeed

		AccessSpeed := A_TickCount
		for key in Obj {
		    c1 := Obj[key][1]
		    c2 := Obj[key][2]
		    c3 := Obj[key][3]
		    c4 := Obj[key][4]
		    c5 := Obj[key][5]
		    c5 := Obj[key][6]
		    c5 := Obj[key][7]
		}
		AccessSpeed := A_TickCount-AccessSpeed
	}
	else
	{
		Obj := {}
		AllocateSpeed := A_TickCount
		Loop iterations {
		    str := A_Index * 70
		    Obj.%str% := [2,13,55,123,200,315,398]
		}
		AllocateSpeed := A_TickCount-AllocateSpeed

		AccessSpeed := A_TickCount
		for key in Obj.OwnProps() {
		    c1 := Obj.%key%[1]
		    c2 := Obj.%key%[2]
		    c3 := Obj.%key%[3]
		    c4 := Obj.%key%[4]
		    c5 := Obj.%key%[5]
		    c5 := Obj.%key%[6]
		    c5 := Obj.%key%[7]
		}
		AccessSpeed := A_TickCount-AccessSpeed
	}

	msgbox "Mode: " . Mode . "(" . ((Mode==1)?"MapObject":"ObjectLiteral") . ") , Iterations: " . iterations " , Speed(Allocate|Access): " . AllocateSpeed . "|" AccessSpeed
}
Results:

Code: Select all

Mode: 1(MapObject) , Iterations: 1 , Speed(Allocate|Access): 0|0
Mode: 1(MapObject) , Iterations: 10 , Speed(Allocate|Access): 0|0
Mode: 1(MapObject) , Iterations: 100 , Speed(Allocate|Access): 0|0
Mode: 1(MapObject) , Iterations: 1000 , Speed(Allocate|Access): 0|0
Mode: 1(MapObject) , Iterations: 10000 , Speed(Allocate|Access): 16|16
Mode: 1(MapObject) , Iterations: 100000 , Speed(Allocate|Access): 63|219
Mode: 1(MapObject) , Iterations: 1000000 , Speed(Allocate|Access): 578|2281

Mode: 2(ObjectLiteral) , Iterations: 1 , Speed(Allocate|Access): 0|0
Mode: 2(ObjectLiteral) , Iterations: 10 , Speed(Allocate|Access): 0|0
Mode: 2(ObjectLiteral) , Iterations: 100 , Speed(Allocate|Access): 0|0
Mode: 2(ObjectLiteral) , Iterations: 1000 , Speed(Allocate|Access): 0|0
Mode: 2(ObjectLiteral) , Iterations: 10000 , Speed(Allocate|Access): 16|16
Mode: 2(ObjectLiteral) , Iterations: 100000 , Speed(Allocate|Access): 750|219
Mode: 2(ObjectLiteral) , Iterations: 1000000 , Speed(Allocate|Access): 79516|3234
Object Literals are Painfully Slow when performing large number of iterations. I'd prefer using Map Objects, unless Object Literals are needed when doing some operations.
Developer of AJOM's DOTA2 MOD Master, Easiest way to MOD your DOTA2 without the use of internet :lol: . I created this program using Autohotkey thats why I love Autohotkey and the community!

GitHub:
https://github.com/Aldrin-John-Olaer-Manalansan
just me
Posts: 9466
Joined: 02 Oct 2013, 08:51
Location: Germany

Re: [v2doc] More best practices guidance for choosing between Map and (mere) Object in X usage scenario?

27 Nov 2023, 06:07

@aldrinjohnom,
you're comparing apples with bananas. The poorer speed for the allocation of the object is caused by the fact that you are using integer keys in ascending order. Maps are able to store integer keys whereas Objects always use string keys. This causes a different sorting order. Try to run your test with the following change:

Code: Select all

	if (Mode==1)
	{
		Obj := Map()
		AllocateSpeed := A_TickCount
		Loop iterations {
		    str := String(A_Index * 70) ; <<<<<<<<<<<<<<<<
		    Obj[str] := [2,13,55,123,200,315,398]
		}

Return to “Wish List”

Who is online

Users browsing this forum: No registered users and 32 guests