TL;DR Such composition is a primitive (in the sense that it cannot be decomposed into other higher-order functions) operation that neither Python nor the tools
module supports. You need to implement it yourself.
What you are missing (or rather, what Python and the tools
module are missing) is the notion of an applicative functor. To understand what that means, let's first review two functions in the tools
module:
compose
lets you chain together two functions. That is,
compose(f,g) == lamba x: f(g(x))
curry
is related to partial application: a demonstration will be quicker than an explanation:
curry(f)(x)(y) == f(x, y)
That is, curry(f)(x)
is basically the same as partial(f, x)
; both take a value y
to return the value f(x, y)
.
In addition, a functor is basically a way to map a function over some value. You are no doubt familiar with the list functor:
map(f, [a,b,c]) == [f(a), f(b), f(c)]
Functions are also functors, but instead of map
, we use compose
. That is, mapping f
over g
produces compose(f, g)
.
Now, to combine mul
, f1
, and f2
into g = lambda x: g(f1(x), f2(x))
, it seems like both compose
and curry
would be useful. To wit,
lambda x: mul(f1(x), f2(x)) == lambda x: curry(mul)(f1(x))(f2(x))
and
lambda x: mul(f1(x), f2(x)) == lambda x: compose(curry(mul), f1)(x)(f2(x))
(That is, curry
is what allows us to compose a two-argument function with another function.)
But composition is, in some sense, strictly a linear operation; the input of one function comes from the output of another. The composition of mul
and f1
creates a function that expects an argument and returns a function that expects that same argument. How do we move x
out of the "middle" of either expression? What we need is some
mystery function foo
such that
foo(f, g) = lambda x: f(x, g(x))
that makes a function that passes its argument to both f
and g
, while also passing the result g(x)
to f
. With such a function foo
, we could write
lambda x: foo(compose(curry(mul), f1), f2)
and get our desired result.
And that brings us to the idea of an applicative functor. It provides the necessary function foo
def foo(f, g):
def _(x):
return f(x, g(x))
that combines the notions of composition and currying that we don't currently have.
In other words, foo
is a distinct primitive operation; you can't implement it in terms of composition itself.