guides:custom_enumerators

Making Custom Enumerators (v1 Guide)

Originally by Geek (GeekDude)

An enumerator object is the object that defines the parameters of a for-loop. The default enumerator object gives the contents of an array sorted alphanumerically by the keys. Enumerator objects can be much more flexible than this, generating any values you could want.

An enumerator object is a class that has a method Class.Next. This method gets called every iteration of the for loop. There are up to two parameters for the Class.Next method, not counting the built in "this" parameter. The parameters are most commonly ByRef k and ByRef v. The names of the variables doesn't matter so much, but they do have to be ByRef.

The Class.Next method sets the values of the two ByRef parameters, and they become the two variables k and v from for k, v in MyArray. The method returns a True/False value that indicates if the loop should keep looping or if it should break.

For example, this custom enumerator would enumerate through the values 1, 2, 2, 4, 3, 6 and so on.

class CustomEnum
{
	static i := 0
	Next(ByRef k, ByRef v)
	{
		this.i++
		k := this.i
		v := this.i*2
		return True
	}
}

An enumerator object is gotten when a for loop calls the _NewEnum method on the object passed into it. For example, the code for k, v in Members would call Members._NewEnum() to get the enumerator object.

To implement your own enumerator object, you will have to override the _NewEnum method on the object so it returns your custom enumerator. This can be done in a few ways, but the simplest is to just set it to a function reference. For example:

MyArray := ["Nobody", "expects", "the"]
MyArray._NewEnum := Func("MyNewEnum")
 
MyNewEnum(this)
{
	return new CustomEnum()
}

Another way would be to create a custom class that already has _NewEnum overridden.

MyObject := new ObjectWithCustomEnum()
 
class ObjectWithCustomEnum
{
	_NewEnum()
	{
		return new CustomEnum()
	}
}

For cleaner code, the enumerator object can be defined as a nested class of the original object.

class ObjectWithCustomEnum
{
	_NewEnum()
	{
		return new this.CustomEnum()
	}
 
	class CustomEnum
	{
		static i := 0
		Next(ByRef k, ByRef v)
		{
			this.i++
			k := this.i
			v := this.i*2
			return True
		}
	}
}

Or if you want, it can even be the same object!

class ObjectWithCustomEnum
{
	_NewEnum()
	{
		; Reset i since we aren't making a new object
		; that starts off with a fresh i as 0
		this.i := 0
		return this
	}
 
	Next(ByRef k, ByRef v)
	{
		this.i++
		k := this.i
		v := this.i*2
		return True
	}
}

Putting the parts together, you can get a working custom enumerator setup going. Here's an example that enumerates the Fibonacci sequence.

for a, b in new Fibonacci()
	MsgBox, % a ", " b ", " b/a ; b/a is an approximation of the golden ratio
 
class Fibonacci
{
	_NewEnum()
	{
		return new this.MyEnum()
	}
 
	class MyEnum
	{
		static Fib := [0, 1]
		Next(ByRef a, ByRef b)
		{
			this.Fib.Push(this.Fib[1] + this.Fib[2])
			a := this.Fib.RemoveAt(1)
			b := this.Fib[1]
			return True
		}
	}
}

Since classes are really just objects, your custom enumerator class can be created using object syntax, for example {Next: Func("CustomNext"), i: 0, Jiggly: "puff"}. Going along with how methods are implemented in AutoHotkey, CustomNext would need to have the "this" parameter explicitly defined. CustomNext(this, ByRef a, ByRef b).

The second ByRef parameter of Next can be made optional, in the case that you want to only use the first value in the for loop (i.e. for k in Thing). This would create a Next method that looked something like this: Next(ByRef a, ByRef b="")

If you're inclined to, you can actually fit this entire system into a single function. This is a function that can be used to let you use a for loop on COM collections.

for element in Wrapper(document.getElementsByTagName("div"))
	MsgBox, % element.innerHTML
 
Wrapper(ByRef this, ByRef p="")
{
	if IsObject(this) && ComObjType(this)
		return {_NewEnum: Func("Wrapper"), self: this}
	else if IsObject(this) && ObjHasKey(this, "_NewEnum")
		return {Next: Func("Wrapper"), self: this.self, i: 0}
	else
		return this.i<this.self.length?(True,p:=this.self.item(this.i++)):False
}