PDA

View Full Version : ASH Maps question.



Nightmist
07-14-2006, 02:00 PM
Hmmm just wondering but is there a function in ASH that will "remove" a section of a ASH map? (The manual doesnt even have any documentation on maps so im left with asking the people here >>)

Veracity
07-14-2006, 09:13 PM
There are functions to count the number of keys in a map and test whether a particular key is present. You can insert a new key/value pair into the map using an assignment statment. But, I don't think there's currently a way to remove a key. (I say "I think" since I'm at work now and can't check - or change - the code.)

It's a reasonable request, so I'll do it when I get home.

Tirian
07-16-2006, 12:26 AM
Ooh, neat. I've got a few things of my own that I'd love to have clarified.

Currently, to the best of my investigation, if you query a map with a key that is not defined, it returns the most "zero-ish" member of the codomain. Is this something we can count on?

Also, will the "foreach" iterator go through a map in the order in which the keys were initialized, or will it be less predictable than that?

Veracity
07-16-2006, 02:54 AM
Currently, to the best of my investigation, if you query a map with a key that is not defined, it returns the most "zero-ish" member of the codomain.* Is this something we can count on?
Yes. Every data type has an "initial" value and that is what you get if you try to map an unset key in a map.


Also, will the "foreach" iterator go through a map in the order in which the keys were initialized, or will it be less predictable than that?
Make no assumptions about the order of the keys.

Regarding the "delete key" operator, I've been pondering it. What you currently have available:

map[index] = value;
boolean map contains index;
int count(map);

A "delete" operator takes a key of whatever type is needed by the map slice. So, we'd need something like:

delete(map, key);

...where the type of key depends on the expected type of the key to the map slice you give. ASH doesn't currently have functions - even built-in ones - for which an argument can be of any type, presumably checked at run time, rather than compile time, which is what we really want.

I'm thinking:

Just like we have

boolean <map> contains <key>

where <key> is of the "correct type for the map slice" - and is checked at compile time - perhaps we can have:

<value type> <map> delete <key>

where <key> is of the correct type and the return value of the expression is the value that was in the map for that key before.

string [int] foo;
foo[5] = "xyz";

foo contains 5 --> true
foo contains 6 --> false
foo[5] --> "xyz";
foo[6] --> "" // default value for a string

new API:

string old = foo delete 5;
old --> "xyz";
old = foo delete 5;
old --> "" // default value for a string, since the key is not present

What do you think?

Tirian
07-16-2006, 03:18 AM
This must be why non-OO languages don't use maps. :D

I think I prefer the look of the way Perl does this, which would translate to ASH as "old = delete foo[5];" So that it looks to the human like a value, but the language has to do a little extra work to detach the key from the map (which might be bar[2][$item[cog]]["semprini"] instead of "foo").

Veracity
07-16-2006, 06:01 PM
OK. I just added a "remove <aggregate reference>" operator to ASH.

An aggregate reference is "<map name>[ <index 1> ... <index n> ]"
<map name>[ <index 1> ... <index n-1> ] specifies the "slice"
<index n> specifies the "key"

Which is just what you expect, if you fully specify the indices; for a single dimensional map, "map[10]" -> "map" is the slice and 10 is the key.

"remove" removes the "key" from the "slice".


string [int] map1;

map1[5] = "foo";
print( count( map1 ) + " " + map1 contains 5 + " " + map1[5] );
print( "remove: " + remove map1[5] );
print( count( map1 ) + " " + map1 contains 5 + " "* + map1[5] );
print( "remove: " + remove map1[5] );

int [string, string] map2;

map2["me","you"] = 17;
print( count( map2["me"] ) + " " + map2["me"] contains "you" + " " + map2["me","you"] );
print( "remove: " + remove map2["me", "you"] );
print( count( map2["me"] ) + " " + map2["me"] contains "you" + " " + map2["me","you"] );
print( "remove: " + remove map2["me", "you"] );

print( count( map2 ) + " " + map2["me"] );
print( "remove: " + remove map2["me"] );
print( count( map2 ) + " " + map2["me"] );
yields:


*>*remove.ash
1 true foo
remove: foo
0 false
remove:*
1 true 17
remove: 17
0 false 0
remove: 0
1 aggregate int [string]
remove: aggregate int [string]
0 aggregate int [string]
Script succeeded!

Presto Ragu
07-25-2006, 04:29 AM
Just so I do not have to start a new thread, I have a map question:

Is it possible to "pull" a key value from a map for a specific result?

I have tried to read Veracity's notes on maps... But for some reason I can't wrap my brain around them fully... Maybe I am getting just too old to understand programming as easily as I could. :P

But let me clarify:

If you made a map for MP restorers, where the two keys are the min MP and the max MP, could you pull the key value from the item assigned to it?


And another question, that is pretty much moot if the above is false:


Can maps have more than two keys?

Nightmist
07-25-2006, 05:13 AM
If you made a map for MP restorers, where the two keys are the min MP and the max MP, could you pull the key value from the item assigned to it?


And another question, that is pretty much moot if the above is false:


Can maps have more than two keys?


I'm sort of confused at what your asking but something like this might suit your needs (Total guess at what your trying to do.).

int [ item, string ] HP_Restore_Map;
HP_Restore_Map [ $item[none], "min"] = 5;
HP_Restore_Map [ $item[none], "max"] = 10;

print( HP_Restore_Map [ $item[none], "min"]);
print( HP_Restore_Map [ $item[none], "max"]);
This will print the minimum and then max. (Although you could just use 2 different maps.)

Tirian
07-25-2006, 05:27 AM
Yup, I was just writing what Nightmist just said, that two maps seems like it would be the best way to do that. Assuming we're understanding your MP restorer question correctly.

As far as figuring out how to use maps, I've started working them into my scripts, so maybe seeing them being used in context would give you some ideas to reinforce Veracity's primer:

http://kolmafia.us/index.php/topic,139.0.html -- the bounty hunter section contains a map of all of the items that you could want to redeem with the bounty hunter, iterated through with the foreach command.

http://wiki.kolmafia.us/?page_id=12 -- sample 7 shows a different use of maps where we don't care about the value but are just using a bunch of keys like a set and using the "contains" operator to see if a value is in the set.

http://kolmafia.us/index.php/topic,297.0.html -- here I'm building maps to contain my logic for when I've picked up all the items I could want from adventuring in a certain location, and then passing that map to a function to see if I've completed it.

Veracity
07-25-2006, 12:23 PM
Is it possible to "pull" a key value from a map for a specific result?
A map defines a "mapping" from keys of a particular type to values of a (potentially different) particular type. So, if you want to map integers to strings, your map could contain:


1 -> "first string"
8 -> "another string"
12 -> "another string"

Note that the keys are unique within a given map - the same key does not map to multiple values - but the values are not unique. At least partly for that reason, ASH provides no "inverse mapping" facility which will tell which key(s) map to a specified value.


If you made a map for MP restorers, where the two keys are the min MP and the max MP, could you pull the key value from the item assigned to it?

Maps cannot have "two keys". Any given map has exaqctly ONE key, which is used as described above. Now, ASH provides syntax for making multi-dimensional maps, but internally, those are really "maps of maps of ...".


[1,2] -> "a string"
[1,3] -> "another string"
[2,3] -> "yet another string

This looks like two keys, and for many purposes you can simply use them like that. But what it really is is two layers of maps, and there are times when you need to be aware of that. What that really is:

mapping from integer to ( mapping from integer to string )


1 -> <map from integer to string>
* * 2 -> "a string"
* * 3 -> "another string"
2 -> <map from integer to string>
* * 3 -> "yet another string"

If you look at things like that, you can see that this is how you can store multiple values for a given key; the key "1" has two values, which are referenced in turn by a second integer.

You can often think of the two values as both being keys of the same map, but they're not really, and if you look at how you'd use foreach, it might be more obvious:


string [int, int] map1;

foreach key1 in map1
* * // key1 is as integer and the value is a map from int to string
* * foreach key2 in map1[key1]
* * * * // key2 is an integer and the value is a string
* * * * print("[" + key1 + "," + key2 + "] -> " + map[key1, key2] );

In your example, I'm not sure you want to map from "min HP" AND "max HP" to an item. In fact, using either "min HP" or "max HP" as a key in a map is questionable, since maps require their keys to be unique; what result would you expect if you had two different items with the same "min HP" restored?


Can maps have more than two keys?

A map can have exactly one key, but if you want to make "maps of maps of ..." and access such using [key, key, ...] index syntax, there is no limit to the number of dimensions.

Presto Ragu
07-26-2006, 04:48 AM
Well...

First, since most of you hadn't noticed, I have already grasped the basic uses of maps.

If you check my new version of MuseumStockUp here (http://kolmafia.us/index.php/topic,226.0.html"), and RestorerSelector here (http://kolmafia.us/index.php/topic,39.0.html), you will see that simple uses I can handle already. So, script examples weren't even needed either.

I was simply looking for answers to specific questions.

And since my intentions were not quite received correctly, let me try again.

I personally despise the idea of having to define multiple maps for multiple pieces of data about the same "object." Perhaps it is my stubbornness... But when Veracity claimed that maps were much more usefull than arrays, I expected them to behave similarly.

What I had in mind was a multiple dimension map that held all of the usefull information about foods and drinks.

For example:


item [int, stat, int, int] FoodItem;
FoodItem [<fullness>, <main stat increase, if any>, <minimum adventures gained>, <maximum adventures gained>] = $item[<whatever>];

This would allow the user to reference almost any data that they wanted related to a food item. You would be able to average the adventure gain, and divide by the fullness in order to get the adventures per fullness if you want.

It seems silly to my mind to have multiple maps to hold the different values when there should already be a way to reference the values in one multiple dimensional map.


But I guess I am too old school to see how maps are better than arrays yet... :P

Nightmist
07-26-2006, 04:56 AM
It seems silly to my mind to have multiple maps to hold the different values when there should already be a way to reference the values in one multiple dimensional map.


Thats interesting although in effect it just compresses the number of lines you need for a single item down to a single line (And then again maps just compress a series of if-return functions into less lines...)

I would be rather interested in how you could define which value you expected returned though.


Although to answer your original question with the examples you just gave then no it isnt current possible with the current ASH map system. (Well possible as far as I know of)

Veracity
07-26-2006, 05:35 AM
I personally despise the idea of having to define multiple maps for multiple pieces of data about the same "object."* Perhaps it is my stubbornness...* But when Veracity claimed that maps were much more usefull than arrays, I expected them to behave similarly.

I believe that my position was that you can do anything with a map that you can with an array, and more. Nothing you've said makes me want to change that position.


What I had in mind was a multiple dimension map that held all of the usefull information about foods and drinks.

For example:


item [int, stat, int, int] FoodItem;
FoodItem [<fullness>, <main stat increase, if any>, <minimum adventures gained>, <maximum adventures gained>] = $item[<whatever>];

You are asking, essentially, for data structures, and for a map to be able to go from item to your structure.

You can get the effect you want, today, using maps as they exist in ASH.
Nightmist told you exactly how to do it.

int [item, string] map;
map[ $item[foo], "fullness" ] = 2;
map[ $item[foo], "min stat increase" ] = 5;
map[ $item[foo], "max stat increase" ] = 10;
map[ $item[foo], "min adventure gained" ] = 6;
map[ $item[foo], "max adventure gained" ] = 16;

Since you seem to be using "arrays" as your model for what you seek, this is the equivalent of you having an array of <n> integers, and the value you fetch from the map is such an array. The second index of the map is the index into your array.

Since you refer to "old school" thinking, you'd probably do it like this:

int FULLNESS = 0;
int MIN_ADVENTURES = 1;
int MAX_ADVENTURES = 2;

int [ item, int] map;
map[ $item[foo], FULLNESS ] = 3;
map[ $item[foo], MIN_ADVENTURES ] = 6;
map[ $item[foo], MAX_ADVENTURES ] = 16;

...which has precisely the same effect as what was done above with a string as the second index.

Now, if you wanted different data types - integers, stats, whatever - then you couldn't store them in a single map. You could not say:

map[ $item[foo], "stat" ] = $stat[muscle];

to store a "stat" in your map whose values are ints.

But then again, you couldn't store them in a single array in most languages, either. You would need a data structure:

struct x {
* *int field1;
* *stat field 2;
* *item field 3
* *...
};

However, since you mention "arrays" as what you want the power of, this is moot; it's more than an "array" would give you.


This would allow the user to reference almost any data that they wanted related to a food item.* You would be able to average the adventure gain, and divide by the fullness in order to get the adventures per fullness if you want.

Go for it. Use ASH maps as they exist today to hold the three integers you mention, indexed by item and by string, as described above.


But I guess I am too old school to see how maps are better than arrays yet...* :P

I can give you examples of things that you can do with an ASH map that you can't do with an "array". But so far you have failed to give an example of something you can do with an "array" that you can't do with an ASH map.

macman104
07-26-2006, 05:38 AM
But then again, you couldn't store them in a single array in most languages, either. You would need a data structure:

struct x {
int field1;
stat field 2;
item field 3
...
};Completely irrelevant to the discussion, I was just excited because I just read about structures for a program I was working on, and I understood what you wrote. Yay!

Veracity
07-26-2006, 06:03 AM
Looking again at what you request:



item [int, stat, int, int] FoodItem;

Yup. You are explicitly asking for "structs".

struct FoodItem {
* * int fullness;
* * stat mainStat;
* * int minAdventures;
* * int maxAdventures;
};

And a way to map an "item" to such a structure and reference the various fields.

Sorry. All your talk about "arrays" mislead me, since an "array" in just about any language I've heard of wouldn't help you here; an "array" is defined to be a data that holds data of a particular type - which can be an aggregate types like a "struct", in languages that provide that.

A "struct" holds items of disparate types and thus can't be modeled by an array. It's an orthogonal concept.

Now, if we added "structs" to ASH to allow you to group together disparate data types as you desire, I'd expect that you could store them in a map indexed by items to achieve the result you want.

But an ASH "map" is not an implementation of a "stuct".
It's merely a superset of an "array".

Presto Ragu
07-26-2006, 04:27 PM
Well, when I was doing Pascal programming back in High School... Some 15 or so years ago, what we used that were called arrays allowed us to define what the dimensions were, what those dimensional values were, and reference them as necessary.

Maybe they were structs, and we never learned to call them what they really were...

I will grant you that.

But that is what I think of when I think of arrays. In short, it would be like building a spreadsheet within the script and being able to reference each cell as you needed. Not make five or six pages, and have to reference them separately.

But, I did receive my answers. Thank you.

Veracity
07-26-2006, 04:56 PM
Well, when I was doing Pascal programming back in High School...* Some 15 or so years ago, what we used that were called arrays allowed us to define what the dimensions were, what those dimensional values were, and reference them as necessary.

I used to program in Pascal. Professionally, even. But the last time I even looked at a Pascal program was in 1979, so I am rather rusty.

The Wikipedia has an article that compares Pascal and C (http://en.wikipedia.org/wiki/Pascal_and_C#Array_types), including arrays.

Near as I can tell, the main difference is that you can use user-defined types - enums, in particular - as array indices. So, you can do:


type enum = (red, green, blue);
myarray = array[enum] of integer;

and reference it with


myarray[red]

But notice: the element type is specified - "integer" in that case.

This provides nothing that ASH doesn't provide.

You want to "define what the dimensions were, what those dimensional values were, and reference them as necessary"? OK.

item [int, int, stat, int] testMap;
testMap[ 5, 5, $stat[ muscle ], 123 ] = $item[ knob sausage chow mein ];

The key thing in your original request which is absent in ASH - and in every other language's implementation of "arrays", including Pascal's - is the ability to say "I have this value - $item[ knob sausage chow mein ] - and I want to get the set of array (map) indices where that value is stored." You want some sort of content-addressable data structure, and ASH - and Pascal and C and Java and ... - don't provide that facility. EXCEPT by turning it around and making the value - the item, in this case - be what you index on and the set of indices be a composite data type that you get by indexing on the item.

And that is why we-all have been trying to steer you into thinking about structs - "records", as they are called in Pascal - as that is the language construct that provides what you are asking for.

Arrays - and maps - are "aggregate" data types: they hold multiple pieces of data of the same type.

Structs - and records - are "compound" data types: they hold multiple pieces of data of diverse types.


In short, it would be like building a spreadsheet within the script and being able to reference each cell as you needed.* Not make five or six pages, and have to reference them separately.

Can a spreadsheet reference a cell by its contents, and not by row and column? Can you say "what are the row and column where the number 100.23 appears?" I don't use spreadsheets, so I'm in no position to say, but I'd never heard that they were content-addressable...

Presto Ragu
07-27-2006, 04:34 AM
Well, once I got over the problem of things not working like I thought they should, I have figured out that it should be possible to get it to work.

It is very convoluted, and frankly there are problems with it...

First, I can't seem to get my sample script to work. That is likely due to the fact that I just haven't grasped the nuances of maps yet.

Second, without the ability to be able to pass data from functions by reference, it becomes operationally more efficient to use multiple maps. Although... As far as defining goes, it is easier to do it with one map.

But I will present my sample script for everyone to look over, and see what I was thinking when I wrote it. Bear in mind, there is an error that prevents it from functioning properly.


EDIT:: Oh, founditem was intended for a check if an item was even found. I forgot to finish that logic, since I never got the basics working. The line would look something like:

if( !founditem)
{
return "The requested item could not be found";
}

And that would be before the checks for testvalue.

Tirian
07-27-2006, 05:42 AM
The first problem is that your data structure is not conducive to maps at the moment. For simplicity's sake, I got rid of the function in the middle just to test your assignment.


item [int, stat, int, int] FoodItem;

FoodItem [6, $stat[muscle], 21, 29] = $item[spaghetti with skullheads];
FoodItem [6, $stat[moxie], 21, 29] = $item[fettucini inconnu];
FoodItem [6, $stat[mysticality], 21, 29] = $item[gnoccetti di nietzsche];

void main()
{
print(count(FoodItem));
}

You can verify that the output is 1, not 3. This is because FoodItem is not a map with four keys, it is a chain of four maps. The first assignment is 6 => (muscle => (21 => (29 => s.w.s))), and your second assignment overwrites that with a new value for 6.

This seems to work:


boolean [ item, int, stat, int, int ] FoodItem;
FoodItem [ $item[spaghetti with skullheads], 6, $stat[muscle], 21, 29] = true;
FoodItem [ $item[fettucini inconnu], 6, $stat[moxie], 21, 29] = true;
FoodItem [ $item[gnoccetti di nietzsche], 6, $stat[mysticality], 21, 29] = true;

string unravel (item food_key, string value_wanted)
{
if ( !FoodItem contains food_key )
{
print( "no such food!" );
return "";
}
foreach fullness in FoodItem[ food_key ]
{
if ( value_wanted == "fullness" ) return int_to_string( fullness );
foreach mainstat in FoodItem[ food_key, fullness ]
{
if ( value_wanted == "mainstat" ) return stat_to_string( mainstat );
foreach minadventures in FoodItem[ food_key, fullness, mainstat ]
{
if ( value_wanted == "minadventures" ) return int_to_string( minadventures );
foreach maxadventures in FoodItem[food_key, fullness, mainstat, minadventures]
{
if ( value_wanted == "maxadventures" ) return int_to_string( maxadventures );
}
}
}
}
print( "Illegal value sought!" );
return "";
}

void main()
{
print( unravel($item[spaghetti with skullheads], "fullness"));
print( unravel($item[fettucini inconnu], "maxadventures"));
}

Presto Ragu
07-27-2006, 08:01 AM
You can verify that the output is 1, not 3.


Well... That is just... Wow...

I think I need to rewrite my Restorer script now. Because if that is true, then I am getting maps of 1 item each...

Can you verify that this is the way you wanted it to work Veracity? If so, then I for one vote for plain old arrays. Even the ones that aren't sructs. You can keep your versatility, I'd just take simplistic functionality at this point. :S


EDIT:: That can't be right Tirian...

My Restorer script assigns 18 items. 1 map with 9 items that have a first key of "hp", and 9 items that have a first key of "mp." And that works as intended... The following ones do not overwrite the previous ones.

I think it is official.

I do not understand. :P

holatuwol
07-27-2006, 08:46 AM
Not quite, Tirian.* The count() function simply works differently than you might expect; basically, it does not give you the size of the map, it simply counts how many keys there are in the specified dimension.* As such, the output from this is 1 followed by 3:


item [int, stat, int, int] FoodItem;

FoodItem[6, $stat[muscle], 21, 29] = $item[spaghetti with skullheads];
FoodItem[6, $stat[moxie], 21, 29] = $item[fettucini inconnu];
FoodItem[6, $stat[mysticality], 21, 29] = $item[gnoccetti di nietzsche];

void main()
{
print( count( FoodItem ) );
print( count( FoodItem[6] ) );
}

Veracity
07-27-2006, 03:22 PM
Well, once I got over the problem of things not working like I thought they should, I have figured out that it should be possible to get it to work.

It is very convoluted, and frankly there are problems with it...

Tirian is correct about your data structure not being conducive to maps. And his suggestion of having the item # being the first dimension and simply storing a dummy boolean as the ultimate value is good; it lets you immediately look up the food item and the rest of your lookup is a way to get such and such an attribute of that food item, in the absence of structures or records.

However, in the interest of education, I'll point out some problems with your initial code:


foreach key1 in FoodItem
{
foreach key2 in FoodItem
{
foreach key3 in FoodItem
{
foreach key4 in FoodItem
{
if( FoodItem[" + key1 + ", " + key2 + ", " + key3 + ", " + key4 + "] == testitem)
{
founditem = true;
itemfullness = key1;
itemstat = key2;
itemminadventures = key3;
itemmaxadventures = key4;
}
}
}
}
}

If I were to do this, here's what I would do:


* ** * *foreach key1 in FoodItem
** * * * ** * * foreach key2 in FoodItem[ key1 ]
* * * ** * ** * ** ** * foreach key3 in FoodItem [ key1, key2]
* * * * * ** * * * * * * * ** * foreach key4 in FoodItem[ key1, key2, key3 ]
* * * * * * * * * * * * * * ** * ** * * i f ( FoodItem[ key1, key2, key3, key4 ] == testitem)
* * * * * * * * * ** * ** * ** * * * * *{
* * * * * * * * * * * ** * ** * ** * * ** * ** *// ...do your processing here...
* * * ** * * * * * * * ** * * * * * ** * ** * * return;
* * ** * * * * * * * ** * * * * ** * ** }

I may change foreach to allow this:


* * foreach key1, ..., key4 in FoodItem
* * * * <statement or block that can access key1 ... key 4>

...which is just syntactic sugar for the nested set of loops. It'd be dangerous were I to do so, since the programmer might get lulled into the false sense that it's an efficient way to do things, which it isn't, particularly; if you have <m> dimensions, it's O(N^m).

I continue to be puzzled by your statement that you'd prefer "the simplicity of arrays". I see nothing in your algorithm that would be "simpler" if you had a Pascal array rather than an ASH map.

Tirian
07-27-2006, 03:59 PM
Thanks, Veracity. I guess I was too lazy last night to properly debug Presto Ragu's script, and should have been more honest about the fact that it's usually quicker to write a ten-line function than to debug someone else's ten-line function. So I took another look after a good night's sleep and a nutritious breakfast. The three problems that I found are two that Veracity did with FoodItem[key1, key2, key3, key4] and that the internal foreaches need to iterate over the slices from the previously set iterators. Also, you mispelled "Gnocchetti di Nietzsche" (you missed the h in the first word). With those changes, your code runs properly.

I stand corrected about multiple values in multidimensional maps. I believe that I'm on firmer ground when I say that when you set FullItem[1, $stat[none], 1, 1] = $item[olive] and then follow that up with FullItem[1, $stat[none], 1, 1] = $item[orange], that there will be data loss. So my idea of putting the item name as the first key in the map was twofold: it guaranteed that the total chain of keys would be unique and also that the lookup for that item would be immediate and not the result of a linear search through the FoodItem structure.

Veracity
07-28-2006, 12:37 AM
This discussion has inspired me to extend the syntax of the foreach statement. In the next version of KoLmafia, it will allow multiple key variables, which will get assigned to indices in order.


foreach key1 [, key2 ...] in slice
* * <statement or block>

As an example, the following script:


int [int, string] map2;
map2[ 0, "me" ] = 10;
map2[ 0, "you" ] = 12;
map2[ 0, "him" ] = 15;
map2[ 6, "key" ] = 1;
map2[ 7, "even" ] = 111;
map2[ 7, "odd" ] = 100;

print( "Iterating using nested foreach statements" );
foreach key1 in map2
* * foreach key2 in map2[key1]
* * * * print( "map2[" + key1 + "," + key2 + "] = " + map2[key1,key2] );

print( "Iterating using multiple keys in a single foreach statement" );
foreach key1, key2 in map2
* * print( "map2[" + key1 + "," + key2 + "] = " + map2[key1,key2] );

generates the following:


>*foreach.ash
Iterating using nested foreach statements
map2[0,him] = 15
map2[0,me] = 10
map2[0,you] = 12
map2[6,key] = 1
map2[7,even] = 111
map2[7,odd] = 100
Iterating using multiple keys in a single foreach statement
map2[0,him] = 15
map2[0,me] = 10
map2[0,you] = 12
map2[6,key] = 1
map2[7,even] = 111
map2[7,odd] = 100
Script succeeded!

The version with multiple keys is not only easier to read than the nested version, but it executes more efficiently.

Tirian
08-07-2006, 07:59 PM
Here's a script that does the sort of thing that I'd like to do more often. I run it when I've got a bartender in my campsite that I want to explode. Scraftable_cocktails is a map whose values contain all of the drinks that I would want to craft myself, and make_enough_cocktails() is a function that manages the creation of as many of the drink as I can and buying enough fruit for the job and things like that. The wrinkle is that rather than making the drinks in the same order, I want to focus first on the drinks that I have the fewest of, so that over the course of 10 runs I don't find myself with a thousand fuzzbumps and no rockin' wagons.


item key_with_minimal_value( int [ item ] Mmap )
{
if ( count( Mmap ) == 0 )
return $item[ none ];

int lowest = 999999999;
item runt = $item[ none ];
foreach key in Mmap
if ( Mmap[ key ] < lowest )
{
runt = key;
lowest = Mmap[ key ];
}
return runt;
}

void blow_bartender()
{
int [ item ] Mcocktail_inventory;
foreach cocktail in Scraftable_cocktails
Mcocktail_inventory[ cocktail ] = item_amount( cocktail );

while ( count( Mcocktail_inventory ) > 0 )
{
item cocktail = key_with_minimal_value( MCocktail_inventory );
make_enough_cocktails( cocktail );
print (remove Mcocktail_inventory[ cocktail ]);
}
}

So, this generates two map-related comments:

- "remove" gives an syntax error when it's used in void context (i.e. if you remove the print() around it). Something about expecting a }. I couldn't get it to work without the print.

- key_with_minimal_value is the sort of thing that would be really awesome as an ASH keyword. This function isn't really reusable from my standpoint because the domain and codomain of the map are hard-coded, but inside the program you would be able to anticipate the types in real-time and do what needed to be done. In addition to applications like this that are out to iterate through a map in a sort-ish kind of way, it would also help anyone who generally wants a min( a, b, c ) function with a flexible number of parameters.

Veracity
08-07-2006, 10:39 PM
- "remove" gives an syntax error when it's used in void context (i.e. if you remove the print() around it).* Something about expecting a }.* I couldn't get it to work without the print.

OK, I fixed this. More or less. The problem is that although "remove" looks like a function call with no () around the argument, it's actually coded as an operator, like "+". So, you were getting the same error message you'd get if you tried using "5+3" as a standalone statement.

That last one doesn't make any sense; you calculate a value and discard it. But, although "remove" calculates a value - the element that was formerly in the map - it is reasonable to just let you discard it. So now you can.

I don't think my solution is perfect - you can now do incorrect things like "remove map[key] + 5;" for example, which is just the sort of "calculate a value and discard it" expression I mentioned earlier, but I don't have time right now to figure out how to disallow that. So, don't do it. :)


- key_with_minimal_value is the sort of thing that would be really awesome as an ASH keyword.* This function isn't really reusable from my standpoint because the domain and codomain of the map are hard-coded, but inside the program you would be able to anticipate the types in real-time and do what needed to be done.

I'll have to think of this specific function is what is wanted (with, probably, a different name :P) or if there's some more general facility needed to allow passing general maps to a function. Although I think that'd be hard to deal with, since you'd want to handle any key type and any value type - and potentially return a key, a value, or something else entirely.

Using a map for a generalized "min" function feels convoluted to me.

Tirian
08-07-2006, 11:14 PM
Using a map for a generalized "min" function feels convoluted to me.


Maybe a little. But the times that I use "min", the arguments can be fairly complex, so it wouldn't really add much to the code length. Here's a sample of how one might calculate how many fuzzbumps you could create:


item booze = $item[ bottle of whiskey ];
item midpoint = $item[ whiskey sour ];
item garnish = $item[ coconut shell ];

int x;
int [string] creatable_with;
creatable_with[ "infinite coconuts" ] = item_amount( booze ) + item_amount( midpoint );
creatable_with[ "infinite whiskey" ] = item_amount( garnish );
return creatable_with[ key_with_minimal_value( creatable_with )];

Sure, the key is pretty much unused. But it's something that we could do with maps if that's the only non-scalar data type we have access to, and it's more flexible than a formula that could have from 1 to n arguments.

holatuwol
08-08-2006, 12:52 AM
int creatable_amount( item it )
Returns the number of copies of the item that you are capable of creating with your current inventory. Servant availability and remaining adventures are not factored into this calculation.

That said, minimal_key( map ) makes some sense, given that any value which is allowed as an index can be compared.* However, a minimal_value( map ) function feels counter-intuitive since maps can be multi-index and there's no way for KoLmafia to ensure at parse-time that you're calling that function only on maps that have one index.

Tirian
08-08-2006, 01:30 AM
creatable_amount() wouldn't work in this instance because I don't necessarily have any lemons in my inventory at this stage. I don't want to buy them until I find out how many I'll need. If you want to add a way for me to tell KoLmafia, "Just for grins, let's pretend that I have a billion lemons in my inventory" -- well, that and a time machine would save me a lot of hassle. :)

I would expect that a function like what I'm proposing would abort in runtime if faced with a multi-indexed map, and I'd be the first to defend you if anyone thought that this was a limitation. But, gosh, being able to sort a list based on a user-defined metric is such a natural construct in problem-solving that it should fall under the "easy things should be easy" maxim of language design. Even my beloved Perl makes this hard, where you have to do stuff like


foreach ( sort { $foo{ $a } <=> $foo{ $b }} keys %foo )
{
...
}

holatuwol
08-08-2006, 04:29 AM
If you want to add a way for me to tell KoLmafia, "Just for grins, let's pretend that I have a billion lemons in my inventory" -- well, that and a time machine would save me a lot of hassle.
Actually, one of the customizations I've been planning is to let KoLmafia assume that any item from an NPC store is available in an infinite amount for creation purposes. :D

Veracity
08-08-2006, 01:02 PM
However, a minimal_value( map ) function feels counter-intuitive since maps can be multi-index and there's no way for KoLmafia to ensure at parse-time that you're calling that function only on maps that have one index.

You'd have to make it an operator like "contains" or "removes" rather than a simple function. That would allow you to enforce that you give it a final slice - something that resolves to the value, rather than an intermediate slice - and would also let the value of the expression be the type of the value stored in that slice.

I suppose we could define various "pseudo-functions" that take an arg list in parentheses like a function call but act like an operator, for things that require special parse-time checking of the arguments or which can return different data types.

The proposed function required both.

And a real "min" or "max" could be done that way, too: they could accept any number of arguments and have the result be an integer (if all of the arguments are integers) or a float (if there were any floats in the argument list, in which case all arguments get coerced to float during the comparison).