Composing Functions

compose = lambda f, g: lambda x: f(g(x))

# So now if I go:

def S(x): return x * x
def R(x): return x + (1/2)*x

# I can then build new functions:

print("="*10)
H = compose(S, R)
J = compose(R, S)

print(H(10))
print(J(10))

class Compose:
  """make function composible with multiply"""
  def __init__(self, f):
    self.func = f
  def __mul__(self, other):
    return Compose(lambda x: self(other(x)))
  def __call__(self, x):
    return self.func(x)
    
S = Compose(S)  # <--- this is what @decorator does
R = Compose(R)  # <--- R is replaced with its proxy

print("="*10)
# multiply to compose
H = S * R  
J = R * S  

print(H(10))
print(J(10))


print("="*10)
# decorate to compose

@Compose
def addK(s):
  return s + "K"
  
@Compose
def addM(s):
  return s + "M"

# addM and addK are actually Compose type thanks to decorator
fun_func = addM * addM * addM * addM * addK # multiply!
print("Test decorated versions: ", fun_func("O"))