HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonModerate

Ackermann function

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
ackermannfunctionstackoverflow

Problem

I just came across the Ackermann–Péter function a few weeks ago, found it to be very interesting, and decided to write a simple implementation of it in a favorite programming language of mine.

def ackermann_peter_10(m, n):
    assert isinstance(m, int) and m >= 0, 'm is unacceptable'
    assert isinstance(n, int) and n >= 0, 'n is unacceptable'
    stack = []
    while True:
        if not m:
            if not stack:
                return n + 1
            m, n = stack.pop(), n + 1
        elif not n:
            m, n = m - 1, 1
        else:
            stack.append(m - 1)
            n -= 1


Can you refactor ackermann_peter_10 any further? It surprised me to see how similar the code is to another version found on Stack Overflow.

Reference: Simple loop Ackermann function

Solution

Assert statements are a convenient way to insert debugging assertions into a program

Assert statements are not for what you think they are for.
Don't use them, instead raise explicit exceptions:

def ackermann_peter_10(m, n):
    if not (isinstance(m, int) and m >= 0):
        raise TypeError('m should be an integer')
    if not (isinstance(n, int) and n >= 0):
        raise TypeError('n should be an integer')
    stack = []
    ...


If you are confused about why assert statements are bad here, it is because there can be errors if your code is run out of debug mode, also known as optimised mode, via the -O flag.
This is because assert statements are ignored when you are not in debug mode.

After reading @TLW's comment,
you may want to know why LBYL is a bad idea in Python,
and why EAFP is better.

Python uses duck typing,
this means that it categorizes mallards and ducks as 'ducks',
where a mallard is not a duck.
I.e. both float and int are numbers and different types.

Currently floats are mallards to your function,
they can work since they're pretty much ints,
and so should be classed as a 'duck'.
For example 3. + 1, a simple typo, can cause your function to error on what would be valid input.

To allow floats you can convert the input to an int.
The function won't run if the input cannot be converted,
as an error will be raised.
Which is an EAFP approach.

But there is still a mallard! As stated by @TLW:


This function will throw with any user-defined types, when the function itself would work just fine

Your end user creates the following class:

class Num:
    def __init__(self, num):
        self._num = num

    def __add__(self, other):
        return Num(self._num + other)

    def __iadd__(self, other):
        self._num = self._num + other
        return self

    def __sub__(self, other):
        return Num(self._num - other)

    def __str__(self):
        return 'Num ' + str(self._num)

    def __bool__(self):
        return bool(self._num)

n = Num(0)
print(n)
print(not not n)
n += 1
print(n)
print(n + 1)
print(n - 1)
print(not not n)
# Errors
print(int(n))


As you can guess, it's a number, a 'duck', But it's not an int, a duck.
And the above solution of using int to make it a duck doesn't work.
But it works in your function!

This raises the question, 'what is a 'duck'?'
But figuring out what a 'duck' is can be odd,
for example in this function you would need to do:

try:
    m + 1
    m - 1
except TypeError:
    raise TypeError('m should be a number')


But, when the code is more complex, this could not be a viable option, or not fulfil all the requirements for a 'duck'.
For example, remove __bool__ from Num, and the function will go into an infinite loop.
Do you know how to catch that? Do you know you should catch that? Are there edge-cases?
A simple way to solve this is to instead use an EAFP approach.
Just hope that it's a 'duck', if it's not your user will notice and fix the input.

def int_(n):
    try:
        return int(n)
    except TypeError:
        return n

def ackermann_peter_10(m, n):
    m = int_(m)
    n = int_(n)
    stack = []
    ...


Otherwise, with @oliverpool's changes, your code seems good.

Code Snippets

def ackermann_peter_10(m, n):
    if not (isinstance(m, int) and m >= 0):
        raise TypeError('m should be an integer')
    if not (isinstance(n, int) and n >= 0):
        raise TypeError('n should be an integer')
    stack = []
    ...
class Num:
    def __init__(self, num):
        self._num = num

    def __add__(self, other):
        return Num(self._num + other)

    def __iadd__(self, other):
        self._num = self._num + other
        return self

    def __sub__(self, other):
        return Num(self._num - other)

    def __str__(self):
        return 'Num ' + str(self._num)

    def __bool__(self):
        return bool(self._num)

n = Num(0)
print(n)
print(not not n)
n += 1
print(n)
print(n + 1)
print(n - 1)
print(not not n)
# Errors
print(int(n))
try:
    m + 1
    m - 1
except TypeError:
    raise TypeError('m should be a number')
def int_(n):
    try:
        return int(n)
    except TypeError:
        return n

def ackermann_peter_10(m, n):
    m = int_(m)
    n = int_(n)
    stack = []
    ...

Context

StackExchange Code Review Q#118063, answer score: 15

Revisions (0)

No revisions yet.