1

Is it possible to curry a function at a user-defined n-th position? Functool's partial function can handle partial evaluation of multiple arguments that

from functools import partial

def f(x,y,z):
    return [x,y,z]

Then

partial(f,1)(2,3)

[1,2,3]

and

partial(f,1,2)(3)

[1,2,3]

Is it possibly to curry at a given argument position?

curry_at(f,n)

So that

curry_at(f,2)(1,3)(2)

gives

[1,2,3]

where, I think to keep consistent with possible *arg and **kwarg, n should be no larger than the number of regular arguments?

Is there a method to enumerate the arguments to separate the prefix and postfix arguments to the n-th slot?

alancalvitti
  • 476
  • 3
  • 14
  • 1
    Possible duplicate of [Can one partially apply the second argument of a function that takes no keyword arguments?](https://stackoverflow.com/questions/11173660/can-one-partially-apply-the-second-argument-of-a-function-that-takes-no-keyword) – Mark Jul 10 '19 at 16:29

2 Answers2

1

It's not common to recommend building functions like this, but in this case, I think it's better to have to have static definitions with a little bit of duplication rather than relying on a dynamic function whose complex behavior is difficult to verify -

def apply_at_1 (f, *a, **kw):
  return lambda x, *a2, **kw2: \
    f(x, *a, *a2, **kw, **kw2)

def apply_at_2 (f, *a, **kw):
  return lambda x, y, *a2, **kw2: \
    f(x, y, *a, *a2, **kw, **kw2)

def apply_at_3 (f, *a, **kw):
  return lambda x, y, z, *a2, **kw2: \
    f(x, y, z, *a, *a2, **kw, **kw2)

def dummy (a, b, c, d, *more):
  print (a, b, c, d, *more)

apply_at_1(dummy, 2, 3, 4)(1, 5, 6, 7, 8)
# 1 2 3 4 5 6 7 8

apply_at_2(dummy, 3, 4, 5)(1, 2, 6, 7, 8)
# 1 2 3 4 5 6 7 8

apply_at_3(dummy, 4, 5, 6)(1, 2, 3, 7, 8)
# 1 2 3 4 5 6 7 8

Sure, the dynamic function allows you to apply an argument in any position, but if you need to apply an argument at position four (the 5th argument), maybe it's time to consider a refactor. In this case I think it's better to set a sensible limit and then discipline yourself.

Other functional languages do similar things for typing reasons. Consider Haskell's liftA2, liftA3, liftA4, etc. Or Elm's List.map, List.map2, List.map3, List.map4, ...

Sometimes it's just easier to KISS.


Here's a version of partial supporting wild-cards, __ -

def __ ():
  return None;

def partial (f, *a):
  def combine (a, b):
    if not a:
      return b
    elif a[0] is __:
      return (b[0], *combine(a[1:], b[1:]))
    else:
      return (a[0], *combine(a[1:], b))
  return lambda *b: f(*combine(a,b))

def dummy (a, b, c, d, *more):
  print (a, b, c, d, *more)

partial(dummy, 1, __, __, 4)(2, 3, 5, 6, 7, 8) 
# 1 2 3 4 5 6 7 8

partial(dummy, __, __, 3, __, __, 6, 7, 8)(1, 2, 4, 5)
# 1 2 3 4 5 6 7 8
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • I use the "dynamic" version all the time in another functional, optionally typed language (which moreover allows a direct positional wildcard operator at the desired position to indicate it). I can't think of a reason why you would think this is better. – alancalvitti Jul 10 '19 at 20:33
  • PS, as long as the KISS is [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) – alancalvitti Jul 10 '19 at 20:42
  • @alancalvitti because there's zero complexity and virtually nothing to test. It's basically a syntax rewriter. Any dynamic implementation will be slower as more intermediate values and error-prone logic is required. I called out the "duplication" in my post, and already said it's not common to write functions this way. However in this specific scenario, I think this approach has many advantages. And by evidence of other popular languages using similar techniques, I think it adds credibility to the answer. If/when you find/write a dynamic solution to use in Python, please share it. – Mulan Jul 10 '19 at 21:15
  • I don't know what can be done in Python, it's a far less powerful language than what I've been using for 20+ years (see Paul Graham's [Blub paradox](https://en.wikipedia.org/wiki/Paul_Graham_(programmer)#The_Blub_paradox) ). But I can tell you the dynamic version I use on a daily basis and there are no problems or material slow-downs - as you point out it's handled by the interpreter as a simple term rewrite. – alancalvitti Jul 11 '19 at 01:16
  • _”the dynamic version I use on a daily basis...”_ - in what lang? Lisp? That’s hardly a fair comparison. You’re asking how to do this in Python and the language capabilities are completely different. – Mulan Jul 11 '19 at 04:22
  • I've been using Mathematica (Wolfram Language) where the curry function lets you even permute the order of arguments. With that, I made what I'd really like to replicate in Py, which is a positional curry syntax like `f(1,2,*,4)` where the wildcard indicates which position to partial - to streamline functional data pipes. – alancalvitti Jul 11 '19 at 14:36
  • @alancalvitti per your comment, I added an implementation of `partial` that supports wild-card arguments – Mulan Jul 12 '19 at 15:44
  • thanks, but I am looking for this syntax: `dummy(1, __, __, 4)` - the wildcard(s) are then interpreted to partial the function (note `partial` is not directly invoked by user). – alancalvitti Jul 15 '19 at 13:13
0

I tryed a workarond using the index and a for loop, hope it helps:

from functools import partial

def curry_at(funtion,parameter,index,*args):
  h = funtion
  index-=1
  ls = list(args)
  ls.insert(index,parameter)
  for arg in ls:
      h = partial(h,arg)
  return h()

a = curry_at(f,'a',2,'c','d')

b = curry_at(f,2,2,1,3)

a

['c', 'a', 'd']

b

[1,2,3]

developer_hatch
  • 15,898
  • 3
  • 42
  • 75
  • @alancalvitti that because you missunderstood the signature, first the index then the param, I will make the adjustments. look – developer_hatch Jul 10 '19 at 18:49
  • @alancalvitti I made an update in the answer, could you try it now? – developer_hatch Jul 10 '19 at 18:53
  • Couple of issues - probably fixable. First, `curry_at` should return a function so that parameter can be applied to this function. Also eg `curry_at(f,'x',5,'a','c','d')` incorrectly inserts 'x' at the wrong position. Perhaps more difficult, how to handle `**kwarg`? – alancalvitti Jul 10 '19 at 18:56
  • 1) to simply return a function, you return `h` instead of `h()`, and of course that curry_at(f,'x', 5, 'a','b', 'c') will act extrange, you must know the amount of parameters of the function before use this particular curry_at – developer_hatch Jul 10 '19 at 19:03
  • you would not pass the parameter 'x' when calling `curry_at`. That would be passed to the function returned by `curry_at` – alancalvitti Jul 10 '19 at 19:06