Combine m-ary function with n-ary function in a single (m+n)-ary function returning the pair of their results

96 views Asked by At

I have no idea how useful the application would be, but I got curious about it because of this C++ answer to a question of mine.

So, given, say, a ternary f and a binary g, e.g.

f x y z = x + 10*y + 100*z
g x y = x + 10*y

how can I get a function h such that the following holds?

h f g 1 2 3 4 5 == (321, 54)

Clearly I can define

h f g = \x y z v w -> (f x y z, g v w)

But I was curious to know if it could be done in point-free style for every arities m and n, using some existing abstraction.

For this specific example (m == 3 && n == 2) pointfree.io shows that the result becomes unreadable:

h = flip . ((flip . ((flip . (((.) . (.) . (,)) .)) .)) .)

but I'm still curious if something else exists.

For fun, I've tried to apply combinatory logic mecanically to derive a formula, but still only for the specific case of m == 3 && n == 2:

b = (.)  -- bluebird (names from "To Mock a Mockingbird")
c = flip -- cardinal (names from "To Mock a Mockingbird")
p = (,)
f x y z = x + 10*y + 100*z
g x y = x + 10*y
{-
p(fxyz)(gvw) = ?xyzvw, with ? in terms of p, f, g and known birds
(p(fxyz))(gvw)
B(B(p(fxyz)))gvw
CBg(B(p(fxyz)))vw
B(CBg)B(p(fxyz))vw
B(B(CBg)B)p(fxyz)vw
B(B(B(CBg)B)p)(fxy)zvw
B(B(B(B(CBg)B)p))(fx)yzvw
B(B(B(B(B(CBg)B)p)))fxyzvw
B(B(B(B(B(CBB)(CB)g)p)))fxyzvw
B(B(B(BB(B(CBB)(CB))gp)))fxyzvw
B(B(BB(BB(B(CBB)(CB))g)p))fxyzvw
B(B(B(BB)(BB(B(CBB)(CB)))gp))fxyzvw
B(BB(B(BB)(BB(B(CBB)(CB)))g)p)fxyzvw
BB(BB(B(BB)(BB(B(CBB)(CB)))g))pfxyzvw
B(BB)(B(BB)(B(BB)(BB(B(CBB)(CB)))))gpfxyzvw
C(C(B(BB)(B(BB)(B(BB)(BB(B(CBB)(CB))))))p)fgxyzvw
-}
h = c (c (b (b b) (b (b b) (b (b b) (b b (b (c b b) (c b)))))) p)
h f g 1 2 3 4 5 == (321, 54) -- True
2

There are 2 answers

2
willeM_ Van Onsem On BEST ANSWER

Unless the function is a concrete type, the arity of the function (if we leave out the fact that strictly speaking each function has an arity of one), can be variable. Indeed, a good example is printf :: PrintfType r => String -> r.

This seems a simple function. But there is a problem: r here can be any instance of PrintfType, and we have as instances:

instance PrintfType a ~ () => PrintfType (IO a) where
    -- ...

instance IsChar c => PrintfType [c] where
    -- ...

instance (PrintfArg a, PrintfType r) => PrintfType (a -> r) where
    -- ...

The first two are not that interesting. The last one is. Indeed, it means that it can return another function.

Which means that one can use printf as:

printf "foo"             :: String
printf "foo %i" 14       :: String
printf "foo %i %i" 14 25 :: String

They this implemented some sort of variadic function where the number of parameters depends on how you use printf.

While I'm not entirely happy with printf since it is not very "safe" compared to a lot of other Haskell functions, it demonstrates how one can make variadic functions. For example one can pass too many or too few parameters, and %i is not guaranteed ot retrieve an integer or number-like value, so there are better ways to format strings.

But regardless, the number of parameters does not depend on the function itself, so you can not derive this from the function's point of view, but from how it is used. Therefore, unless the types are all concrete in a function, if typeclasses are used, it is no longer easy to determine the arity of the function and therefore thus "merge" two functions together.

3
DDub On

As Willem Van Onsem points out in the comments, the problem with your question is that, technically, every function in Haskell takes exactly one parameter. I know what you mean, and I know what you want to do, but there's no reason why (+) can't just be a one argument function that returns a function and not a two argument function. Indeed, if we define a Num instance for something like Int -> Int, then it's totally possible that (+) is a 3 argument function!

On the other hand, just because the number of arguments can't be inferred, it doesn't mean that you're doomed. If you're okay with giving GHC a few hints, you can do what you want at the type level. Consider the following:

data Nat = Zero | Succ Nat
type One = Succ Zero
type Two = Succ One
type Three = Succ Two
type Four = Succ Three

class NFun m n f g where
  type family FunT m n f g
  h :: f -> g -> FunT m n f g

instance NFun m n x y => NFun (Succ m) n (a -> x) y where
  type FunT (Succ m) n (a -> x) y = a -> FunT m n x y
  h f g a = h @m @n (f a) g

instance NFun Zero n x y => NFun Zero (Succ n) x (a -> y) where
  type FunT Zero (Succ n) x (a -> y) = a -> FunT Zero n x y
  h f g a = h @Zero @n f (g a)

instance NFun Zero Zero x y where
  type FunT Zero Zero x y = (x,y)
  h = (,)

(This can probably be done prettier using GHC's TypeLits, but I always have a little trouble getting the induction to work out, so I rolled the natural numbers myself.)

Here, I've defined a type class NFun that takes two numeric types that indicate how many arguments the functions will take. The first instance provides arguments to the first function, and the second instance provides arguments to the second function. The final instance pairs up the results.

You can use it like this:

f x y z = x + 10*y + 100*z
g x y = x + 10*y

h @Three @Two f g 1 2 3 4 5 == (321, 54)