[HN Gopher] Problems with default function arguments (2020)
       ___________________________________________________________________
        
       Problems with default function arguments (2020)
        
       Author : flipchart
       Score  : 30 points
       Date   : 2023-10-10 20:21 UTC (19 hours ago)
        
 (HTM) web link (quuxplusone.github.io)
 (TXT) w3m dump (quuxplusone.github.io)
        
       | gpderetta wrote:
       | void log(const std::string_view message, const
       | std::source_location location = std::source_location::current());
        
       | qwerty456127 wrote:
       | Default arguments are used for sake of the DRY principle.
       | Whenever it is highly likely that the majority of cases of
       | application of a specific function are going to use the same
       | parameter value it can be considered reasonable to imply this
       | parameter value as a default while allowing it to be specified
       | explicitly whenever it make sense to tweak it. Using overloading
       | to implement this pattern would be way more verbose, introduce
       | unnecessary complexity and actually repeat some code.
        
         | 4hg4ufxhy wrote:
         | Let's be real, default arguments are most commonly used because
         | the programmer is lazy and doesn't want to update the function
         | call in 200 places.
        
         | Detrytus wrote:
         | Why would overloading repeat the code? It's typical to do it
         | like that (in Java):
         | 
         | public int doStuff(int a, int b) { //some code }
         | 
         | public int doStuff(int a) { return doStuff(a, 7);} // so b=7 by
         | default.
         | 
         | No need to repeat actual business logic code.
         | 
         | Of course that gets complicated if there are many parameters
         | with default values.
        
           | mattstir wrote:
           | I mean yeah, in a language without default arguments, it
           | certainly is typical to use the only other way (overloading)
           | to easily emulate that behaviour.
        
       | mattstir wrote:
       | This may just be my inexperience with the intricacies of C++, but
       | it seems like a decent number of the problems raised here could
       | be fixed by allowing one to name default args like proper keyword
       | arguments. Consider the first issue discussed and how naming the
       | argument just fixes it:                 print_square('*');
       | print_square(fill='*'); // Hypothetical fix
       | 
       | It also seems to be an issue exacerbated by C++ implicitly
       | converting a char into an int.
       | 
       | > The client programmer doesn't want a "puzzling" print_square(x)
       | that treats x sometimes as a side length and sometimes as a fill
       | character!
       | 
       | This is also fixed if C++ allowed you to name keyword arguments
       | explicitly (and didn't sometimes implicitly convert types, but
       | that's baked in pretty hard by now).
       | 
       | > The "boolean parameter tarpit"
       | 
       | Again, imagine how much more understandable it would be if you
       | could write:                 doTask(task1, 100s,
       | catchStderr=true);
       | 
       | I wonder whether the author would change their stance on default
       | args if they were made to be more usable.
        
       | theteapot wrote:
       | All C++ specific though right? Trying to imagine the insanity
       | that would be Numpy (or xyz other Scipy lib) without default
       | function args ...
        
         | boxed wrote:
         | Yea. And there's even pitfalls the author didn't list. My
         | biggest pet peeve about C++ default arguments it that they are
         | compile time inserted based on the header file. So imagine:
         | 
         | void foo(int a = 1) {}
         | 
         | Now you ship this in a DLL, but realize the default was bad, so
         | you change it to 2 and ship the new DLL. Well, the 1/2 isn't in
         | the DLL _at all_. It 's only in the header file. So everyone
         | must recompile. Fun times.
        
           | bregma wrote:
           | Technically that's a subtly incompatible ABI change. ABI
           | changes can be notoriously tricky to spot. That's handled
           | properly on an ELF-based system by bumping the SONAME major
           | (if the library developer knows what they're doing) but that
           | won't help non-ELF platforms or vendored uses.
           | 
           | Of course, the same problem applies in other languages if a
           | constant value changes. A preprocessor macros in C, for
           | example. It's all about non-manifest interfaces and "making
           | things easier" so that any idiot can write software (so they
           | do). Public APIs can be hard when the public is imperfect.
        
             | boxed wrote:
             | > Technically that's a subtly incompatible ABI change
             | 
             | Yea, that's what I said. And it's subtle because of the
             | language making it a foot gun.
             | 
             | > Of course, the same problem applies in other languages if
             | a constant value changes.
             | 
             | That's just C++ the apologist talking.
        
       | Veserv wrote:
       | As someone who does not do modern C++ development, the problems
       | they bring up seem to largely be of the flavor that positional
       | parameters work poorly with default values in C++. The reason
       | being that all default values do is give you the ability to elide
       | the last N arguments. This is much worse than automatic selection
       | of the correct overload based on the supplied arguments.
       | 
       | However, it seems to me that you could entirely resolve this
       | problem by having true keyword arguments and default arguments
       | _must_ be keyword arguments; they can not be supplied
       | positionally. If you then want some syntactic sugar in your API,
       | you can then do what the author suggests as a solution to default
       | values and supply variants that explicitly pass  "defaults" to
       | the master function.
       | 
       | Keyword arguments allows defaults while still allowing exact
       | control over the arguments, exact control over the "overload
       | selected" without explicit variants, and the ability to supply
       | explicit variants if you want API ergonomics. The only downside I
       | can see (relative to the solutions presented by the author) is
       | that you have to write some extra argument names when passing
       | non-default values to any variants that do not trivially pass
       | them through, but that is a pretty minor cost especially with a
       | IDE that can autocomplete the keywords, and it provides extra
       | useful documentation to the maintainer and client, so it is not
       | even all bad.
        
         | chrismorgan wrote:
         | Python has supported keyword-only arguments since 3.0, and
         | added positional-only arguments in 3.8+
         | <https://docs.python.org/3/whatsnew/3.8.html#positional-
         | only-...>:                 def name(positional_only_parameters,
         | /, positional_or_keyword_parameters, *,
         | keyword_only_parameters):           pass
         | 
         | I don't use Python much these days, and Rust and JavaScript
         | don't have equivalents, so I haven't thought much about this,
         | but my gut feeling is that there's probably _never_ a good
         | reason to support taking an argument by both position and
         | keyword, that it should instead be one or the other. But as I
         | say, I haven't meditated on this. Curious if any popular
         | linting tool has rules to detect any of these three classes of
         | argument (... and also _args and *kwargs)._
        
         | boxed wrote:
         | Keyword/labeled arguments are also much more robust against API
         | changes breaking silently.
        
         | jiggawatts wrote:
         | This is what C# does, for example.
        
           | gliptic wrote:
           | Not really. You can have default values for positional
           | arguments just fine in C#.
        
         | sirwhinesalot wrote:
         | In the Julia language overloading only considers positional
         | arguments (which must be given positionally) with keyword
         | arguments entirely separate (and they must be given as keyword
         | arguments).
         | 
         | I quite like that approach.
        
       | Waterluvian wrote:
       | Are there any languages that do not have positional arguments at
       | all and they're always keyword arguments?
       | 
       | My pet peeve is reading code and trying to reason about
       | foo(true, 1,  1, null, "cupcakes")
        
         | justinsaccount wrote:
         | Not a language, but I have seen some recent editor integration
         | with LSP stuff that will visually display that as
         | foo(bake: true, batches: 1,  boxes:1, frosting: null, type:
         | "cupcakes")
        
           | Waterluvian wrote:
           | Oh that's helpful. I should look into setting that for TS.
           | 
           | Also frosting can't be null when type is cupcakes. We found a
           | bug.
        
         | noelwelsh wrote:
         | I believe in Smalltalk all method arguments are by keyword.
         | Objective-C probably has a similar restriction. I've never
         | written any serious code in either, so I could be wrong.
        
       | dataflow wrote:
       | Sure, the pitfalls are real. But they have benefits too. A fair
       | assessment should examine both sides before coming to a decision,
       | not just one. Off the top of my head, these come to mind:
       | 
       | - Defaults avoid having to duplicate the rest of the function
       | (everything outside the body braces) just for the sake of an
       | additional parameter. It might not be a big deal for reset()
       | which is simple and only takes one argument anyway, but for other
       | functions it can be a lot of boilerplate to keep in sync with the
       | main overload: everything from the documentation, templates,
       | parameter names, parameter types, etc. needs to be duplicated and
       | kept in sync.
       | 
       | - Without default arguments, you lose the ability to capture
       | parameters by-value with guaranteed move/copy elision. You have
       | to capture by reference and then construct at least one instance
       | that you otherwise be able to elide. Sure, you don't need that
       | performance all the time, but that's not the point. The point is
       | there are times when you do.
       | 
       | - "Go to definition" in your IDE goes directly to the place you
       | care about; "find all references" finds all references directly.
       | 
       | - Optionals are just way easier to read. Otherwise every reader
       | or maintainer must read every other parameter and ensure they're
       | all forwarded 1:1 without side effects to understand if the
       | semantics of the call are identical with the optional parameter
       | supplied explicitly.
        
         | jstimpfle wrote:
         | It's a funny coincidence that your username is "dataflow" when
         | that is exactly what's broken with default arguments: you can't
         | pass the default values around, they can't flow in the code.
         | 
         | If you want to create a proxy function to a function that has
         | default arguments, and want to transparently allow the
         | "default" features to be used from the wrapper function as
         | well, then you have to duplicate the default value in the
         | signature of the wrapper function.
         | 
         | There are other problems, for example due to the nature of
         | function call syntax with positional arguments.
         | 
         | The solution is: Use a struct to hold default values.
         | struct FooDefaults         {             int arg1 = 3;
         | int arg2 = 7;         }              void FooFunction(int x,
         | int y, FooDefaults defaults)         {             ...
         | }              void usage_code(...)         {             int x
         | = 1;             int y = 2;             FooDefaults defaults;
         | defaults.arg2 = 9;             FooFunction(defaults);         }
        
           | gpderetta wrote:
           | void FooFunction(int x, int y, optional<int> optarg1 = {},
           | optional<int> optarg2 = {}) {          int arg1 =
           | optarg1.value_or(3);          int arg2 = optarg1.value_or(7);
           | }             void usage_code() {          FooFunction(x, y,
           | {}, 9);        }
           | 
           | I.e. for complex interfaces defaulted arguments should
           | default to an out-of-band placeholder, not to the actual
           | value.
           | 
           | I do like the struct as well, but it is still not ideal if
           | you want to use initializers. I.e this doesn't work in C++:
           | FooFunction(x, y, {.arg2 = 9});
           | 
           | You have to specify all preceding values FooFunction(x, y,
           | {.arg1 = 2, .arg2 = 9});
           | 
           | Works better with optional (and converting everything to a
           | struct):                  FooFunction({.x = x, .y=x, .arg1 =
           | nullopt, .arg2 = 9});
        
           | dataflow wrote:
           | > you can't pass the default values around, they can't flow
           | in the code.
           | 
           | I see you worked very hard on those contortions just to find
           | some way to call them dataflow ;)
           | 
           | The values can obviously be passed around just fine. The
           | issue is duplication of their source of truth, not their
           | inability to be passed around. And the duplication of the
           | source is easy enough to fix - if you don't want to hard-code
           | them then you can just make a static function (or constant)
           | that returns them so callers can refer to that same value
           | without duplicating the source of truth. No need to throw
           | entire the baby out with the bathwater.
           | 
           | (And the struct solution is an alternative to unnamed
           | arguments, not to default arguments per se. It has its own
           | advantages and disadvantages.)
        
       ___________________________________________________________________
       (page generated 2023-10-11 16:01 UTC)