4

I was recently tasked with thinking about returning optional tracing information about the execution of a complex algorithm. It's the kind of data you need to analyse the inner workings - a bit like logging of intermediate results but being programmatically available to the caller and optional.

I only had rather bad ideas that involve too many changes to the core algorithm, like passing additional parameters all the way through. Also I couldn't find anything on the web.

5
  • 3
    I'd just "log" normally, but to a String or StringBuffer or whatever your system supports, then make that available.
    – user949300
    Jun 16, 2020 at 19:47
  • 1
    What precisely is your idea about "additional parameters"? What kind of parameters? Why?
    – Doc Brown
    Jun 16, 2020 at 19:53
  • 2
    Write unit tests and use log levels appropriately.
    – svidgen
    Jun 16, 2020 at 19:54
  • Svidgen since the unit is the complex algorithm you may write tests but they wouldn’t be unit tests.
    – gnasher729
    Jun 17, 2020 at 5:19
  • thanks - I like the logging to StringBuffer. So I guess I'll try making them ThreadLocals to be able to use them in production with appropriate log-levels. I thought about logging but it never occurred to me to simply use a thread local string-appender. :facepalm
    – fricke
    Jun 17, 2020 at 8:38

3 Answers 3

5

A common pattern I've come across is to return a "Result" object from the algorithm rather than the raw data. This way you can include metatdata along with the data. To make it so that this change doesn't break your existing code, you can create an implicit operator for this result object, such that it can be used as the raw data type too.

My examples are in c#, but this could be in any language.

If your original function was something like:

float MyAlgorithm(int x, float y) {
   float result;
   //do some work
   return result;
}

then you consume the result like so:

float resultFromMyAlgorithm = MyAlgorithm(x,y);

you could do something this like this instead:

MyAlgorithmResult MyAlgorithm(int x, float y) {
   float result;
   //do some work

   MyAlgorithmResult resultObject = new myAlgorithmResult();
   resultObject.Value = result;
   resultObject.Status = "Success"; // here is your debugging info

   return resultObject;
}


public struct MyAlgorithmResult {
    public float Value;
    public string Status;

    public static implicit operator float(MyAlgorithmResult result) {
        return result.Value;
    }

}

Then, you can still consume the result object as a float, just like above. But now you can also check status and any other metadata you like.

1
  • In C# this is even a source-compatible change. Nice! Sometimes I miss the syntactic sugar of C# in my daily Java life... Nov 17, 2020 at 14:24
1

Adding a parameter is not changing an algorithm.

An example: Your complex algorithm is Quicksort. Usual call:

Quicksort (array, size)

As the caller I want to know for some reason in which order you created partitions. So I declare

Class partition { int left; int right; int pivot; }

I change the prototype to

Quicksort (array, size, vector <partition> *partitions = nil)

You add code that adds information to the partitions vector if it is not nil. And I call your code, optionally passing a partitions vector, and if I do so I can then examine the results at runtime. That’s what you were asked to do.

If your algorithm works by creating an object describing the problem and then calling a method that solves the problem described, then the logging / feedback parameter can be stored as an instance variable and doesn’t need passing around.

2
  • 1
    Hi, right it's not changing the algorithm, but making it more difficult to read - differentiate what's essential and what's a cross-concern. It also adds overhead, since it needs to be forwarded to all parts.
    – fricke
    Jun 17, 2020 at 8:33
  • Well, that’s life.
    – gnasher729
    Jul 17, 2020 at 21:26
1

a bit like logging of intermediate results but being programmatically available to the caller and optional

When you describe it like that, there's three possible bells that ring in my head:

  • Simple logging
  • Pub/sub
  • Callback/continuation methods

Since you describe "passing additional parameters all the way through" as a negative, that excludes using callback or continuation methods. Since you want the findings to be programmatically available, that excludes directly logging your findings to a log file. So we're left with a pub/sub system.

Pub/sub (or event-driven, which in this case is synonymous) systems allow for consumers to optionally subscribe to information. Have your object write its findings to an output feed, and give the consumer the option of subscribing to that feed. If not subscribed, nothing happens. If subscribed, the behavior you define will execute at the appropriate time.

Since your scenario is a small scale implementation, a simple event handler suffices here.

Note that for this example, the method is assumed to be int DoFoo(int) and the related event is therefore called FooDone. Adjust these names to fit your use case.

First, define your findings as event args, and add the properties to house the data that you want to pass. For this example, I assume that the input, output and a string-based explanation suffice.

public class FooDoneEventArgs : EventArgs
{
    public int InputValue { get; set; }
    public int OutputValue { get; set; }
    public string Reason{ get; set; }
}

Next, give your object an event handler that publishes its findings using these event args.

public class FooService
{
    public int DoFoo(int input)
    {
        int result;
        string reason;

        if(input > 5)
        {
            result = 123;
            reason = "the input is bigger than five";
        }
        else
        {
            result = 456;
            reason = "the input is smaller than or equal to than five";
        }

        OnFooDone(input, result, reason);

        return result;
    }

    public event EventHandler<FooDoneEventArgs> FooDone;

    public virtual void OnFooDone(int input, int output, string reason)
    {
        var handler = this.FooDone;
        if(handler != null)
        {
            handler(this, new FooDoneEventArgs()
            {
                InputValue = input,
                OutputValue = output,
                Reason = reason
            });
        }
    }
}

Thirdly, your service's consumer can optionally subscribe to this event:

public class Consumer
{
    private readonly FooService _fooService;

    public Consumer(FooService fooService)
    {
        _fooService = fooService;
        _fooService.FooDone += ReportFindings;
    }

    static void ReportFindings(object sender, FooDoneEventArgs e)
    {
        Console.WriteLine($"Inputting {e.InputValue} resulted in {e.OutputValue}, because {e.Reason}");
    }

    public void MyMethod()
    {
        // Your code that in some way uses the `DoFoo()` method, e.g.
        int result = _fooService.DoFoo(7);
    }
}

If you don't want your consumer to subscribe to the findings, just don't subscribe to the event. You can also make this a dynamic decision, only subscribing to the event when needed. The world is your oyster here.


I do want to take a moment here to point out that if the only goal is to log these findings so the developer can review them at their own leisure, then I would suggest you relax/scrap your "programmaticaly available" requirement, because logging your findings directly to a logger is significantly easier than setting up an event feed and having to programmatically handle the findings, especially if this kind of behavior repeats across your codebase.

When using logging, the optional nature of the logging can be implemented using log levels, which most if not all logging libraries natively support. In short, you simply register your findings at a certain level (e.g. trace seems the most appropriate level here), and then you can configure your logs and tell them to either do or don't capture log messages with a trace level.

You could, as is most commonly done, not capture trace logging by default, but if you ever need to investigate an issue in a deployed release, you can enable the capturing of trace logging temporarily, capturing the messages so you can understand what went on during the runtime. When the investigation is done, you revert back to no longer capturing the trace log messages.

For this, I suggest you look at any established logging library. It's hardly worth reinventing the wheel here.

1
  • the log-results would be exposed via an API so that it can be exposed to users in a UI. That's what I meant with 'programmatically available'. By the time I settled for an explicit accumulator where I can register certain events.
    – fricke
    Nov 18, 2020 at 13:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.