Hyperoperations Implementation in Python, Part 2.
If you missed Part 1, see it here:
In the previous article, I covered the creation of summative Hyperoperators based on simple operations of a unary representation. This included the functions increment, add, multiply, power and tetrate. Next we'll look at subtractive Hyperoperators, i.e. decrement, subtract, divide, log and superlog.
The Code
Decrement
Here is decrement:
def dec(self):
if self.compare(n_.zero) == "equal":
raise UnderflowError("Oops. You tried to decrement a zero. Just natural numbers for now.")
return Number(self.state[1:])
First, we ensure we are not at zero. For our current Number implementation, we're only handling Natural numbers, i.e. positive integers starting at zero. At some point it is possible to add support for the Integer numbers, i.e. include negative numbers. But for now, we raise an exception if we'll go negative.
And then to decrement, we simply return the part of the unary list starting with the second element (index=1) onward. This is similar to Lisp the cdr function. This entire representation lends itself very well to a Lisp implementation. But I don't know anyone who uses Lisp regularly who doesn't have a graduate degree, so we'll keep things a little more accessible.
Subtract
Here is subtract:
def sub(self, s):
result = self
for _ in s.state:
if result.compare(n_.zero) == "equal":
raise UnderflowError(
"Oops. You tried to subtract a larger number from a smaller one. Just natural numbers for now.")
result = result.dec()
return result
We start with the current state of the Number (minuend), and for each unary digit of the parameter (subtrahand), we set the result state to be decremented once. (minuend ? subtrahend = difference) If we get to zero ad try to decrement, raise an exception. I chose to compare rather than just decrementing zero and letting the exception happen so that we can give a little more information. Maybe I should have added the input values to make the message better?
The result is the difference.
Divide
Here is divide:
def div(self, denominator):
numerator = Number(self.state)
divisions = n_.zero
if denominator.compare(n_.zero) == "equal":
raise ZeroDivisionError()
while True:
progress = n_.zero
for _ in denominator.state:
if numerator.compare(n_.zero) == "equal":
return divisions, progress
numerator = numerator.dec()
progress = progress.inc()
divisions = divisions.inc()
It's pretty similar to subtract, but we have to handle a couple extra cases. First, we disallow divide by zero. Looking at the subsequent implementation below, we can see why. We loop over decrements of the numerator by the denominator and count how many times (divisions) we succeed fully subtracting the denominator. We also keep track of partial decrements (progress) so that when we exhaust the numerator, we can return both the division count as well as the remainder (progress).
Log
Here's log:
def log(self, base)
numerator = Number(self.state)
if base.compare(n_.zero) == "equal":
if self.compare(n_.zero) == "equal":
return n_.one, n_.zero
if self.compare(n_.one) == "equal":
return n_.any, n_.zero
raise UndefinedError("0^N where N!=[0,1] is always zero, so log base N cannot be calculated.")
if base.compare(n_.one) == "equal":
if self.compare(n_.one) == "equal":
return n_.any, n_.zero
raise UndefinedError()
magnitude = n_.zero
while True:
numerator, _ = numerator.div(base)
if numerator.compare(n_.zero) == "equal":
remainder = self.sub(base.pow(magnitude))
return magnitude, remainder
magnitude = magnitude.inc():
Structurally it is also very similar, but in addition to disallowing division by zero, we also handle base cases where the base of the log is 0 or 1.
Logˇ0(0) is one due to our definition of X^0 = 1 for all x. This is not a universal definition, but falls out of our choice of power implementation. So we'll follow suite in log to be consistent.
Logˇ0(1) is the special value "any" since any value raised to the power zero power equals one. We define a special type of value that represents "any" value. One may imagine a typical way to handle this is to just raise UndefinedError, but we can do better. The result is a value that is sort of a flexible or special number.
Value, Special and Any
Let's define a new type that both the special values and numbers are derived from called Value.
class Value:
pass
We don't have any need for any specific function here. But we'll redefine Number to subclass it:
领英推荐
class Number(Value):
"""
A number class with unary internal representation that attempts to implement
Now we can make Special that defined some special numbers:
class Special(Value):
class special_types(Enum):
any = auto()
def __init__(self, stype):
if not isinstance(stype, self.special_types):
raise Exception("To make a special, you must say the type, e.g. Special.types.any")
self.stype = stype
def compare(self, value):
if not isinstance(value, Special):
return "not equal"
if value.stype == self.stype:
return "equal"
return "not equal"
So far all we have is "any". But we can compare other things to it and also ensure any subsequent Specials are distinct from each other.
Now the line in the log function makes more sense:
if self.compare(n_.one) == "equal":
return n_.any, n_.zero
Moving on to the base one case...
Logˇ1(n) is has two cases itself. If n is anything other than 1, we return an undefined error since 1^M can never be anything other than one. It's not obvious how we can raise one to a any power and get something other than a one. So we return UndefinedError.
For the 1^N=1 case, any number will return the value.
if base.compare(n_.one) == "equal":
if self.compare(n_.one) == "equal":
return n_.any, n_.zero
raise UndefinedError()
If we are trying to find the log() base one of something, if the number is one, we return "any" (with a zero mantissa). Otherwise it is an UndefinedError exception.
With the more normal rest of the log function we see:
magnitude = n_.zero
while True:
numerator, _ = numerator.div(base)
if numerator.compare(n_.zero) == "equal":
remainder = self.sub(base.pow(magnitude))
return magnitude, remainder
magnitude = magnitude.inc()
This is similar to subtract, and moreso division with us returning the integer remainder (mantissa) for anything left after the last division.
Superlog
The final operation, the inverse of tetration, is superlog:
def superlog(self, base)
if base.compare(n_.zero) == "equal":
if self.compare(n_.zero) == "equal":
return n_.any, n_.zero
raise UndefinedError()
if base.compare(n_.one) == "equal":
if self.compare(n_.one) == "equal":
return n_.any, n_.zero
raise UndefinedError()
height = n_.one
log_progress = Number(self.state)
curr_base = base
while True:
log_progress, _ = log_progress.log(curr_base)
if log_progress.compare(n_.zero) == "equal":
remainder = self.sub(base.tetr(height))
return height, remainder
height = height.inc()
curr_base = curr_base.mul(curr_base):
Conceptually this counts how many times you can repeatedly log something before getting to zero. Another way to think about it is if you raised a base to the power N, multiple times, how call would the tower of ^'s be. For example, using tetration notation: 2^^4 = 2^2^2^2 = 2^2^4 = 2^16 = 65536
So super log of 65536 base 2 is 4 or 65536.superlog(2) = 4
Moving on to the special cases...
Zero to the zero power in some literature is undefined and in others equal to one. But here, it just falls out of the code very simply, so it is one. For the inverse operation we can use the same definition for consistency. For any other values of superlog base 0, we return throw UndefinedError.
For the case of base 1, we have another special case for superlog base 1 of 1. Superlog base 1 of 1 asks the question how many times do we need to raise 1 to the power of to get 1. The answer is of course, any number, so we use our special number any again (with a zero remainder).
The rest is pretty straightforward and counts the times we can iteratively log base N and then reports that number plus the remainder.
Wrapping Up
That finishes up the definition of Superoperators up to N=4 in both incremental and decremental operations. The results are a bit messy with the special cases. But they are needed for consistency.
So, what's next? Basic arithmetic operations and chained operations are good for static calculations. The next article will focus on using the Numbers and Superoperators in expressions with the goal being to see how difficult it is to implement some basic algebraic operations.