Wodzu
Wodzu

Reputation: 6979

How to pass generic procedure TProc<T1,T2> as a parameter and invoke it?

I have a logging class, which links to many modules. The main method of this class is a class method:

type
  TSeverity = (seInfo, seWarning, seError);

TLogger = class
  class procedure Log(AMessage: String; ASeverity: TSeverity);
end;

Somewhere else I have a function DoSomething() which does some things that I would like to log. However, I do not want to link all the modules of the logger to the module in which 'DoSomething()' is declared to use the logger. Instead I would like to pass an arbitrary logging method as a DoSomething's parameter and call it from its body.

The problem is that TLogger.Log requires parameter of TSeverity type which is defined in logger class. So I can't define a type:

type
  TLogProcedure = procedure(AMessage: String; ASverity: TSeverity) of Object;

because I would have to include an unit in which TSeverity is declared.

I was trying to come up with some solution based on generic procedure but I am stuck.

uses
  System.SysUtils;

type
  TTest = class
  public
    class function DoSomething<T1, T2>(const ALogProcedure: TProc<T1,T2>): Boolean; overload;
  end;

implementation

class function TTest.DoSomething<T1, T2>(const ALogProcedure: TProc<T1, T2>): Boolean;
var
  LMessage: String;
  LSeverity: Integer;
begin
  //Pseudocode here I would like to invoke logging procedure here.
  ALogProcedure(T1(LMessage), T2(LSeverity));
end;

Somewehere else in the code I would like to use DoSomething

begin
  TTest.DoSomething<String, TSeverity>(Log);
end;

Thanks for help.

Update

Maybe I didn't make myself clear.

unit uDoer;

interface

type
  TLogProcedure = procedure(AMessage: String; AErrorLevel: Integer) of Object;


// TDoer knows nothing about logging mechanisms that are used but it allows to pass ALogProcedure as a parameter.
// I thoight that I can somehow generalize this procedure using generics.
type
  TDoer = class
  public
    class function DoSomething(const ALogProcedure: TLogProcedure): Boolean;
  end;

implementation    

class function TDoer.DoSomething(const ALogProcedure: TLogProcedure): Boolean;
begin
  ALogProcedure('test', 1);
  Result := True;
end;

end.

Separate unit with one of the logging mechanisms.

unit uLogger;

interface

type
  TSeverity = (seInfo, seWarning, seError);

// I know that I could solve my problem by introducing an overloaded method but I don't want to
// do it like this. I thought I can use generics somehow.

  TLogger = class
    class procedure Log(AMessage: String; ASeverity: TSeverity); {overload;}
    {class procedure Log(AMessage: String; ASeverity: Integer); overload;}
  end;

implementation

class procedure TLogger.Log(AMessage: String; ASeverity: TSeverity);
begin
  //...logging here
end;

{class procedure TLogger.Log(AMessage: String; ASeverity: Integer);
begin
  Log(AMessage, TSeverity(ASeverity));
end;}

end.

Sample usage of both units.

implementation

uses
  uDoer, uLogger;

procedure TForm10.FormCreate(Sender: TObject);
begin
  TDoer.DoSomething(TLogger.Log); //Incompatible types: Integer and TSeverity
end;

Upvotes: 4

Views: 3345

Answers (2)

user3392332
user3392332

Reputation:

As David Heffernan says, you cannot use generics in this way. Instead you should use a function to map the error level to a severity type, and use that to glue together the two. Based on your updated example, one could modify it like this:

unit uDoer;

interface

type
    TLogProcedure = reference to procedure(const AMessage: String; AErrorLevel: Integer);


// TDoer knows nothing about logging mechanisms that are used but it allows to pass ALogProcedure as a parameter.
type
    TDoer = class
    public
        class function DoSomething(const ALogProcedure: TLogProcedure): Boolean;
    end;

implementation    

class function TDoer.DoSomething(const ALogProcedure: TLogProcedure): Boolean;
begin
    ALogProcedure('test', 1);
    Result := True;
end;

end.

You can then provide the glue procedure which converts the error level to a severity:

implementation

uses
    uDoer, uLogger;

function SeverityFromErrorLevel(const AErrorLevel: Integer): TSeverity;
begin
    if (AErrorLevel <= 0) then
        result := seInfo
    else if (AErrorLevel = 1) then
        result := seWarning
    else 
        result := seError;
end;

procedure LogProc(const AMessage: String; AErrorLevel: Integer);
var
    severity: TSeverity;
begin
    severity := SeverityFromErrorLevel(AErrorLevel);

    TLogger.Log(AMessage, severity);
end;

procedure TForm10.FormCreate(Sender: TObject);
begin
    TDoer.DoSomething(LogProc);
end;

Note I didn't compile this, but the essence is there. I used a procedure reference (reference to procedure) as they're a lot more flexible, which may come in handy later.

Upvotes: 0

David Heffernan
David Heffernan

Reputation: 612993

Introducing generics here does not help. The actual parameters that you have are not generic. They have fixed type, string and Integer. The function you are passing them to is not generic and receives parameters of type string and TSeverity. These types are mis-matched.

Generics won't help you here because your types are all known ahead of time. There is nothing generic here. What you need to do, somehow, is convert between Integer and TSeverity. Once you can do that then you can call your function.

In your case you should pass a procedure that accepts an Integer, since you don't have TSeverity available at the point where you call the procedure. Then in the implementation of that procedure, where you call the function that does accept a TSeverity, that's where you convert.


In scenarios involving generic procedural types, what you have encountered is quite common. You have a generic procedural type like this:

type
  TMyGenericProcedure<T> = procedure(const Arg: T);

In order to call such a procedure you need an instance of T. If you are calling the procedure from a function that is generic on T, then your argument must also be generic. In your case that argument is not generic, it is fixed as Integer. At that point your attempt to use generics unravels.


Having said all of that, what you describe doesn't really hang together at all. How can you possibly come up with the severity argument if you don't know what TSeverity is at that point? That doesn't make any sense to me. How can you just conjure up an integer value and hope that it matches this enumerated type? Some mild re-design would enable you to do this quite simply without any type conversions.

Upvotes: 2

Related Questions