Appending `!` to names of callable objects (functors) that modify their argument?

question
metaprogramming

#1

Suppose I have defined

struct MyFunctor
    x::Int
end

and I want to make MyFunctor objects callable in two ways, where one method modify the argument, say

(f::MyFunctor)(y) = y + f.x
(f::MyFunctor)(a, y) = push!(a, f(y))

Now

f = MyFunctor(2)
f(3) # -> 5
a = [1, 2]; f(a,3) #a == [1, 2, 5]

works fine. However, the style guide says “Append ! to names of functions that modify their arguments”, so my second method should ideally be named f! instead of f. Is there a nice (standard?) way of accomplishing this without having to (manually) rename the callable object? In other words, it would be nice to automatically have the second method named f! when I define f = MyFunctor(2).

I’m sure there is a metaprogramming way of accomplishing this. Unfortunately I’m not sure of it’s (recommended) direction…


(Integrated) autocorrelation time
#2

My personal opinion on this is that functor objects shouldn’t modify the outside world. What is your application? Why do you need to have functor objects modifying external variables?


#3

Could you elaborate? I do not see why - on a principal basis - a functor object should be more restricted than any other function. If we can define rand and rand!, why not f::MyFunctor and f!::MyFunctor? I agree that it would be bad in many cases (like my silly, minimal example), but in all?

To be a little more specific: I’m implementing a reweighting package. There I construct a Reweights object from a set of input data (obtained from Monte Carlo sampling), i.e. rw = Reweights(somedata). (And by the way, the construction of the object may be expensive, so I would like to initialize it once per data set.) From that object I want to be able to calculate a set of weights (stored in a Vector) at some parameter of choice p, like weights = rw(p). However, since I would like to repeatedly calculate weights for different p's, I thought it would be nice to be able to calculate weights in-place (non-allocating) if desired, like rw!(weights, p). It of course works fine with rw(weights, p), but then the notation is not following the style-guide…

Sure, it is possible to redesign my program and use a couple of functions like make_weights(rwo::ReweightDataObject, p) and make_weights!(weights, rwo::ReweightDataObject, p) instead, but that looks less elegant to me…


#4

I kind of like your second suggestion, it looks more elegant to me to have a verb in the function like make_weights and make_weights! instead of a functor object. Multiple dispatch and functional programming play very well together, and using active verbs in the function names makes things extremely clear in my opinion. As you said, this is a matter of taste.

EDIT: you can also rename your function make_weights to something like evaluate if you want something more elegant:

rw = ReweightObj(data)

weights = evaluate(rw, p)
evaluate!(rw, p, weights) # no allocation

#5

So are there some guidelines on when one should use callable objects f(x) and when one should use the evaluate(f, x) structure? I mean, my Reweights is conceptually not that different from, say, the polynomial example in the manual (or the huge functions/functors people are throwing around when doing machine learning)…

(I agree that evaluate is more elegant than make_weights :slight_smile: )


#6

That is a good question, I will defer it to someone else, didn’t thought about it enough. :slight_smile:

As a super simple rule of thumb, I only consider functor objects for concepts that somehow have a one-to-one correspondence to a mathematical concept. Like the example with polynomials you pointed out. Other than that, the evaluate approach is more clear in my opinion. If was using a package like yours for example about reweighting, I would never think of a weight as a function in the mathematical sense.


#7

Another current limitation of functor objects is that you can’t specify the interface in the abstract level:

abstract type Functor end

# try to specify interface
(f::Functor)(x) = error("not implemented") # this won't compile

and I always take that into account before I commit to functor objects. It shouldn’t be a limitation in future versions of Julia though.


#8

Good points. (Although what constitutes a “one-to-one correspondence to a mathematical concept” is not very clear to me. One (wo)man’s polynomial is another (wo)man’s point in a Hilbert space… )

I guess I’ll migrate my code to an evaluate style, then :slight_smile:


#9

We talk about both as functions still :slight_smile:


#10

My point is that it depends on ones perspective: Do we focus on the functions (operators/maps) themselves, or the elements they map to? Both perspectives are perfectly valid (both for polynomials and, say, Reweights objects.)

Anyway…


#11

Generally, for me the choice between f(x) and g(f, x) would boil down to

  1. whether f is close to the idea of a mathematical (“pure”) function, (use f)
  2. whether I want to pass it to another function (eg map), (use f)
  3. whether I want to do something else with f, (use g),
  4. whether g is common enough as an operation that it deserves its own function name, (use g),
  5. whether I have more fs or gs.

Having both f(x) (= g(f,x)) and h(f, x) are confusing (I think, this may be subjective). Having h!(f, x) at the same time just makes it worse, for me this would violate (1) above.

In your case, since you have both modifying and functional calls, I would stick to g(f, x) and g!(f, x).

Again, this is largely subjective, YMMV. Sometimes I rewrite interfaces back and forth and go in circles for a while. Hope this helps.


#12

@Tamas_Papp Thanks for your thoughts! Yeah, you and @juliohm have made me drift towards the g(f, x) and g!(f, x) side (for now… I also tend to “go in circles” :slight_smile: )