Wednesday, March 07, 2007

Fun with enumerators, part 3 - parameterized enumerators

[Part 1 - Introduction, Part 2 - Additional enumerators.]

In first two installments of this series, we took a look at for..in statement, described Delphi support for custom enumerators and demonstrated one possible way to add additional enumerator to a class that already implements one.

Today we'll implement a parameterized enumerator. Enumerators of this kind can be especially handy when used as filters. For example, an enumerator that takes a class reference as a parameter can be used to filter a TObjectList containing objects of many different classes.

IOW, we would like to write a following construct: for obj in list.Enum(TSomeClass) do. Clearly, Enum cannot be a property anymore, but it can be a function.

We can still use an enumerator factory object stored inside our class (as in the Part 2). Besides that, we must implement enumeration function that will take one or more parameters, preserve them inside the factory object and return the factory object so that Delphi compiler can call its GetEnumerator function.

Sounds complicated but in reality it really is simple.

First we have to add an internal factory object and an enumeration function to the TCustomStringList class.

  TCustomStringList = class(TStringList)
private
FEnumEveryNth: TCSLEnumEveryNthFactory;
public
constructor Create;
destructor Destroy; override;
function SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
end;

constructor TCustomStringList.Create;
begin
inherited;
FEnumEveryNth := TCSLEnumEveryNthFactory.Create(Self);
end;

destructor TCustomStringList.Destroy;
begin
FEnumEveryNth.Free;
inherited;
end;

function TCustomStringList.SkipEveryNth(skip: integer): TCSLEnumEveryNthFactory;
begin
FEnumEveryNth.Skip := skip;
Result := FEnumEveryNth;
end;


Enumeration function SkipEveryNth takes one parameter and passes it to the factory object. Then it returns this factory object.



We also need a new factory class. This is an extended version of factory class from Part 2. It must implement storage for all enumerator parameters. In this case, this storage is implemented via property Skip.

  TCSLEnumEveryNthFactory = class
private
FOwner: TCustomStringList;
FSkip: integer;
public
constructor Create(owner: TCustomStringList);
function GetEnumerator: TCSLEnumEveryNth;
property Skip: integer read FSkip write FSkip;
end;

constructor TCSLEnumEveryNthFactory.Create(owner: TCustomStringList);
begin
inherited Create;
FOwner := owner;
FSkip := 1;
end;

function TCSLEnumEveryNthFactory.GetEnumerator: TCSLEnumEveryNth;
begin
Result := TCSLEnumEveryNth.Create(FOwner, FSkip);
end;


As you can see, GetEnumerator was also changed - in addition to passing FOwner to the created enumerator, it also passes current value of the Skip property.



New enumerator is very similar to the one from the Part 2, except that it advances list index by the specified ammount (instead of advancing it by 2).

  TCSLEnumEveryNth = class
private
FOwner: TCustomStringList;
FListIndex: integer;
FSkip: integer;
public
constructor Create(owner: TCustomStringList; skip: integer);
function GetCurrent: string;
function MoveNext: boolean;
property Current: string read GetCurrent;
end;

constructor TCSLEnumEveryNth.Create(owner: TCustomStringList; skip: integer);
begin
FOwner := owner;
FListIndex := -skip;
FSkip := skip;
end;

function TCSLEnumEveryNth.GetCurrent: string;
begin
Result := FOwner[FListIndex];
end;

function TCSLEnumEveryNth.MoveNext: boolean;
begin
Inc(FListIndex, FSkip);
Result := (FListIndex < FOwner.Count);
end;


We can now write some test code.

procedure TfrmFunWithEnumerators.btnSkipEveryThirdClick(Sender: TObject);
var
ln: string;
s : string;
begin
ln := '';
for s in FslTest.SkipEveryNth(3) do
ln := ln + s;
lbLog.Items.Add('Parameterized enumerator: ' + ln);
end;


Delphi compiler will translate this for..in roughly into

enumerator := FslTest.SkipEveryNth(3).GetEnumerator;
while enumerator.MoveNext do
ln := ln + enumerator.Current;
enumerator.Free;


So what is going on here?





  1. FslTest.SkipEveryNth(3) sets  FslTest.FEnumEveryNth.Skip to 3 and returns FslTest.FEnumEveryNth.

  2. Compiler calls FslTest.FEnumEveryNth.GetEnumerator.

  3. FslTest.FEnumEveryNth,GetEnumerator calls TCSLEnumEveryNth.Create(FslTest, 3) and returns newly created object.

  4. Enumerator loops until MoveNext returns false.


Test code result:





Tomorrow we'll do something even more interesting - we'll add new enumerator to existing class without creating a derived class.





Technorati tags: , ,

11 comments:

  1. This is interesting stuff, and don't get me wrong, I'll certainly be looking out for the next installment. But... you need more lines to hook into the compiler's for/in syntax than if you didn't bother! Cf.:

    ICSLEnum = interface
    function FindNext(out S: string): Boolean;
    end;

    TCStringList = class(TStringList)
    public
    function CreateEnumerator(Skip: Integer): ICSLEnum;
    end;

    TCSLEnum = class(TInterfacedObject, ICSLEnum)
    private
    FListIndex: Integer;
    FOwner: TCStringList;
    FSkip: Integer;
    protected
    function FindNext(out S: string): Boolean;
    public
    constructor Create(Owner: TCStringList;
    Skip: Integer);
    end;

    constructor TCSLEnum.Create(Owner: TCStringList; skip: integer);
    begin
    FOwner := Owner;
    FSkip := Skip;
    end;

    function TCSLEnum.FindNext(out S: string): Boolean;
    begin
    Result := (FListIndex < FOwner.Count);
    if not Result then Exit;
    S := FOwner[FListIndex];
    Inc(FListIndex, FSkip);
    end;

    function TCStringList.CreateEnumerator(Skip: Integer): ICSLEnumEveryNth;
    begin
    Result := TCSLEnum.Create(Self, Skip);
    end;

    with FslTest.CreateEnumerator(3) do
    while FindNext(s) do ln := ln + s;

    I've shortened the class names so it looks better in the comment box. Anyhow, note there's no factory class needed, which keeps the overall line count down. Also, what replaces the actual for/in construct takes the same number of lines (i.e., 2) that your original did.

    ReplyDelete
  2. Sure, but that way you cannot use the for..in :)

    I've always been an advocate of the school that says "write more code when preparing an infrastructure so you can write less code when using this infrastructure"

    ReplyDelete
  3. Anonymous12:06

    Very nice stuff, one question I have is will this work with BDS 2007, 8.. or is this an undocumented feature?

    thanks again
    Dave N.

    ReplyDelete
  4. This is all fully documented (I even quoted Delphi help in Part 1) and will continue to work in future Delphis.

    ReplyDelete
  5. I've always been an advocate of the school that says "write more code when preparing an infrastructure so you can write less code when using this infrastructure"

    Yeah, but in my version, the 'infrastructure' amounts to less code and the 'use' of it amounts to about the same... Anyhow, I'll be looking out now for part 5. You're going to be creating a class helper, I assume...

    ReplyDelete
  6. Of course :)

    With plenty of dire warnings.

    ReplyDelete
  7. Anonymous23:14

    This is a nice example, except that it isn't thread-safe. In fact, worse than that, it isn't even possible to have two enumerators (with different Skip factors) on the same string list at one time, even on a single thread (a for in loop nested within another for in loop).

    ReplyDelete
  8. To run multiple enumerators on one structure, you have to use enumerator factory and interfaces. See parts 4, 5, and 6 for more information.

    As for the thread-safety - threading is hard. My enumerator code doesn't try at all to be multithread-aware.

    ReplyDelete
  9. Anonymous10:39

    I don´t see why with this code it shouldn't be possible to have two enumerators (with different Skip factors) on the same string list at one time. The index counter is inside the enumerator and that is instantiated for every single for..in loop, meaning another counter for each loop. Where would be the problem?

    ReplyDelete
  10. Is the implementation of the TCSLEnumEveryNth constructor missing the "inherited Create;"? Or is it not needed in this case...

    ReplyDelete
  11. As suggested I made a enumerator that returns the specified class from a collection that supports :-

    for page in mBook.Filter< cPage > do begin
    for picture in page.Filter< cPhotoItem > do begin

    implemented :-

    cObjNode = class // base class for tree objects
    ....
    private
    oMakeEnum : TObject; // cMakeEnumMyList< T >;
    function FindMatch< T : class >( index : integer ) : integer;
    type // see http://www.thedelphigeek.com/2007/03/fun-with-enumerators-part-3.html Primož Gabrijelčič

    cEnumNode< T : class > = class
    constructor Create( owner: cObjNode );
    private
    mOwner : cObjNode;
    mIndex : integer;
    public
    function GetCurrent() : T; // [ index ]
    function MoveNext: boolean; // Inc( index ) if index
    property Current : T read GetCurrent;
    end;

    cMakeEnumNode< T : class > = class // as required by 'for in'
    constructor Create( owner : cObjNode );
    private
    mOwner : cObjNode;
    public
    function GetEnumerator: cEnumNode< T >;
    end;

    public
    function Filter< T : class >() : cObjNode.cMakeEnumNode< T >;
    end;


    ...

    function cObjNode.Filter< T >() : cObjNode.cMakeEnumNode< T >;

    begin
    oMakeEnum.Free; // free the previous one
    result := cMakeEnumNode< T >.Create( self );
    oMakeEnum := result;
    end;


    function cObjNode.FindMatch< T >( index : integer ) : integer; // filter logic

    begin
    result := ChildCount; // default to over the end
    while index < ChildCount do begin
    if oChildren[ index ] is T then begin
    result := index;
    break;
    end;
    Inc( index );
    end;
    end;


    constructor cObjNode.cEnumNode< T >.Create( owner: cObjNode );

    begin
    mOwner := owner;
    mIndex := -1;
    end;


    function cObjNode.cEnumNode< T >.GetCurrent() : T;

    begin
    if mIndex < mOwner.ChildCount then result := mOwner[ mIndex ] as T
    else result := nil;
    end;


    function cObjNode.cEnumNode< T >.MoveNext: boolean;

    begin
    mIndex := mOwner.FindMatch< T >( mIndex + 1 );
    result := mIndex < mOwner.ChildCount;
    end;


    constructor cObjNode.cMakeEnumNode< T >.Create( owner : cObjNode );

    begin
    mOwner := owner;
    end;


    function cObjNode.cMakeEnumNode< T >.GetEnumerator: cEnumNode< T >;

    begin
    result := cObjNode.cEnumNode< T >.Create( mOwner );
    end;



    ReplyDelete