Comment by jollybean
5 years ago
Yes and no.
What I find is that function boundaries have a bunch of hidden assumptions we don't think about.
Especially things like exceptions.
For all these utility functions are you going to check input variables, which means doing it over, over and over again. Catching exceptions everywhere etc?
A function can be used for a 'narrow use case' - but - when it's actually made available to other parts of the system, it needs to be kind of more generalized.
This is the problem.
Is it possible that 'nested functions' could provide a solution? As in, you only call the function once, in the context of some other function, so why not physically put it there?
I can have it's own stack, be tested separately if needed, but it remains exclusive to the context that it is in from a readability perspective - and you don't risk having it used for 'other things'.
You could even have an editor 'collapse' the function into a single line of code, to make the longer algorithm more readable.
The problem is abstraction isn't free. Sometimes it frees up your brain from unnecessary details and sometimes the implementation matters or the abstraction leaks.
Even something as simple as Substring which is a method we use all the time and is far more clear than most helper functions I've seen in code bases.
Is it Substring(string, index, length) or Substring(string, indexStart, indexEnd)
What happens when you pass in "abc".Substring(0,4) do you get an exception or "abc"?
What does Substring(0,-1) do? or Substring (-2,-3).
What happens when you call it on null? Sometimes this matters, sometimes it doesn't.
Also:
- Does it destructively modify the argument, or return a substring? Or both?
- If it returns a substring, is it a view over the original string, or a fresh substring that doesn't share memory with the original?
- If it returns a fresh substring, how does it do it? Is it smart or dumb about allocations? This almost never matters, except when it does.
- How does it handle multibyte characters? Do locales impact it in any way?
With the languages we have today, a big part of the function contract cannot be explicitly expressed in function signatures. And it only gets worse with more complicated tools of abstraction.
I posted this elsewhere in the thread, but local blocks that define which variables they read, mutate and export would IMO be a very good solution to this problem:
There are a couple of newer languages experimenting with concepts like this, Jai being one: https://youtu.be/5Nc68IdNKdg?t=3493
This is a fascinating idea. In some languages like C or Java or C#, the IDE can probably do this "for free" -- generate, then programmer can spot check for surprises. Or the reverse, highlight a block of code and ask the IDE to tell you about read/mutate/export. In some sense, when you use automatic refactoring tools (like IntelliJ), extract a few lines of code as a new method needs to perform similar static analysis.
In the latest IntelliJ, the IDE will visually hint about mutable, primitive-typed local variables (including method parameters). A good example is a for loop variable (i/j/k). The IDE makes it stand-out. When I write Java, I try to use final everywhere for primitive-typed local variables. (I borrowed this idea from functional programming styles.) The IDE gives me a hint if I accidentally forget to mark something as final.
> local blocks that define which variables they read, mutate and export would IMO be a very good solution to this problem:
this is basically a lambda you call instantly.
It's similar, but lambdas don't specify the behaviour as precisely, and they're not as readable since the use of a lambda implies a different intention, and the syntax that transforms them into a scope block is very subtle. They may also have performance overhead depending on the environment, which is (arguably) additional information the programmer has to consider on usage.