-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add is_external_fun/1 and is_local_fun/1 guards #8554
Comments
Creating only the guard function I still have hope that Erlang/OTP might fix the fact that |
Additionally note, while I put |
I agree that this would be useful. I'd like to change the names however (and not just here, though the ship might have sailed already) as "local fun" and "external fun"/"remote fun" might make people think about distribution, when it's more about how the fun relates to its module: the former being a fun tied to a specific module and the latter a fun tied to an MFA. |
Yes, I agree this would be very helpful to have, and I also agree that we could likely do better with the naming. I also thing this should be coupled with having more places in the system use such funs instead of MFA tuples - with the recent changes they are smaller in memory, and also integrate far better with various analysis tools - typecheckers, dialyzer, static analysis, IDE tooling (go to definition navigation, etc), linting... |
What about There are, as said, important cases where you need the restriction that a |
@RaimoNiskanen |
I don't think it's common here. A "local" fun is not tied to a particular instance of a node, and works just fine everywhere as long as the required code is available. "Local" refers to the fun's relation to a specific module, which is relevant in non-distributed scenarios (code upgrades, funs persisted to disk or similar). |
I'd float some ideas |
Floating on...: |
I like the idea of promoting |
Coming back to this, I think |
I was thinking about this a bit more. I think using the term "exported" will be problematic given the semantics of the already-existing |
@michalmuskala I agree with that and your presumption at least as far as what I had in mind was definitely not to check if the code was loaded. |
I think using the term "exported" is valid because it is describing a future expectation that the function must be exported to be used, otherwise it will be considered undefined (i.e., an |
@okeuday I definitely don't think you are wrong here, especially coupled with what @RaimoNiskanen stated, that this is related to the export table on a module. I think part of it for me is what's easier on the eyes and would what result in less ambiguity, the head of clause should be enough to disambiguate. What about a happy medium between promoting |
@starbelly People haven't liked That is why I saw this as a new guard function to associate with the existing Using suffixes to describe variation is a little odd for people that want to write function names as English sentences because using suffixes for the variation should be equivalent to a subject-object-verb word order. Using subject-object-verb word order is most common among natural languages though, so it should be how most people naturally think. |
@okeuday I don't disagree with what you've stated, but I'm still wondering if there is a better name that doesn't "run together" with what exists already. Needs a little more thought, I do believe, this would be something that sticks for a long time after all 😄 |
Another idea |
@starbelly A named function could be a local function, so the "named" word doesn't help to distinguish between exported functions and local functions. |
Yes, I thought about this after I shared the idea, and ambiguity therein. So, I think we need to be clear about the "export" part, at least I can not think of something that is quite ambiguous, sans what @michalmuskala suggested around I'd like to hear from OTP team at this point 😄 |
@starbelly Using |
I think export / exported is fine, and it makes sense as that's precisely what we're talking about. I think ideally we'd have a name that conveys something more, but thus far I don't think anyone has come up with something that is non-ambiguous and sends a signal to the user. |
I'm about to open a PR for this, while it would be nice to have the name ironed out here vs the PR, I'm not sure it'll play out like that. As happy medium and what I think might just work well, is |
I was thinking about this again. Perhaps it would be easier to name things if we flipped the problem around? Something like an |
I like it, though I'm wary of how verbose it will seem a decade down the line when we're all familiar. How about Edit: Come to think of it |
I suspect this problem isn't perfectly suited for flipping around. We want to know if a function is safe to keep around over module upgrades so when you later call it the call may be to a new module:function/arity, that is if the call goes to an exported/external function. If a function has a closure is a slightly different property: foo() ->
F1 = fun (Y) -> erlang:is_boolean(Y),
F2 = fun erlang:is_boolean/1,
{F1, F2}. Here F2 is an export entry fun, but F2 is not. Neither has a closure. Although an export entry fun cannot have a closure, a "local" fun also may not have one. |
I would still say F1 is a closure, it just happens to close over an empty environment. But yes, I agree it's somewhat ambiguous. |
You can also argue that F1 is a true closure because it closes the specific module version it's in. |
So the property we want: is module specific function or is code upgrade safe function. But I see what you mean; it is possible to define a closure fun to be any fun that isn't called trough an export entry, that is every fun that is created through the |
|
Hmm, while it sounds nice, and maybe this is niche, folks will have to perform two checks with |
I'm partial to having both |
Humm, should extend the type t_fun or add t_closure / t_export in the same PR? Edit: I suppose only extending t_fun makes sense, but the question still remains. |
If I may push back on this feature a bit. I agree that being able to distinguish between these two is important but is it common enough to warrant adding a guard for it? Only few functions are available in guards today and we typically teach the guards early on and we tend to memorize them. Is this distinction important enough to make it front-facing in the language? I have written several dozens of thousands of Erlang/Elixir code at this point and I probably had to distinguish between them 2 or 3 times... and I tend to be writing lower level code. How many use cases can we even surface collectively? In my opinion, calling |
@josevalim Yes, it is common enough to add a guard for it. The difference matters when you care about hot-code loading of functions that require validation before they are loaded. Often hot-code loading is avoided by Erlang developers (due to operations preferring static source code) but it is a critical feature of the Erlang language that influenced its design and helps to justify its use (for the important fault-tolerance use-cases that would be nice to see in the future). I don't believe the existence of Elixir is a reason to not improve Erlang. |
Where was this argument laid out? Can you please show me so I can improve myself? Because I don't think I said at any moment that Erlang should not improve because Elixir exists. |
On the flip side a guard that isn't used doesn't need to be memorized. If any of us were asked to provide a list of guards off the top of our heads, who would remember to include |
Just to be clearer about the use-cases of the A separate use-case is when a module wants to support hot-code loading and returns a function. In that situation you want to ensure the function returned only calls exported functions. If the module uses configuration to use Erlang functions dynamically, then you care about having the The compiler can't check for that problem in all possible ways it can occur, so any check for it would be incomplete (if one was added). If a compiler check was added, it would likely be a warning that is always turned off to avoid causing noise for source code that doesn't care about this problem. So, the use-cases for these guards are important but might appear unusual to people. |
An additional idea is adding a separate function syntax that forces you to only use exported functions for your function calls in the function body. That would be a way of avoiding the |
I don’t think the use cases have been unclear to the folks in this thread. I was personally asking how common they would be in practice. Providing code examples that do such checks today (or should be doing them) would, IMO, be a useful addition to the discussion. Of course, it is not my decision to make and no one has to answer to my concerns. I only hope that me asking for examples is not going to lead to more strawman arguments that I don’t want Erlang to evolve. |
I too would like to hear from other people how they would use this, tbh. I have stated my cases, which to brief is to guard against anon functions being stored (either in disk or in memory) for safe code upgrades (and thus used at all within the code that would said stored functions, which require consistent validation, vs just once) and guarding against anon functions being slung over the wire. I think I I understand your concerns aside from usage, which may be, it could be confusing to new comers, when to use what, is that right? Edit: Additionally, if we really don't want new guards or bifs, it is still possible to modify is_function to take a third argument. I'm ok with the new guards, but if enough people think it's clutter, then that's still a possibility. The annoying thing about this approach, is that in order to check whether something is or isn't a closure, you always have to provide the arity. Though, we could pattern match and dispatch based on the type of the second argument, though I feel that's a bit messy. |
My concern is not newcomers per-se, but rather that the number of guards have been historically kept small. And given what is a guard or not is mostly an arbitrary decision, I'd say that keeping the list small is a good thing, since it impacts the way you write the code (it controls what goes on the right side of There is also the argument that there are other functions that we could make guards: While I don't think any of the arguments above are blockers, I think more examples of how we could use this feature would be helpful. The most recent example I can think of is in telemetry but that's not a use case screaming to be a guard. :) |
These are arguments I can get behind. I can not say the use wouldn't be rare, as it would not. Not rare in the sense that it's never used, but the frequency of usage in a code base would not be great, contrast that with Another alternative, in response to your points, these do not have to be guards. What drove this initially on my end? I don't fancy the thought of using a debug function to get some of the information I'm after to make a decision, especially if if I'm going to have to call it not once, but many times 😄 . These could just simply be bifs, if they still lived in Still, we need to hear more from others, I'm a fan of listening 😄 |
Guard functions need to be referentially transparent, as described in the spec. Unfortunately, The more guard functions that are available, the better source code can be, so there is a benefit by making things simpler. From that perspective, using I ran into the hot-code loading use-case when attempting to hot-code load https://github.com/CloudI/CloudI/blob/develop/src/lib/cloudi_service_monitoring/src/cloudi_service_monitoring.erl due to the aspect functions (that are used to attach extra monitoring to services) which needed to use only exported functions. That is fixed now, but there is the potential for getting into that problem elsewhere and that type of bug is easy to have in source code (due to nothing checking for it). |
Still haven't heard back from others on use cases, I'm just going to defer to @jhogberg on whether to open this PR or not. |
Is your feature request related to a problem? Please describe.
No problems.
Describe the solution you'd like
The code exists in beam utilities to discern whether a fun is an external fun ref or a local fun. It would be quite nice to have these as guards. Said guards would mostly be convenience since we can get this information from
erlang:fun_info/1
today. Though, there may be performance benefits to having a simple guard vs whaterlang:fun_info/1
returns.Describe alternatives you've considered
Simply use
erlang:fun_info/1
in the body of a function required to check whether a function is external or local.Additional context
In some situations one must validate that function is not a local function in order to ensure this is not slung around between nodes, as this can be a problem in a mixed cluster with different versions of OTP. In my case, we need to ensure callback functions in configuration are only ever external funs for this reason.
Additionally, I'd be happy to do the PR, but I figured an issue regarding the feature was appropriate first.
The text was updated successfully, but these errors were encountered: