Adding manhattan_distance() to the built-in 2d vector type

I would like to propose adding a .manhattan_distance() method to the complex type, implemented as abs(self.real) + abs(self.imag). While the implementation is trivial, I believe that this useful and commonly reimplemented code.

I’ve been doing Advent of Code this year, and I’ve already had a few occasions to use a favourite trick: using complex as a 2d vector or coordinate type.

complex works really well for this purpose; It supports vector addition and subtraction, and multiplication or division by a scalar. It is immutable and hashable so can be used well with sets and dicts, and even has a literal syntax, making it easy to say, UP, DOWN, LEFT, RIGHT = -1j, 1j, -1, 1. It’s easy to get the length of the vector (abs()), and it’s even occasionally useful to be able to convert it to a polar representation (which is in cmath).

Just about the only thing that seems to come up regularly in these sorts of challenges which isn’t built in to either the type, or the cmath module is finding the Manhattan distance between two coordinates (or equivalently the Manhattan distance along a vector). It’s trivial code, but I’m pretty sure I’ve implemented it a number of times, and I’m sure many others have too.

The obvious objection is that complex is for representing complex numbers, and operations that don’t make sense on complex numbers don’t belong on it. I don’t know just how common it is to (ab)use complex as a generic 2d vector type, but it seems just fine to me, and I think it should probably be encouraged.

1 Like

Have you considered putting it in a function just once, and calling the function? Function vs method is just syntactic sugar (plus some dispatching logic that you don’t actually need in this situation), after all.

Karl is right. According to OO principles, it should be a function and not a method since it can be implemented using the public interface.

I don’t think this is a reasonable name for this function. I think cmath.abs2 would be much more reasonable. See the Array API discussion and the decision that it should go into a separate library of composite functions. Here are the historical numpy discussions: Ufuncs for complex numbers · Issue #13179 · numpy/numpy · GitHub and Please consider adding abs_square · Issue #19321 · numpy/numpy · GitHub.

If I’m not mistaken these are two different operations (although abs2 does sound like another appropriate addition).

I’d be fine with it being included as a function in the cmath module.

Oh, my mistake, I thought you were squaring. I misread. I think what you’re doing is usually called a Manhatten norm. I don’t think it’s useful enough with complex numbers to add. Or do you have any common examples?

Anyway, I think you should probably just use numpy if you’re allowed to. Then your operation is available as np.linalg.norm(..., axis=..., ord=1) or np.sum(np.abs(...), axis=...).

I must admit that I don’t have sufficient mathematical background to recognise the term “norm”, although googling does suggest that does seem to be a correct term for the concept, and if that naming is in-keeping with the rest of the library, I suppose it would be fine, although I’d hope the documentation would highlight what it’s useful for!

As I said in my initial post, the situation where I’ve seen this come up most frequently is in recreational programming challenges (like Advent of Code) - where the problem is unlikely to be posed in terms of complex numbers, but having a type that works as a 2d vector is extremely handy.

As for using numpy - I think in circumstances where you want this, it would be much easier to just implement the function by hand… once again.

Right, but what I’m saying is that irrespective of how the problem is (arbitrarily) posed, the concept that fits with what you’re trying to do is vectors—and not complex numbers. So I don’t think it makes sense to extend Python’s complex number interface. Instead, you should just use the vector interface.

Sorry, but I don’t agree with that.

That is the point: (unless I’m wildly mistaken) the complex number interface is the vector interface, at least when it comes to the built-ins and the standard library!

There is no vector interface in the stdlib/builtins. This is fully loaded off to third party libraries, like numpy, smaller dedicated libraries or as part of other libraries like pygame. A PEP to add a proper Vector type to the stdlib might be a good idea (and has probably already been done and rejected), but I don’t think adding stuff to complex numbers to simplify their “misuse” is a good idea.

1 Like

This seems like a particularly poor reason to add something to the language, honestly. “Specifically useful for toy examples” is the opposite of a broadly needed feature.

4 Likes

Yet another name for this is ℓ1-norm (and taxicab distance).

Apparently the BLAS specification calls this (confusingly) just “the absolute value of a complex number” https://linux.die.net/man/l/dcabs1 and other functions like “take the sum of the absolute values” https://linux.die.net/man/l/dzasum are apparently based on that as well.

(But that’s not really a reason for or against adding it.)

2 Likes

Thanks for all the replies. A few thoughts:

I think that if this were to be added, a function in cmath, rather than a method is certainly the right approach.

I think having read further on the topic, using the term taxicabnorm() is now my preferred name (assuming that people more knowledgeable agree). The ‘norm’ terminology is consistent with the character of the rest of the functions in this module, and I think that while ‘manhattan’ is perhaps more common than ‘taxicab’ in the discussions I’ve seen, both are commonly used, and using the name of a place seems odd in an API. Using terms like l1 seems unnecessarily obscure, from the perspective of the anticipated users. The alloneword spelling is for consistency with the rest of the module, although most other functions have shorter names.

I bristled slightly slightly at the implied suggestion that recreational programming isn’t a valid usecase for Python. Naturally it wouldn’t be sensible to over-optimise the language or library for it, but the fact that it’s possible to map interesting problems to powerful facilities in the language and get concise, clear solutions is one of the things that’s great about Python - in every domain. This is a case where there’s a tool that feels like it’s missing from the toolbox, is well-defined, imposes no cost on the people who don’t need it, costs little to add it, with very little reason to anticipate maintenance burden, or risk of future bugs. I know that it’s a slippery slope to start adding things just because it’s easy and someone wants it, but the slope slides the other way too; Python is not a language with a minimalist library, including only the things that were impossible to refuse.

I anticipated that people might object to the idea of adding an operation on the complex type which isn’t obviously useful when these objects are used to represent mathematical complex numbers. Some commenters seem to suggest that it is in some sense wrong to use complex in programs outside of that mathematical or engineering problem space, and you should use a type with the right name, adding 3rd-party dependencies if necessary.

I actually agree with this view, but only up to a point. Obviously it’s best to use the “right” type, if such a thing exists, especially if you’re working on a large or long-lived program. But unless you’re blindly translating formulae from a text-book, programming is problem solving. Choosing a representation, and selecting the types that have desirable properties and support useful operations is part of that process. “Abusing complex as a vector type” could also be called “defining a mapping to points on the complex plane”, if it makes you feel more comfortable.

After all, if you feel queasy at the idea of using complex for something that is naturally a vector, you are going to lose your mind when you see how the integers are used in typical programs!

I agree.

This is true; but notably, Python tends to offer tools to recreational programmers rather than prebuilt solutions. It all depends on how valuable some tool is, compared to how hard it is to roll your own.

There’ve been several references to complex vs vector. To be honest, I don’t see that there’s any significant difference between a complex number and a vector :slight_smile: Complex numbers are extremely useful for calculations involving 2D vectors. Could you also represent a 2D vector as a tuple of floats? Of course! And if you need to generalize to 3D, 4D, or any other type of vector, sure! But complex numbers work just fine too.

Notably, though, anyone who’s doing recreational programming in any sort of intensive way should probably be looking at numpy and scipy. So it’s a question of whether it’s worth adding this to the cmath module rather than having people use the facility from there (a quick Google search showed that it can be done here scipy.spatial.distance.cdist — SciPy v1.11.4 Manual - it’s called “city block” rather than “taxi-cab”, but “Manhattan” is mentioned in the docs).

I think Python is a great language for programming contests. Contests are actually my background (I did the IOI and the ACM over the years, and I coached an ACM team a couple times too). I absolutely love contests.

Yes, but I think that was a mistaken choice. You want to treat them as vectors, so in my opinion you should use vectors. Your choice to use complex numbers here was arbitrary. If the problem becomes three-dimensional, then what would you do? It is just the wrong mapping in my opinion.

The issue I have with adding this function is that it’s so arbitrary and niche. Taking the L1 norm of a complex number is extremely rare. A lot of the kinds of things you do with complex numbers are analytical functions, and the L1 norm is not analytical.

1 Like

Well in many cases the most obvious representation for a given problem might be a pair of integer variables, or perhaps two ints in a tuple. Since the machine operates purely on patterns of bits, choosing any particular data type is really a matter of convenience to you as the programmer. Sometimes choosing a representation that works well for your problem makes a very big difference indeed.

I’ve personally found using complex to be useful in a number of situations, allowing short, neat solutions quickly, while staying within the standard library, and allowing me to move on with my day. I’d recommend it, and I think the docs could usefully recommend it too. If the problem changes to require a 3rd dimension, a short, neat program is easier to rewrite, than a longer one, and at least I’m starting the process sooner.

But of course the proposed addition is a trivial function, that can be rewritten on demand in seconds. The time spent debating it is never going to be worth it. I think it’s pretty clear that there’s no enthusiasm for the idea. I’m not really sure whether this forum allows this idea to be conclusively accepted or rejected, or if they just hang around forever, but if anyone decides they’d welcome a PR to add this, let me know.

I love recreational programming, and I have programmed some truly silly things in my free time. I wasn’t suggesting it’s not a valid way to use Python. I just don’t think it’s a valid reason to add things to the standard library.

If you don’t want to use any packages to solve these challenges, that’s a constraint you’re putting on yourself, presumably for fun. Adding niche methods to get around a self-imposed challenge is counter-intuitive, to me.

2 Likes