9

I'm writing this library in which the user can provide custom code defining the algorithm used for finding an optimal solution. In the papers that I have read, the targeted user thinks in terms of finding the hyperplane that maximizes a measure of impurity. Since I want the user to articulate its code in a way that is consistent with their view of the problem, I created a struct hyperplane and a function double impurity(hyperplane* phyperplane) that should ease their writing of the algorithm.

However, inside the implementation of impurity, many intermediary expensive calculations that are of interest to the calling library code are performed. If the user-defined code only returns an instance of the struct hyperplane, the calling code will have to re-calculate these values once again.

I was thinking of adding a field representing each of these values to the definition of the struct hyperplane. This way, when impurity() is calculated, the intermediary values are stored in the instance returned to the calling library code. However, this will change both what struct hyperplane and impurity stand for. I could rename them to struct hyperplane_and_values and get_impurity_and_set_values respectively but this could risk confusing the end-user, which shouldn't be concerned with these details. On the other hand, keeping the original names seems like dirty code because it misleads the reader to what the named entities actually represent. Is there a "right" choice here?

1
  • 3
    Is there any way that the user-visible behavior of your "hyperplane with values" differs from the behavior of a hyperplane? If, from the user's perspective, it behaves like a hyperplane in every way, then I can't imagine how it could be harmful to call it a hyperplane. Commented Jul 6, 2023 at 20:44

4 Answers 4

14

This is why we invented the word meta. You're packing the result structure with meta info that isn't strictly part of what you're returning. But the info is about what you're returning.

Just carve out a meta section in the struct. Makes it clear this is extra incidental info that isn't the main focus of the struct or it's name.

1
  • 5
    Or, as a comment below suggests, you can flip these around. The funtion can return a HyperplaneCalculationResult, which contains a hyperplane member, and some other members. Commented Jul 6, 2023 at 15:48
7

I don't see why you consider it "dirty" to keep the original name, as that's exactly what OOP does - there's a public interface to an object, and there are some internal implementation details which the consumers of that object don't need to know about. But we still call it a string, we don't feel the need to call it string_with_values just because it carries around some stuff that isn't user-visible.

If you're in something like C, you can't enforce the private/public distinction, but you can do it via convention or via the simple means of never documenting the internal details.

4
  • 1
    My concern is that the original name does not give an honest description of what will the new struct represent. I'm not sure I'm familiar with the string example, but perhaps the stored values help define what the string is. In my case, the added values don't contribute to the definition of what a surface is, as understood from the perspective of the user. Their only purpose is to avoid repetetive calculations. Commented Jul 5, 2023 at 17:02
  • 7
    @MehdiCharife: You've got one idea on how a hyperplane type could be represented, the most barebones one. You've now come up with a second way to represent it, which tackles additional considerations. You don't need to be so rigid with your "my first idea is still valid and therefore no second idea could be correct" train of thought.
    – Flater
    Commented Jul 5, 2023 at 18:34
  • 7
    @MehdiCharife: ... but if you remain so inclined, HyperplaneCalculationResult would cover your concerns as to accounting for any additional values. Again, it's not necessary, but there you have it.
    – Flater
    Commented Jul 5, 2023 at 18:36
  • 3
    @MehdiCharife in some languages, the string type holds only characters and there's a special "end" signal value (usually a zero, not a '0') to indicate the end. Imagine an implementation that also holds an integer to tell it how long the string is, thus saving itself the trouble of traversing to find that information. This answer points out that when implementations do that, they still call the thing "string" even though it also holds some cached info for performance. (There are also string implementations that hold a length and don't use a stop value. This isn't that.) Commented Jul 6, 2023 at 22:23
3

It seems you are just creating a struct hyperplane which for some reason can do a particular calculation very fast. Because you were forced for some reason to do that calculation anyway and just cached the result. That’s perfectly fine, caching results happens all the time.

Maybe there are ways to create a struct without this extra data, then the same call requesting the result of the calculation would take a long time - and cache the result. And another call would be fast.

And you are not misleading anyone. The struct behaved exactly the way it should and the way it’s documented. The only difference is that one calculation is unexpectedly fast.

1

I understand your intent to be for the user of your library to write an optimisation algorithm along the following lines:

int converged;
struct hyperplane h;
double imp;

while (!converged) {
    generate_new_hyperplane(&h);
    imp = impurity(&h);
    converged = check_convergence(imp);  
}

You provide the hyperplane type and the impurity() function, and the user does the rest.

If I understand correctly, the impurity() function calculates the impurity given a particular hyperplane, and optimising that impurity solves some kind of problem. There are certain intermediate results of impurity() which depend only on the problem but not on the hyperplane, and which can therefore be reused in future calls to impurity(). Those need to be stored somewhere for efficiency reasons, and you propose putting them in the hyperplane struct.

I don't think that's a good idea, because there's nothing stopping the user from creating an entirely new hyperplane struct in between calls, thus deleting the cached data. So that's bad, but there's no way for the user to know that it's bad.

More generally, you have two related bits of data (the intermediate results and the information they're based on) stored in different places, and also two unrelated bits of data (the hyperplane and some intermediate results that aren't based on it) in a single place, and that's not good.

So instead, I would add another type struct problem (or whatever makes sense in the context), which stores the definition of the optimisation problem to be solved. This struct would then have space for cached calculations that depend only on the problem and not on the hyperplane, and the user would write their code as

int converged = 0;
problem p;
struct hyperplane h;
double imp;

init_problem(&problem, args...);

while (!converged) {
    generate_new_hyperplane(&h);
    imp = impurity(&p, &h);
    converged = check_convergence(imp);
}

Now the hyperplane only contains the hyperplane definition, and the problem object has the cached data and the data it's based on, making it easier to keep them in sync or at least invalidate the cache.

The struct problem here is really just a separate context object, which adds a bit of complexity to the user's code in the form of another variable to keep track of, but it makes for better modularity and avoids a potential leaky abstraction, so I think it's worth it here.

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