Hacker News new | ask | show | jobs
by bjourne 1740 days ago
Named parameters misses the point. Functions should do one thing and only one thing. This is why we have cos, sin, and tan and not a universal function for trigonometry: trig(mode='cos', ...). Such functions often become cumbersome to use since they are essentially multiple functions baked into one.
4 comments

Religiously splitting functions with boolean arguments doesn't always result in more maintainable code.

Instead of trigonometry functions, how would you refactor JS's fetch() with many of its behaviour-altering flags?

> how would you refactor JS's fetch()

as @flavius29663 said (https://news.ycombinator.com/item?id=28593669) you can use the builder pattern

    FetchBuilder()
      .withUrl(ur)
      .withMode("cors")
      .withCache(true)
      .withHeader('Content-Type', 'application/json')
      .accept('*/*')
      .post()
      .then(response => response.json())
      .then(data => console.log(data));
I'm not following how moving the options out of the function parameters and into the call chain makes the actual function more maintainable. It's still doing the exact same thing with the exact same options it's just pulling them from elsewhere. If anything you now have more functions to maintain on top of the function that does many different things based on the calling info.

    // The original "misses the point"
    trig(mode="cos", type="hyperbolic")
    
    // The style fetch() uses today
    trig({mode: "cos", type: "hyperbolic"})
    
    // The builder refactor
    trigBuilder().withMode("Cos").withType("hyperbolic").calculate()
Personally I don't like using with prefix in builders, I was simply presenting an example from another comment.

the difference, IMO, is - in Javascript - that this

    // The style fetch() uses today
    trig({mode: "cos", typ: "hyperbolic"})
                       ^^^
would fail silently while

    // The builder refactor
    trig().mode("cos").typ("hyperbolic")(val)
would trigger a compilation error

but, IMO, passing objects is good enough most of the times, and I consider it a much better solution over passing boolean flags

I see the builder pattern as a way to manage lack of keyword arguments. I see very little difference between your example and the actual fetch API that takes an object as JS's version of keyword arguments.

Languages with good support for named/keyword arguments have more features such as required arguments and preventing duplicate arguments. With builder patterns your only real option is to make the builder constructor have required arguments (or throw a runtime error upon finalizing the builder).

> . I see very little difference between your example and the actual fetch API that takes an object as JS's version

true

the only difference is in the tooling

code completion for methods names works much better than autocompletion for objects' fields.

And you can't mistype a method name, it would not run and give you back a - hopefully - meaningful error, while the same is not true for objects' fields.

> code completion for methods names works much better than autocompletion for objects' fields.

That is true for vanilla JS, unknown parameters will be ignored and unset will be set to undefined. However for languages that support keyword arguments (or even TypeScript[1]) the tooling should be even better than for the keyword argument case.

[1] https://www.typescriptlang.org/play?#code/GYVwdgxgLglg9mABMO...

Good rules of thumbs are given in the "Deciding which to use" section of the article. For the fetch() function, I'd keep most parameters as is since they don't change the essense of the function. But "cache" and "redirect" do (following redirects can cause N http requests rather than just one and using the cache perhaps 0) so I'd refactor them as new functions. Imagine adding retry functionality to the fetch() function using parameters. I think you can see how this leads to feature creep.
I do agree with you completely. But the problem here is as old as programming itself. It's not always easy to define function boundries.

If your function returns pizza, as mentioned in other comment, adding some toppings won't change the boundaries. It still returns pizza, with peperoni or not. It doesn't change how you make the pizza. You're just adding more data to it. You may solve it with syntax, language data structures, etc. Whatever you like to make it more readable to the caller. But probably you'd want to pass toppings in arguments.

On the other hand if you have a boolean param that changes how function works. That's questionable in my opinion. Say you want to return list of users from DB but omit interns, sometimes, and you need to call API (or query DB) to know if someone's intern. You could define `omitInterns` bool argument but it seems clunky to me.

I may be mistaken, though. As said: defining boundries is not easy.

This also touches other problem a bit. Should we strive to decrease `if` branching in our functions or not? I personally tend to branch very early on, so later I can follow straight path. That's not always possible, but if it is, it helps greatly. Makes code easier to follow.

A valid use case for functions with many arguments are setup/init/creation functions which sometimes take dozens of configuration options. It's definitely better to do such setup operations in a single 'atomic' function call than spreading this out over dozens of configuration functions where it isn't clear when and in what order those should be called, or a combinatorial explosion of atomic setup functions, one for each valid combination of setup options.
function pizza(boolean pepperoni, boolean bacon, boolean mushroom, boolean artichoke)

now becomes 16 distinct functions.

    pizza_with_pepperoni_and_bacon_and_mushroom()
Wow glad I took out those boolean params!
pizza is very suitable for the builder pattern: CreatePizza() .WithBacon() .With(artichoke) .Build();

or any combination of the above: CreatePizza() .WithMussroom() .Build()

Even better, you can add new ingredients without changing any of the existing signatures: CreatePizza() .WithProsciuto() .WithTomatoSauce() .Build()

What is the difference of this and doing an object paramater, similar to JS?

createPizza({bacon: true, artichoke: true});

This has the same benefit you describe of being able to add parameters without altering any old call sites

createPizza({prosiuto: true, tomatoSauce: true});

Not OP, but it's useful for if:

- something is done to object when some props is set or validation, such as .withStuffedCrust("cheese"), it'll set the internal props as crust="stuffed" and stuffContent="cheese"

- it's branching. So rather than making user looking for the components or configuration themselves, library author can guide them with builder. Such as:

  myVehicleBuilder.withSixTires().withTrailerAttachment().attach(container)
In this case withTrailerAttachment (and possibly withOpenBack or withBox) won't show up if you call withTwoTires(), and attach won't show up if you don't call withTrailerAttachment().
In my example the could matter, or not. I see this all the times. In your example, can you make the order matter? In my example, after each selection, you can limit or expand the further options.
Nothing to stop you from doing the same within the code of the multi-parameterized single function, is there?

  if Shrimp in Fillings then begin
    Add(Shrimp);
    Add(ShrimpOil);              // So yummy together
    Fillings.Remove(Jalapenos);  // Don't go together
  end;

  if Jalapenos in Fillings then begin
    Add(Jalapenos);
  end;
etc. No?
You just moved the problem to a different layer.

In your case you would end up calling your function like this: DoToppings(true, false, true, true);

Whereas in my case you would call the builder directly with (only) the params you want different than the defaults.

You could use named arguments, but that doesn't solve the problem completely. You will still have a large method signature, hard to use, harder to refactor, harder to test, error prone.

It also causes a lot of redundant code: the called usually only cares about one or 2 arguments, it's rare that you *need* to pass more. The rest of the args are filled in by defaults in the implementation. There could be elegant ways to handle the defaults, like overloading methods or just passing in defaults in the method signature. Default value are pretty bad IMO, for all the reasons above. Btw, if you *need* to pass that many arguments to a function, that is another code smell worth it's own discussion.

Multiple overloaded methods could have about the same amount of code int he implementation like the builder pattern, but they have a huge drawback: the caller cannot mix and match which arguments they want to pass in. If you have 4 arguments, there would be quite a high combination of parameters (18 possibilities? - 18 functions); Using the builder pattern you have to implement 4 methods only, and you're covering all the possible combinations the client might want. You can also limit some combinations in elegant ways right in the IDE while the developer is writing the code.

Think of FluentAssertions https://fluentassertions.com/introduction They have literally hundreds of possible assertions that are represented by object instances. You can combine them in an almost infinite number of possibilities.

Sure, there is no black and white, and depends on the language, the builder pattern is a good tool to have in the toolbox.

not necessarly.

First of all, this

    function pizza(boolean pepperoni, boolean bacon, boolean mushroom, boolean artichoke)
breaks down when you want to add ham, potatoes and sausages to the pizza.

Secondly, you can optimize for the common case:

    fn pizza() # -> default pizza e.g. margherita
    fn pizza(list_of_ingredients) # -> your custom pizza

if you we are talking of simple functions and not more complex patterns, such as piping, in Elixir I would do

    pizza |> add_ham |> add_mushroom |> well_done
when using boolean parameters you are also passing down a lot of useless informations (a pizza with pepperoni would include 3 false just to remove the ingredients from the pizza and only one true) and confining yourself to a fixed set of options, that could possibly also represent an impossible state.
What kind of monster puts potatoes on a pizza?
You don't know what you're talking about :)

In the image: pizza with potatoes, a typical roman recipe

https://i0.wp.com/www.puntarellarossa.it/wp/wp-content/uploa...

I don't care who invented it. It sounds fucking terrible.

You might as well put some mashed potatoes in your rice and some pasta in a sandwich while you're at it.

It's like someone said "what type of carbs would you like with this meal" and the answer was "yes".

> I don't care who invented it. It sounds fucking terrible.

Sorry, but why should people care about what sounds terrible to you?

> put some mashed potatoes in your rice and some pasta in a sandwich while you're at it

If you weren't too obsessed with yourself, you'll know that that pasta actually exists, it's called "pasta e patate" and someone has put it in a sandwich for sure...

there is also a very popular variant made of pasta, potatoes and mussels.

> It's like someone said "what type of carbs would you like with this meal" and the answer was "yes".

It's like someone asked you "what are you first World problems" and your answer was "yes"

The recipe I'm talking about come from Italian rural tradition, when people were poor and carbs were the only thing they could afford to eat to keep being alive, not a privileged people's self inflicted fictional problem.

Sorry for the brutal honesty.

But you anglophones are not qualified to judge other culture's food. Your food is usually terrible.

I love potatoes on pizza and order it at multiple pizza places. They add flavor and creaminess. The secret is to cook the potatoes properly and not just throw some french fries on the pizza.
But it should just be:

  function getPizza(toppings: Iterable<Topping>): Pizza {
    ...
  }