Breaking notation convention - a potential anti-pattern?

I have a scenario where I think it might be warranted to depart from the end with an exclamation mark convention for function names that modify at least one of their arguments.

However, I wouldn’t want to get into an anti-pattern here - so I am really curious about your take on this.

I think the best approach is to show a MWE here:

struct MyStruct
    arr::Vector{Int} # updatable thing
    updatable::Bool
end

function myfun(x::MyStruct)
    # some sophisticated logic using x
    y = 1 # resulted from the logic
    success = true # can also be false - depending on the logic
    success && x.updatable && push!(x.arr, y)
    return x
end

x = MyStruct([1, 2, 3], true)
myfun(x)
@info x

In this scenario, myfun has the potential to modify its argument - and will always fail to modify it when the updatable is set to false. And will almost always modify it when the updatable is set to true (with a few exceptions - depending on the internal logic of the function).

I am aware that I could isolate my logic and have myfun and myfun! versions that can call myfun_logic that returns (result=an Int, success=true/false) and perform the update only inside myfun! - but in this case I could very well just drop the updatable field and decide which function to call each time. So let’s stick with the MWE - where the user does not need to select between the two functions at each call location.

So, should I switch to myfun! in this scenario? Or, is myfun a legitimate option?

IMO it isn’t. There are many functions with a bang that might fail to modify their argument for some reason. The bang is here to tell you that they might.

julia> push!(Int[], 0.5)
ERROR: InexactError: Int64(0.5)
Stacktrace:
 [1] Int64
   @ ./float.jl:900 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] push!(a::Vector{Int64}, item::Float64)
   @ Base ./array.jl:1060
 [4] top-level scope
   @ REPL[1]:1
7 Likes

Just to be sure about your intended message: do you mean I should switch to myfun! and drop the myfun (given the function might modify the argument)?

I am writing this because you quoted should I switch to myfun! in this scenario? but you replied you shouldn’t. However - the rest of your message seems to support the bang usage in the context.

This is a mutating function, and should end with a !

There are some exceptions, like print, which don’t end with a !, but I don’t see that this is a relevant comparison. Keep the !

2 Likes

I was trying to confuse you. And me. Mission accomplished :sunglasses:
Also, just edited my message

1 Like

Another convention that I think is nice is the !! convention from BangBang.jl, where

push!!(v, x)

is something like

if x fits_in v
   push!(v, x)
else
   vcat(v, [x])
end

i.e. the function is allowed to choose if it does mutation or an out-of-place operation. The mutation is basically an optimization.

8 Likes

The ! is just a convention, designed to be a helpful signal to end users about what the function may or may not do. IMO it all depends on the use-case and how you might imagine callers reasoning about the function call.

6 Likes

Thanks - after reading your messages, I will opt for bang usage in my scenario.

To not open a new topic - and somewhat derived from the present one - how about the callable objects?

Continuing the MWE from OP:

function (x::MyStruct)(someargs...)
    y = 1 # resulted from the logic
    success = true # can also be false - depending on the logic
    success && x.updatable && push!(x.arr, y)    
    return x
end

Calling the object might update the state of the object - I don’t see any convention opposing such behavior.

What are your thoughts about this?

1 Like

A mutable callable object is kinda cursed :rofl: I guess you have no other choice than to document it thoroughly

2 Likes

I guess in this case one could just advise that people name the callable object with a !. i.e.

function (x!::MyStruct)(someargs...)
    y = 1 # resulted from the logic
    success = true # can also be false - depending on the logic
    success && x!.updatable && push!(x!.arr, y)    
    return x!
end

but I’m not sure that’s a great idea

1 Like

What if your mutatable callable object computed factorials?

don’t

1 Like

Thank you, everybody, for the input.

I guess that such behavior can be permitted inside the internals of a package - primarily used in a limited scope.

function enclosing(someargs...)
    # define the updatable thing here
    x = MyStruct([1, 2, 3], true)
    # do crazy stuff in this scope
    x()()()()()()
    @info x
    # return and hide from API what you did
    return x.arr
end

I am not asking these things just for the sake of departing from the convention for no reason. I imagine scenarios where multiple lines of code can be saved and where the weird thing is self-explanatory inside a limited scope. Obviously, if the weirdness spans across functions/modules/files, it becomes a clear no-go.

Another scenario I can think of is when you create a DSL that will end up with a specific set of documentation (overlapping or not with Julia’s) - and where Julia conventions are not expected (and provided there are good arguments for this approach in the context of the DSL design).

But I get that doing this stuff and exposing it via that public interface is like setting traps for the users of the package (or ending up with pretty complex documentation that might be hard to follow - especially if convention-breaking occurs).

Thank you all.

I appreciate all your inputs, and I don’t think there is a solution - however, I find @gdalle’s don’t a very good (and funny) thing to show together with the OP - that is, for potential new users who are reading the OP and don’t want to follow the thread.

At the same time, I acknowledge that things can be more nuanced (e.g., see @mbauman contribution).

I think is better to go with don’t default - and once people are really comfortable and have good enough contextual arguments can opt for dropping the bang.

1 Like

Actually, my “don’t” was a response to this, as in “don’t even go there it is beyond cursed”

I have updated the accepted answer, as I think Matt’s reply is more helpful for users who might land on this page from Google

2 Likes

You are right. Thank you.