dash.el icon indicating copy to clipboard operation
dash.el copied to clipboard

helper to create a partial that ignores (some) arguments

Open wbolster opened this issue 8 years ago • 16 comments

when advising functions, i sometimes just want to call a function of my own after the original function, which can be accompllised using something like

(advice-add 'some-function :after #'my-function)

however, this means my function needs to have matching arguments, which in practice means i end up with something ugly like:

(advice-add 'some-function :after (lambda (&rest ignored) (my-function)))

i was wondering whether there is a better approach to this. something like

(advice-add 'some-function :after (-funcall-ignore 'my-function))

which seems (at least to me) similar to -const (and -cut) from dash-functional.

the name -funcall-ignore is probably not a good one. to me it looks like it's the inverse of -partial: instead of providing some fixed arguments upfront, this one "swallows" or "ignores" some of them.

would something like this be a useful addition to dash-functional?

am i just ignorant and is there already a sane way to accomplish what i want without the ugly lambda wrapper? (i am relatively new to lisp.)

wbolster avatar Apr 02 '17 20:04 wbolster

I have a bit of a mental problem with this, because if your function does not depend on the arguments, it must operate by side-effect which makes it "non-functional" (pun not intended).

However, I understand the pragmatism behind this, I often do the same thing. I'm not aware of something to do this. I ended up writing my own macro but these days I just do the lambda wrapper as I find it more readable.

One thing to make things less annoying: prefix the ignored arguments with _, they won't get the unused warning when compiling.

Fuco1 avatar Apr 02 '17 20:04 Fuco1

well, the "for side-effects only" argument holds only when all arguments are ignored, which can be considered the "extreme" case of the more general "ignore some arguments" case.

i fildded a bit and came up with this:

(defun -partial-ignore (fn &optional n)
  "Takes a function FN (that may take any number of arguments), and
returns a function that takes N extra arguments. When called, the
returned function ignores the first N args and calls FN with the
remaining args. As a special case, N may be omitted (or nil), meaning
that FN should always be called with no arguments at all."
  (-compose
   (-applify fn)
   (if (null n)
       (-const nil)
     (-compose
      (-partial (-flip '-slice) n)
      'list))))

...which seems to do the job:

(defun my-function (&rest args)
  (message "hi %S" args))

(funcall (-partial-ignore #'my-function) 1 2 3 4)  ;; => "hi nil"
(funcall (-partial-ignore #'my-function 2) 1 2 3 4)  ;; => "hi (3 4)"

ideas and feedback welcome. i am still learning. :)

wbolster avatar Apr 02 '17 21:04 wbolster

oh, and for completeness, an accompanying -rpartial-ignore would be nice as well.

wbolster avatar Apr 02 '17 21:04 wbolster

this implementation is a lot simpler:

(defun -partial-ignore (fn &optional n)
  "Takes a function FN (that may take any number of arguments), and
returns a function that takes N extra arguments. When called, the
returned function ignores the first N args and calls FN with the
remaining args. As a special case, N may be omitted (or nil), meaning
that FN should always be called with no arguments at all."
  (lambda (&rest args)
    (apply fn (when n (-slice args n)))))

wbolster avatar Apr 03 '17 07:04 wbolster

even simpler:

(defun -partial-ignore (fn &optional n)
  (lambda (&rest args)
    (apply fn (when n (-drop n args)))))

(defun -rpartial-ignore (fn &optional n)
  (lambda (&rest args)
    (apply fn (when n (-drop-last n args)))))

gives:

(defun my-function (&rest args)
  (message "hi %S" args))

(funcall (-partial-ignore #'my-function) 1 2 3 4)  ;; => "hi nil"
(funcall (-partial-ignore #'my-function 2) 1 2 3 4)  ;; => "hi (3 4)"

(funcall (-rpartial-ignore #'my-function) 1 2 3 4)  ;; => "hi nil"
(funcall (-rpartial-ignore #'my-function 2) 1 2 3 4)  ;; => "hi (1 2)"

wbolster avatar Apr 03 '17 07:04 wbolster

I'm thinking, we could call it -with-args-ignored or something like that, that would fit nicely with the Emacs "convention" of with- prefix for things which temporarily or locally modify some resource.

Fuco1 avatar Apr 03 '17 12:04 Fuco1

actually i would find that very confusing. most of the with-... things in emacs are macros that execute some &body form with some special setup, e.g. with-current-buffer, with-no-warnings, and so on.

the two functions i named -partial-ignore and -rpartial-ignore are more of an "inverse" of -partial and -rpartial. the latter are function wrappers that fix (as in "make them use a fixed value") some arguments to the original so that the resulting functions take less arguments. the former (my proposed addition) are function wrappers that ignore/eat/swallow some arguments before calling the originals, so that the resulting functions take more arguments than the original. these extra argument will be ignored when provided, hence my suggestion to name this -partial-ignore.

there may very well be better names, but personally i think anything with with-... is not among those.

wbolster avatar Apr 03 '17 16:04 wbolster

Hm, that's a reasonable point, I think you're right with the macros... but I don't particularly like the -ignore suffix. To me this is conceptually very similar to const, as you have pointed out... maybe we could try to come up with something involving that.

It is not so much ignoring as actually adding arguments to the function but ignoring them:

add2 :: (c -> d) -> a -> b -> c -> d
add2 f _ _ y = f y

-- alternative implementation returning a closure "directly"
add2' :: (c -> d) -> (a -> b -> c -> d)
add2' f = \_ _ x -> f x

This is a haskell implementation of such thing: take a function of 1 argument and add 2 useless arguments to make it a ternary function.

Fuco1 avatar Apr 04 '17 09:04 Fuco1

Maybe something like -amplify where you take an N-ary function, turn it into variable arg function, and just pass the first N arguments to it and throw away the rest (and error if you pass less than N).

I guess we wouldn't even need the n argument as you can figure out how many arguments a function can take and use that instead (and you can't use any other number otherwise it won't be possible to call it)

Fuco1 avatar Apr 04 '17 09:04 Fuco1

hmmm. personally i am not a fan of "magic detection" behaviour. it's brittle and it is not obvious what happens from the caller's perspective. the wrapped function may take a variable number of arguments (&optional and &rest) so i would like to keep the n argument (or achieve the same goal in another way).

-partial-ignore-args? -partial-ignore-extra-args? -partial-ignore-{first,last}-args?

wbolster avatar Apr 04 '17 10:04 wbolster

-ignore-args and -ignore-last-args?

-eat-args? -swallow-args? -suppress-args?

wbolster avatar Apr 04 '17 10:04 wbolster

The core here is that you give it a function with n arguments and turn it into a function with m arguments, so I find the prominence of ignore a bit in conflict. We are not ignoring anything, we are "adding" stuff (which just simply doesn't do anything :D)

Agreed on the &optional, I haven't realized that. Let's keep the argument.

Maybe someone else will have some ideas? /cc @Wilfred @fbergroth

Fuco1 avatar Apr 04 '17 10:04 Fuco1

fn.el may come in handy here

(advice-add 'some-function :after (fn (my-function)))
;; or you may prefer
(advice-add 'some-function :after (fn: my-function))

fbergroth avatar Apr 04 '17 12:04 fbergroth

Interesting question! I personally would just live with the extra verbosity, but I can see how it feels similar to the existing function combinators in dash.el.

Looking at the advice combinators: https://www.gnu.org/software/emacs/manual/html_node/elisp/Advice-combinators.html there's no combinator that does exactly what you want. In principle you could add an entry to advice--where-alist of the form (:after-do "bytecode..." 4) but I don't know exactly what bytecode value you'd need.

Regarding names, -const-fn, -ignore-args or -drop-args would be my suggestions.

Example implementation:

(defun -const-fn (fn)
  "Return a function that ignores all arguments and returns the result of calling FN.
See also `-const'."
  (lambda (&rest _) (funcall fn)))

I'd be reluctant to build something for dropping 1, 2, ... unlimited arguments when we don't currently have a use case for dropping a fixed number of arguments AFAICS.

Wilfred avatar Apr 04 '17 17:04 Wilfred

Relatedly, the old advice system does not make you worry about arguments:

(defun foo ()
  (message "in foo")
  1)

;; Doesn't matter what arguments `foo' takes.
(defadvice foo (after message-after-call activate)
  (message "in advice"))

Wilfred avatar Apr 04 '17 17:04 Wilfred

@Wilfred Yea, I might be the only person but I actually prefer the old style... it seems way more readable and to the point /shrug

Fuco1 avatar Apr 05 '17 12:04 Fuco1