8 ms·
Safe dead code removal in a pure functional language
- brundolf 6y agoThis is one reason I go out of my way to divide functions between those that return something and those that do something. In JS I use class methods as a shorthand for the latter: "this will probably mutate something". Plain functions, then, are relatively safe to treat as pure/remove if their return value isn't used.
- Blikkentrekker 6y agoIt seems very bad design to begin with to have a function both return a value and be non-transparent in any other way than what it needs in order to return said value. But in some cases, this is needed for efficiency.
- tsimionescu 6y agoI think about it the other way. If you are doing something effectful, how often is it useful to also return some information? To me, the answer is 'extremely often', so I don't see the value of what GP is proposing. It's nice to have pure functions and some kind of separation, but it's not worth it to give up returning values from effectful procedures.
- Blikkentrekker 6y agoOnly to return information about the effect's success, which would again make it easy to see that the function must have some effect, if all it returns be true or false or another trivial value that could be constructed from æther.
- tsimionescu 6y agoI think a general case is concurrent mutation of a data structure. If you are adding some item to a list in parallel with others, you will usually need to return some token (e.g. index) that will allow your caller to retrieve that item later. You MAY be able to get away with making this a pure function by returning a shallow copy, but I believe that depends on how central the sharing aspect is.
- brundolf 6y agoYeah, so the problem here is that the value creation logic is non-deterministic; not only does it depend on hidden state, it doesn't even do so predictably because you don't know the order in which other threads might interplay. I established three categories of imperative-logic-return-values above: > The return value is either going to pertain to the state before the side-effect, the state after the side-effect, or the side-effect itself. The first two could easily be broken out into pure functions. The third, again, mostly comes down to error reporting. So this definitely falls into category three, which makes the coupling legitimate. It's possible I was wrong in my statement that that category "mostly comes down to error reporting"; it was just based on my own past experience.
- brundolf 6y agoIt probably varies by domain - most of my deep experience is on the front-end - though even outside of that, I can't think of many cases outside of errors where I fundamentally need to return something in tandem with a side-effect. The return value is either going to pertain to the state before the side-effect, the state after the side-effect, or the side-effect itself. The first two could easily be broken out into pure functions. The third, again, mostly comes down to error reporting. I guess things get more complicated when you consider I/O. A function that gets a file handle or makes a GET request is technically impure and mutates the state of the macro-system, and yet is primarily concerned with what it returns. I tend to lump these into the "plain functions" bucket, but it's a gray area.
- magicalhippo 6y agoWe're in the procedural world, but a typical case we have is a variation on this theme: A function takes some declaration id, generates one or several records based on the declaration data, commits them to the database and returns the record id's so they can be used in the UI or similar. Either the record id's are displayed to the user directly, they're used for filtering in a grid, used for generating response files to external system, or something else. Often this would get called several times for the same declaration id, usually generating new records, so it's easier to have the function return the id's rather than trying to reconstruct what it did. Could be there's an easy way to map this to pure functions, I haven't thought about it. Would be interested to know though.
- brundolf 6y agoWhere my mind initially goes is to have a pure function going from id -> records, and then pass those records off to a separate procedure that persists them to the database. The same records can then also be passed around to the UI or whatever else. > Often this would get called several times for the same declaration id, usually generating new records, so it's easier to have the function return the id's rather than trying to reconstruct what it did. Are the "new" records meaningfully different in some way, or are they just new instances in memory of what is fundamentally the same value? In the former case, there's state somewhere that's causing each one to be different (there is not a pure relationship from id -> record set). In the latter case, you don't actually need multiple instances (unless you expect them to be mutated by other code down the line and are guarding against side-effects, in which case you could defensively clone the pure function's result as needed instead of calling it multiple times). Edit: it's possible I misunderstood, and that what's happening is you're inserting data into a database which has its own auto-incrementing IDs that it then returns to you. If so, the coupling between value creation and state/mutation exists in the database's API itself, and so is outside of your control. There's not much you can do in that case because you don't own that code.
- jfmengels1 6y agoIn pure functional languages (at least in Elm), what happens is that you have data that describes what should be done, instead of doing it directly. is talk called "Effects as Data" describing that idea: https://www.youtube.com/watch?v=6EdXaWfoslc https://www.youtube.com/watch?v=6EdXaWfoslc
- brundolf 6y agoI'm aware; I'm not sold on this most extreme version of pure-functionalism. I think it's unintuitive, syntactically clunky, and hard to reason about, despite the benefits of purity. I'm a believer in the functional core/imperative shell approach.
- Latty 6y agoI thought similarly until I committed to working on an Elm project. I developed an open source game in Elm, and I've been very impressed with the benefits of the pure functional approach. It takes some getting used to, for sure, but it makes things really simple to reason about once you get used to it. There are definitely rough edges still: Elm in particular is just missing a lot of stuff, but I was surprised how much value the pure functional approach gave me.
- closeparen 6y agoThis is exactly how I understand functional core/imperative shell. The functional core returns a structure describing what should be done and the imperative shell goes out and does it.
- nerpderp82 6y agoI often use a pattern where I do a batch and group a bunch of operations baked into a tuple that describes the computation. Instead of do_thing(on_data), the intermediate pass would emit a data structure full of list([("operation_do_a_thing", on_data), ... ] and then a command executor can do the actual scheduling of the operations, serial, parallel, batched, try/except with backpressure, etc. Separate the thing you want done from the doing it, because when you intermingle the doing-the-thing it greatly complications the scheduling logic. Think of it as a working programmers io monad.
- notretarded 6y agoAnd if I have a function executed from user input or defined from configuration.
- imoverclocked 6y agoRemoving dead code is incredibly valuable. Even in time-consuming environments with highly effectful languages it can mean the difference between pulling in and maintaining old/archaic dependencies or not. I wish more of our static analysis tools for non-functional languages had better concepts of purity. There are so many places where it fails in languages without this concept. eg: Java annotations can change the behavior of a method from a simple getter to something else entirely with hooks to be used/referenced at runtime. One place I have found that static analysis fails is with API contexts. Either they get it wrong and offer to remove API elements or they get it less-wrong and just don't touch anything that can be exported. Granted getting it right would require all usages of the API or a blessed list of things we know we want to keep ... in which case we are potentially back at our source code as a source of truth anyway. Another interesting hole is "do we keep code that is only referenced by tests?" In some cases, it makes sense but it's definitely a knob that the user needs to adjust manually so far as I have seen.
- Twisol 6y ago> eg: Java annotations can change the behavior of a method from a simple getter to something else entirely with hooks to be used/referenced at runtime. To be slightly fair to Java, annotations don't themselves do anything to the annotated method. Calling a method directly is the same regardless of whatever annotations are on it -- of course, unless it or one of its upstreams looks at the annotations, but that's isomorphic to just passing the data along. Java annotations induce different behaviors in specific callers that are designed to take advantage of those annotations, which is arguably even worse -- like a meta analogue of the COMEFROM keyword.
- imoverclocked 6y agoTotally agree. The annotations basically just mark code. Things like AspectJ can then place side-effects around marked (and unmarked!) code. Also, coming across transient runtime errors because you use multi-catch (a basic Java language feature) and some java library that injects bytecode is pretty lame. All the benefits of a typed+compiled language with all the benefits of monkey patching!
- UncleMeat 6y agoPure-function analysis is not that hard. If you want the easy one, it is a pretty straightforward local analysis. If you want the hard stuff you need a sound call graph and some fix-point computation over the whole program, but it is still achievable. The thing that is valuable here is the existence of pure functions, not necessarily the requirement. For a huge number of real programs written in java or whatever the exact same technique applies.
- amelius 6y ago> but it is still achievable I would say it is undecidable, to be honest. For example if(undecidable_function()) return lots_of_code(); else return 0; Can you remove lots_of_code() or not?
- AlexCoventry 6y agoYou can get 99% of the way there without solving the halting problem, though. :)
- jfmengels1 6y agoSure, but under some assumptions. Assumptions like that some piece of code won't be transformed by a macro, that reasonably looking code like `a.b` doesn't have side effects, etc. It's not that it's impossible, but that 1% (or 5%, 10%, ???% depending on how the project is structured and whether it uses a lot of side-effects or dynamic properties) can be enough make your program crash if the assumption turns out to be wrong. Another example that you could go for, is can you determine whether a property of an object is used or not. If you have a language like JavaScript where you can do `object[propertyName]`, that turns out to be very hard. In a pure functional language, that is comparatively pretty straightforward to detect.
- wolfadex 6y agoThe javascript example gets further compounded by doing something like `object["prefix" + variable]`. I've seen some very large companies abuse JS object proxies so that code is nearly impossible to follow even with the best IDEs and someone who's excellent at grepping.
- zepatrik 6y agoIt would actually make sense to have linter rules in javascript to report impure functions and avoid them as much as possible. But as the article said, JS is kinda broken already as you can have proxies and stuff that possibly adds side effects to the most basic expressions.
- jfmengels1 6y agoThe article author wrote this ESLint plugin to do just that https://github.com/jfmengels/eslint-plugin-fp https://github.com/jfmengels/eslint-plugin-fp then he (I) happily switched to Elm, because even with a lot of static analysis rules, it's really hard to avoid impure functions when a lot of the libraries and existing code use that, and sometimes even require that.
- pbiggar 6y agoIn Dark, we label (either manually for stdlib, or automatically for other functions) whether they are pure or not. Pure functions (most of them) do not need their results stored in traces (Dark has an always-on-debugger that uses "traces" saved from real inputs). Making a (mostly) pure functional language enables so much!
- mhh__ 6y agoD has a keyword "pure", it's pretty useful. It also is part of the type signature so you can take a pointer to a pure function as a library.
- millstone 6y agoIn Elm pure functions may allocate memory. How does D handle this?
- mhh__ 6y agoD pure functions can always allocate memory via the GC. If you don't want to use the GC it is customary to use a pure wrapper around malloc (even though you theoretically shouldn't, it works fine in practice). https://dlang.org/library/core/memory/pure_malloc.html https://dlang.org/library/core/memory/pure_malloc.html https://run.dlang.io/is/xB0Qeu https://run.dlang.io/is/xB0Qeu this RPN calculator (the one I wrote for the dlang.org page but ungolfed) is pure and checked as such by the compiler. It's also referentially transparent because "in" means the input parameter is constant and can't escape (and thread local fyi).
- sweeneyrod 6y agoI'm not convinced lack of side effects is the thing that lets you build tools that prompt removal of code in this way. In OCaml, if you had an unused variable like in the example you would get a compiler warning about it, and 99% of the time the compiler is right that you should delete the code. It's perfectly possible in principle that computing the value could have side effects. But in good (OCaml) code, you should very rarely have functions that have side effects and return a value other than unit. So the compiler is almost always right in practice (and you can still use side effects when you want to interact with the outside world, which is handy).
- habitue 6y agoThe difference between "should be safe if your ocaml code is good" and "is safe" is the thing that allows you to use tooling to delete the code. Otherwise the best you can do is lint and rely on a human to use their best judgement about whether to remove it or not.
- kazinator 6y agoAnd then there is C++. The C++ language allows temporary objects to be generated and/or optimized away. Those objects can have constructors and destructors with side effects, and those side effects appear or go away together with the object to which they are attached. If your program depends on whether a temporary object does something or not, you are officially screwed.
- sweeneyrod 6y agoelm-review still has a confirmation prompt though, right? You don't want to instantly remove an unused value, because you might be in the middle of writing code and about to use it.
- deleted 6y ago[deleted]
- kazinator 6y agoThere is no safe dead code removal. If the code being removed has any effects in it (the encoding of which is possible in a pure language), the fact that it's unreachable could be a bug. If you sneakily just remove it without telling the user, you're colluding with the bug. Recently, I discovered an like-new pair of shoes at the bottom of a closet. I bought them some four or five years ago, I guess, and somehow forgot about them. They got buried under other items. This was a day or two before Christmas; it was like an unexpected present. Now, someone could have surreptitiously entered my home at any time before that and removed these "dead shoes". I would never have known. So therefore, it wouldn't be stealing, right? It's a "semantics-preserving transformation of someone's belongings".
- setr 6y agoWell, it’s safe in that it’s correct. You could remove it without warning, and the program would behave the same. That does not preclude notifying the user, or getting confirmation
- jfmengels1 6y agoAnd that is exactly the reason why the tool from the article asks you for confirmation at every step. So that you can actually go and say "oh, no, I want to keep and use that". But even if you do blindly accept each fix, the behavior of the program won't change.
- millstone 6y agoHow do fatal errors (like asserts) work in Elm? Are assertions considered to have side effects? What if the removed function call would have produced an important error?
- Blikkentrekker 6y agoIf it be like in any other pure language, as far as the type system is concerned fatal errors do return a value. It's no different from a function entering into an infinite loop and never returning. It has a return type; it simply doesn't ever return. Some type systems go further and have the type system differentiate between total and partial functions, where total functions are guarantee to always return a value of their type eventually.
- millstone 6y agoThe post says "there is no observable difference between calling a function and not using the result, and not calling it at all." But you just described an important difference! In Haskell one must use the assertion's result to trip the assertion, which can be awkward. But Elm is strict; I wonder how assertions are treated in this sort of DCE.
- Blikkentrekker 6y agoMethnks you approach the control flow differently, as in for instance in pseudocode: assert(list.len() > 0); //code that assumes that the list not be empty Whereas in a functional language, what would happen is: if list.len() > 0 then // code assumes that the list not be empty else assertion_fail("list is not empty") Functional languages lack sequencing syntax that execute two expressions, ignoring the result of the first, altogether, and every function definition is one single expression. One does not as such first makes an assertion as the first element of a sequence, and then proceeds under the assumption that the assertion did not fail, but rather calls the `assertion_fail` function on some code paths, which then never returns if ever it be reached. The type system will call the function all the same, as as far as the type system and language runtime is concerned, it is an ordinary function that will return a value of the expected type, but under the hood it aborts the entire program, and prints proper diagnostics.