0
\$\begingroup\$

I’m trying to implement, for educational purposes only, an example of the Postel’s law on a demonstrative function.

This function is not for production code, it’s for pedagogical purposes only and might be considered as an over abstraction to an easy to understand list comprehension. This isn’t the topic I’m interrested here, only type-hints and what can be infered from.

def update_prices(
    prices: typing.Iterable[numbers.Complex], 
    increase_percentage: numbers.Complex
) -> list[numbers.Complex]:
    return [price * (1 + increase_percentage / 100) for price in prices]

From my point of view, I think that

  • since the code only need the * and / implemented, even if a Real is expected, we could ask for a Complex, the super-type of a Real,
  • a / operator on a Complex should return a Complex, can’t we be more specific?

In python documentation, https://docs.python.org/3/library/numbers.html, Number is mentioned, is it better than Complex or Real?

Postel’s law or robustness principle: https://en.m.wikipedia.org/wiki/Robustness_principle

\$\endgroup\$
2
  • 2
    \$\begingroup\$ Why should a price or an increase ever be complex? \$\endgroup\$
    – Reinderien
    Commented Jan 21 at 21:42
  • \$\begingroup\$ @Reinderien I agree about the business case here, should be limited to int or float. But if we put that apart and focus on a protocol point of view ? \$\endgroup\$ Commented Jan 21 at 22:37

1 Answer 1

1
\$\begingroup\$

You are conflating LSP with the Robustness Principle. They are similar but distinct concepts.


Decades ago, when Postel worked at ISI, I was a big fan of Robustness.

But after {supporting, diagnosing, designing} systems implementing protocols like DNS, RIP, OSPF, BGP, FTP, SMTP, and HTTP(S), plus documents they transport, I find the principle can get in the way just as much as it helps.

If there are multiple ways to express e.g. "noon in London on January 21st", then a conforming implementation must be tested for each of those ways. And standards documents don't always supply comprehensive test vectors.

Keeping track of "peer A's bug prevents it from using standard format X" and "peer B's bug prevents it from using standard format Y" soon becomes tedious. And impossible to support, as bug fixes and new (buggy?) features are rolled out.

Only a small fraction of global email ever went through rfc822-compliant mail servers. Thank goodness for the rfc2822 update, which is simpler and more amenable to testing.

When designing a system that should be Robust, go for simplicity. There should be one obvious way to accomplish a use case. Demand that interoperating implementations all support some bare bones Profile. If you want to get fancy, specify additional negotiated superset profile(s), which are implemented entirely or not at all. The spec's appendix should include Normative test vectors for bakeoff testing.

If the spec says that a peer sending the Wrong Thing shall immediately tear down the session with fatal error, then implementors soon learn to send the Right Thing instead, and field testing will be less chaotic.


Liskov Substitution Principle is an OO concept about inheritance and contracts, which lets us prove theorems about code. Duck Typing is a concept about protocols and contracts, which lets an implementer worry about "what can this object do?", instead of "what is this object?". When authoring pythonic code we often try to accommodate duck typed collaborators.


review

signature

def update_prices(
    prices: typing.Iterable[numbers.Complex],  ...

This signature doesn't read smoothly. And since it was constructed for didactic purposes, readability is one of its principle goals.

Prefer:

from numbers import Complex
from typing import Iterable

def update_prices(
    prices: Iterable[Complex], ...

docstring

There isn't one.

So you are asking the function's name to do the heavy lifting. We are told the contract is to "update [inflate] prices" by some increase_percentage. (That last is a lovely identifier, and I thank you for it.) Absent some flowery language in a docstring about what a price is and what use cases might call for increasing them, we are left with the bog standard definition.

Now, I concede that one might choose to offer a paperback for sale according to this vector price:

    price = 10 + 10.91j

where to actually make the transaction with a business partner we would unpack like this:

    eur, usd = price.real, price.imag

But the docstring didn't spell out such details, so this is a POLA violation.

Now suppose that next year it sells for one-eighth more:

>>> price * 1.125
(11.25+12.27375j)

Piece of cake. Americans will spend more than twelve bucks, got it.

But wait, there's more!

The signature promised me that I can make complex increases. Let's suppose inflation is coming down and we can only manage an 8% U.S. increase. Easily computed, right?

>>> price * (1.125 + 1.08j)
(-0.5328000000000017+23.07375j)

Hold on, we're giving folks half a Euro to take the book off our hands? And Americans pay twenty-three bucks? OIC, should be much better when we use your formula.

>>> price
(10+10.91j)
>>> 
>>> price * (1 + (12.5 + 8j) / 100)
(10.3772+13.07375j)

Whoops, quite the bargain, still not satisfying that business use case.

Ok, I've had enough fun playing with your proposal. The code you submitted for review lacked several important elements:

  1. requirements document
  2. internal documentation
  3. test suite

We can skimp on those and get away with it, sometimes. But it's not a sure thing.


is [Number] better than Complex or Real?

No.

When authoring a function, only make promises you can keep. That is, promises which you have already verified, in your automated test suite, and which you are willing to take support calls for.

Suppose that update_prices, or some downstream consumer of its result, wants to round() a price to the nearest Euro. This works for Decimal, Fraction, float, and int. But it blows up horribly for Complex.

Even if it doesn't, when we ignore rounding, simple text formatting operations are likely to yield surprising results when diverse input types are attempted. Beyond exceeding a column width, we may run afoul of regexes that try to parse "numeric" data using too small of a character set.

Often a software engineer gets to design the rules a system will play by. The Robustness Principle asks you to write robust code. If you craft conservatively simple rules, your system will be more robust.

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for your feedback, I'll take that into account ! \$\endgroup\$ Commented Jan 22 at 17:06

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