ASH Maps question.

holatuwol

Developer
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:

Code:
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

Developer
Staff member
[quote author=Presto Ragu link=topic=289.msg1692#msg1692 date=1153974888]
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...[/quote]

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:

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:

Code:
        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:

Code:
    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

Member
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

Developer
Staff member
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.

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

As an example, the following script:

Code:
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

Member
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

Developer
Staff member
[quote author=Tirian link=topic=289.msg1795#msg1795 date=1154980778]- "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.[/quote]

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

Member
[quote author=Veracity link=topic=289.msg1797#msg1797 date=1154990388]
Using a map for a generalized "min" function feels convoluted to me.
[/quote]

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

Developer
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

Member
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

Developer
tirian said:
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

Developer
Staff member
[quote author=holatuwol link=topic=289.msg1801#msg1801 date=1154998320]
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.[/quote]

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).
 
Top