Design or Code Pattern to return multiple optional values from a function

605 views Asked by At

I keep writing several utility, reusable functions in C#. Many functions return multiple values for which I use out or ref parameters. Many functions also have additional information, which may be of use to some callers (but not all callers).

Example, a function to read a CSV file may have additional information like no. of blank lines, no. of lines with duplicate values, and some other statistics.

The additional information could also include warnings, messages, etc.

Not every caller will be interested in this information, so I don't want to include all these as out, ref or Tuples, which will make it mandatory for the caller to declare all such expected variables.

I just wanted to figure out if there is a way to make the additional information available to the callers, so that the caller can choose or retrieve some of the optional additional information it is interested in.

For example, Func 1 can call Func B. After calling it, it gets all the standard return values. Additionally, can it call something like FuncB.GetAdditionalInfo(infoType) without Func B getting executed again?

It may be possible to design this using a class which serves as an intermediary to store all the optional values as well, and then return them to the caller on request; but I want it to be generic enough to be used across all my utility functions.

One possibility is Func B storing all these in some kind of global variables, which the caller can access if required. But if a utility class has several such resuable functions, I will need to have so many public variables, for the additional info of each function!

I am on .NET 4.5 as of now. Is there a design pattern for this? I am open to know if there is a good solution in F#.

Also, I want to avoid too many overloaded versions to achieve this! Thanks.

3

There are 3 answers

0
boromak On

I do not contend to present you with the ideal implementation, but here is one that makes sense to me. Design two different data structures: one would represent the options that your function accepts and the second one would be the one that your function returns. For example:

public class Helper
{

    // General cover-it-all implementation that accepts an option object
    // and analyzes based on the flags that are set in it
    public static CSVStatistics AnalyzeCSV(string file, CSVAnalysisOptions options)
    {
        // define what we are analysing by reading it from the
        // from the options object and do your magic here
    }

    // Specific implementation that counts only blank lines
    public static long CountBlankLines(string file)
    {
        var analysisResult = AnalyseCSV(file, new CSVAnalysisOptions
        {
            IsCountingBlanks = true
        });
        //I'm not doing a null check here, because I'm settings the
        //flag to True and therefore I expect there to be a value
        return analysisResult.BlanksCount.Value;
    }
}
// Analysis options structure
public struct CSVAnalysisOptions
{
    public bool IsCountingBlanks { get; set; }
    public bool IsCountingDuplicates { get; set; }
    public bool IsCountingOther { get; set; }
}

// Analysis results structure
public struct CSVStatistics
{
    public long TotalLineCount { get; set; }
    public long? BlanksCount { get; set; }
    public long? DuplicatesCount { get; set; }

}

In the above example CountBlankLines is a specific implementation that counts only blank lines and acts as "sugar" that simplifies the call, while AnalyzeCSV is the method that actually will do the counting. Also, notice how the CSStatistics structure has nullable longs. This will allow you to check if a value is null and therefore know that it was not actually analysed instead of outputting a zero (which is a possible value).

The CSVAnalysisOptions structure could also be replaced by bit flags, you can read about them here - https://www.dotnetperls.com/enum-flags.

3
plinth On

I feel like what you're trying to do is to build a very chunky API that can do a whole lot of things in one shot. Generally speaking we don't like chunky API's because they can get complicated especially if there are side-effects or unusual quirks in the interactions between options in the API.

Honestly, the best way to do this is to create a chattier API wherein each call does one thing, does it right and does it well.

When you do this, the code ends up being easier to factor and unit test.

That's not to say that there isn't cause for a moderate amount of chunkiness, but it should be logical.

So for example, if you're cracking an image file to decode, say, a PNG or a JPG, you will need the image width, height, resolution, and color type up front. It would make total sense to grab all of those in one go. Would you need to dig out metadata information or the color profile right away? Probably not.

So it would make sense to have a single call that returns and aggregation of all the basic image information and then separate calls for getting the rest.

"But performance!" you say, "what about performance?!"

Simple. You measure it and see what falls out. A few years ago a wrote a PNG decoder and unlike libpng which reads chunks sequentially, I thought it would be easier to just build a database up front that maps where every chunk of the file is, then refer to that database to find any given chunk. Surprisingly enough, this impacted performance in no significant way and made the consuming code so much easier to read and maintain.

Let things get called multiple times and if there is a performance issue, figure out how to address it. Typically, you do this with a cache or session private to the objects that get the information.

What you're describing sounds like it would be neither easy to read nor to maintain, let alone to test.

0
rmunn On

Here's an F# pattern that might be suitable for your use case. It's pretty much the same pattern that the Argu library uses for command-line arguments: you declare a discriminated union that contains all the possible "flags" that your function might want to return (I put "flags" in quotation marks because some of them might be more than just booleans), and then your function can return a list of those values. If there are dozens, then a set might be worth it (because a list has to be searched linearly), but if you don't expect to return more than seven or eight such flags, then the extra complexity of a set isn't worth it and you might as well use a list.

Some F# code illustrating how you might use this pattern, with dummy functions where your business logic would go:

type Notifications
    | InputWasEmpty
    | OutputWasEmpty
    | NumberOfBlankLinesInOutput of int
    | NumberOfDuplicateLinesInOutput of int
    | NumberOfIgnoredErrors of int
    // Whatever else...

type ResultWithNotifications<'a> = 'a * Notifications list
// The syntax "TypeA * TypeB" is F# syntax for Tuple<TypeA,TypeB>
// And the 'a is F# syntax for a generic type

type outputRecord = // ... insert your own data type here

// Returns the filename of the output file, plus various notifications
// that the caller can take action on if they want to
let doSomeProcessing data : ResultWithNotifications<outputRecord list>
    let mutable notifications = []
    let outputFileName = getSafeOutputFilename()
    if List.isEmpty data then
        notifications <- InputWasEmpty :: notifications
    let output = data |> List.choose (fun record ->
        try
            let result = record |> createOutputRecordFromInputRecord
            Some result
        except e
            eprintfn "Got exception processing %A: %s" record (e.ToString())
            None
    )
    if List.isEmpty output then
        notifications <- OutputWasEmpty :: notifications
    if List.length output < List.length data then
        let skippedRecords = List.length data - List.length output
        notifications <- (NumberOfIgnoredErrors skippedRecords) :: notifications
    // And so on. Eventually...
    output |> writeOutputToFilename outputFileName
    outputFileName, notifications  // Function result

Hopefully the F# code is comprehensible without explanation, but if there's anything that isn't clear in the above, let me know and I'll try to explain.