[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)