4

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;
Wodzu
  • 6,932
  • 10
  • 65
  • 105
  • You don't need generics because you know the type of the parameters. The first is a string, the second an integer. – David Heffernan Sep 28 '16 at 11:49
  • But the TSeverity and Integer are not compatibile so the procedure signatures will be different, Delphi will not allow me to pass procedure(A: String; B: TSeverity) as a parameter of DoSomething() which expects a procedure of signature: procedure(A: String; B: Integer) – Wodzu Sep 28 '16 at 11:55
  • 1
    Is your parameter and integer or a TSeverity – David Heffernan Sep 28 '16 at 11:56
  • you can always cast the integer to the enum but that would defeat the purpose of the enum. I don't really see the problem here, you are adding a depency in the form of logging and this means including the logger unit to the uses clause... – whosrdaddy Sep 28 '16 at 12:00
  • My procedure `DoSomething()` expects a procedure of type: `(AMessage: String; ASeverity: Integer)` but I would like to pass to it a procedure of type `(AMessage: String; ASeverity: TSeverity)`. That is why I've tried to do it via generics, and somehow convert `Integer` to `TSeverity` inside of `DoSomething()` – Wodzu Sep 28 '16 at 12:00
  • @whosrdaddy the problem is that I don't want to add this dependency, I've stated this clearly in my question: _I do not want to link all the modules of the logger_ . I want to cast integer to the enum but I don't want to know the exect type, because that would require me to include the unit in which type is declared. I would like to cast integer to a parametrized type instead. – Wodzu Sep 28 '16 at 12:02
  • Then I would take a look at [AOP](https://en.wikipedia.org/wiki/Aspect-oriented_programming). Spring4D has some examples concerning AOP and logging. – whosrdaddy Sep 28 '16 at 12:15
  • @whosrdaddy Thanks but that is beyond the scope of my question. – Wodzu Sep 28 '16 at 12:28
  • 5
    *I don't want to add this dependency.* Fine. Then pull the definition of `TSeverity` out of the logging modules, put it in a separate module, and use that new module in both places. It's now no longer part of the logging module, and so you don't have that dependency. – Ken White Sep 28 '16 at 12:37
  • Generics aren't any use here. You know what the types are. Generics aren't a means for you to avoid type safety. – David Heffernan Sep 28 '16 at 12:38
  • @DavidHeffernan Please allow me to decide what I want to do with generics. I am only asking is it doable or not? – Wodzu Sep 28 '16 at 12:40
  • 1
    @Wodzu Sure, you can decide what you want, I'm just telling you that your decision won't lead you anywhere. – David Heffernan Sep 28 '16 at 13:04

2 Answers2

2

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.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Please take a look at my updated answer, maybe now it will be clearer. – Wodzu Sep 28 '16 at 13:29
  • 1
    The question was perfectly clear before, perhaps you might re-read my answer and see if it becomes clearer to you. If you are dead set on using generics, go ahead and do so. – David Heffernan Sep 28 '16 at 13:30
  • I've re-read it couple of times and I know that casting Integer to TSeverity is unsafe. I just wanted to use the generics here as an excercise but I am stuck, that is why I've asked the question. – Wodzu Sep 28 '16 at 13:38
  • Generics won't get you around that unsafety. Either you make `TSeverity` available, or you convert. How can generics help with either of those? But again, you don't seem ready to consider that generics might not be the solution to your problem. When I first said so, you told me not to make such suggestions, and that if you wanted to solve the problem with generics, then you would do so. Frankly I think you need to open your mind a little. – David Heffernan Sep 28 '16 at 13:46
  • I thought I can cast Integer to T2 somehow which would be a TSeverity. – Wodzu Sep 28 '16 at 13:49
  • I already said that generics don't provide a mechanism to arbitrarily convert between two types. I'm hesitant to state that again, because the last time I did, you didn't appreciate that suggestion. – David Heffernan Sep 28 '16 at 13:50
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124429/discussion-between-david-heffernan-and-wodzu). – David Heffernan Sep 28 '16 at 13:53
  • @Dsm Really? T2 could be anything. A huge record. A pointer. An interface. No conversions to those make any sense. Your case statement is no better because whilst you can switch on an integer, what value of T2 do you use? After all in a generic method, you don't know what type T2 is. – David Heffernan Sep 28 '16 at 15:25
  • @DavidHefferman yes, a case statement can be made safe in the sense that you can restrict to valid values of T2 and raise an exception otherwise. It doesn't matter what the integer refers to in that context. Safety and sensible are not the same thing and if you read my reply I said that you could *not* cast safely. – Dsm Sep 28 '16 at 15:36
  • As far as T2 can be anything - we know that in this case it is not. – Dsm Sep 28 '16 at 15:42
  • @Dsm You also are missing the point. You know what T2 is. But the compiler does not. It is an unconstrained generic. It can be anything. The compiler has to cope with that. – David Heffernan Sep 28 '16 at 15:47
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/124439/discussion-between-dsm-and-david-heffernan). – Dsm Sep 28 '16 at 15:56
0

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.