17

The idea is inspired by the fact operators such as +, -,%, etc. can be seen as functions with either one or two arguments passed, and no side-effects. Assuming I, or someone else, writes a language which stops more than two arguments from being passed, and also only works via return value:

a) would such a language lead to easier to understand code?

b) would the flow of the code be clearer? (forced into more steps, with potentially less interactions 'hidden'

c) would the restrictions make the language inordinately bulky for more complex programs.

d)(bonus) any other comments on pros/cons

Note:

Two decisions would still have to be made- the first is whether to allow user-input outside main() or its equivalent, and also what the rule will be regarding what happens when passing arrays/structures. For an example, if someone wants a single function to add multiple values, he could get around the limitation by bundling it into an array. This could be stopped by not allowing an array or struct from interacting with itself, which would still allow you to, for example, divide each number by a different amount, depending on it's position.

17
  • 4
    Hi. Lists of Pros and Cons tend to make bad answers. Is there any way you could rephrase your question to still get the information you need but in another format?
    – MetaFight
    Jun 22, 2016 at 9:22
  • 22
    Your reasoning doesn't even begin to make sense to me. Some functions have few arguments so let's limit all functions? Normally when one proposes arbitrary restrictions, there is a reason, something to be gained. I can't see what this could possibly gain you.
    – user7043
    Jun 22, 2016 at 9:26
  • 2
    Not that there's anything inherently wrong with 'what if' questions (though they are sometimes hard to answer as @MetaFight said), but if even you, who thought of the thing and cared enough to ask a question, can't really name a benefit, then I'm pretty certain my initial reaction of "what? no! that's stupid why would you do that" is accurate.
    – user7043
    Jun 22, 2016 at 9:41
  • 6
    There are quite a few languages that only allow a single argument per function: anything based on the lambda calculus. The result usually is a function taking a single list argument, or a function returning a function that takes the next argument until all arguments have been processed: result = f(a)(b)…(z). This is the case in the ML language family such as Haskell, but also conceptually in other languages such as Lisp, JavaScript, or Perl.
    – amon
    Jun 22, 2016 at 10:14
  • 3
    @Orangesandlemons: Okay, then I can encode an arbitrary number of integers within a single integer using just multiplication and addition (for encoding) and division and subtraction (for decoding). So, you need to disallow integers as well, or at least multiplication, addition, division, and subtraction. (One consequence of the power of programming is that you can encode almost anything using almost anything, and thus restricting things is really, really hard. In general, restrictions don't actually "restrict" anything, they just annoy programmers.) Jun 22, 2016 at 15:36

6 Answers 6

41

Robert C. Martin in his book "Clean Code" recommends heavily the use of functions with 0, 1 or 2 parameters at maximum, so at least there is one experienced book author who thinks code becomes cleaner by using this style (however, he is surely not the ultimative authority here, and his opinions are debatable).

Where Bob Martin is IMHO correct is: functions with 3 or more parameters are often indicators for a code smell. In lots of cases, the parameters might be grouped together to form a combined datatype, in other cases, it can be an indicator for the function simply doing too much.

However, I do not think it would be a good idea to invent a new language for this:

  • if you really want to enforce such a rule throughout your code, you just need a code analysis tool for an existing language, no need to invent a completely new language for this (for example, for C# something like 'fxcop' could probably be utilized).

  • sometimes, combining parameters to a new type just does not seem worth the hassle, or it would become a pure artificial combination. See, for example, this File.Open method from the .Net framework. It takes four parameters, and I am pretty sure the designers of that API did this intentionally, because they thought that would be the most practical way to provide the diffferent parameters to the function.

  • there are sometimes real world scenarios where more than 2 parameters make things simpler for technical reasons (for example, when you need a 1:1 mapping to an existing API where you are bound to the usage of simple datatypes, and can't combine different parameters into one custom object)

12
  • 17
    The smell with multiple parameters is often the different parameters actually belong together. Take for instance the calculation of body mass index, BMI. It's a function of a person's length and weight. f(length,weight), but those two parameters really belong together because would you ever do this calculation with the height of one person and the weight of another? So to represent that better you would get f(person) where person can have a weight, length interface.
    – Pieter B
    Jun 22, 2016 at 11:10
  • @PieterB: of course, see my edit.
    – Doc Brown
    Jun 22, 2016 at 11:55
  • 5
    Tiny, tiny language nitpick: "might indicate a code smell" Isn't a code smell by definition just an indication that you should reconsider something, even if you ultimately don't change the code? So a code smell wouldn't be "indicated." If a specific aspect indicates the possibility of a problem, it is a code smell. No?
    – jpmc26
    Jun 22, 2016 at 16:16
  • 6
    @jpmc26: From another point of view, a code smell is a possible problem, not the indication of one ;-) It all depends on the exact definition of code smell (and once it smells, it has gone bad, hasn't it?)
    – hoffmale
    Jun 22, 2016 at 20:00
  • 3
    @PieterB Does anyone actually do this though? Now you have to create a new Person instance every time you just want to calculate a BMI with two arbitrary values. Sure, if your application already uses Persons to begin with and you find yourself often doing something like f(person.length, person.height) you could clean it up a bit but creating new objects specifically to group parameters seems overkill.
    – Lawyerson
    Jun 23, 2016 at 10:17
48

There are lots of languages which already work this way, e.g. Haskell. In Haskell, every function takes exactly one argument and returns exactly one value.

It is always possible to replace a function that takes n arguments with a function that takes n-1 arguments and returns a function that takes the ultimate argument. Applying this recursively, it is always possible to replace a function that takes an arbitrary number of arguments with a function that takes exactly one argument. And this transformation can be performed mechanically, by an algorithm.

This is called Frege-Schönfinkeling, Schönfinkeling, Schönfinkel-Currying, or Currying, after Haskell Curry who researched it extensively in the 1950s, Moses Schönfinkel, who described it in 1924, and Gottlob Frege, who foreshadowed it in 1893.

In other words, restricting the number of arguments has exactly zero impact.

10
  • 2
    That is if you allow functions to be returned, of course. Even if not, all you have to do is have the next function called in the main program. I.e. you could have 1+1+1 where the first addition is a function which returns a function for additional addition, or it can simply be called twice. The latter style would be, in theory, cleaner, or am I mistaken?
    – user232573
    Jun 22, 2016 at 10:58
  • 5
    "has exactly zero impact" - The OP's question was if the readibilty of the code would increase or decrease, and I guess you do not claim such a design decision for a language has no impact on that, do you?
    – Doc Brown
    Jun 22, 2016 at 11:05
  • 3
    @DocBrown With decent power and operator overloading, I can take a curry language and make it look like an uncurried language. Example: f *(call_with: a,b,c,d,e) Overload call_with : to begin a chain, , to extend the chain, and * on the LHS to invoke f by passing it each of the contents of the chain one at a time. A sufficiently weak operator overloading system just makes the syntax cumbersome, but that is the fault of the operator overloading system more than anything.
    – Yakk
    Jun 22, 2016 at 14:44
  • Actually, Haskell's currying reduces a function with n arguments to a function with one argument returning another function with n - 1 arguments.
    – Ryan Reich
    Jun 22, 2016 at 20:00
  • 2
    @RyanReich You're right that you can see a "ghost" of the "number of arguments" of a Haskell function in its type signature. It's a ghost rather than a true signature because in general you have no way of knowing whether the last type variable in the signature is also a function type. At any rate, this ghost being there doesn't invalidate the fact that in Haskell all functions take 1 argument and return either a non-function value or another function which also takes 1 argument. This is built into the associativity of ->: a->b->c is a->(b->c). So you are mostly wrong here.
    – Ian
    Jun 23, 2016 at 0:34
7

I've been spending some time these last few weeks attempting to learn the J computer language. In J, pretty much everything is an operator, so you only get "monads" (functions that have only one argument) and "dyads" (functions with exactly two arguments). If you need to more arguments, you have to either provide them in an array, or provide them in "boxes".

J can be very concise, but like its predecessor APL, it can also be very cryptic -- but this is mostly a result of the creator's goal to emulate mathematical succinctness. It's possible to make a J program more readable by using names rather than characters to create operators.

1
  • ah, so it allows arrays to interact with themselves.
    – user232573
    Jun 22, 2016 at 15:20
5

A language based around how it constrains the developer is dependent on the assumption that the language developer understands the needs of each programmer better than the programmer understands those needs themselves. There are cases where this is actually valid. For example, the constraints on multithreaded programming requiring synchronization using mutexes and semaphores are considered by many to be "good" because most programmers are completely unaware of the underlying machine-specific complexities that those constraints hide from them. Likewise, few wish to fully grasp the nuances of multithreaded garbage collection algorithms; a language which simply doesn't let you break the GC algorithm is preferred over one which forces a programmer to be aware of too many nuances.

You would have to make a valid argument for why, as a language developer, you understand argument passing so much better than the programmers using your language that there is value in preventing them from doing things you consider harmful. I think that would be a tough argument to make.

You also have to know that programmers will work around your constraints. If they need 3 or more arguments, they will use techniques like currying to turn them into fewer-argument calls. However, this often comes at the cost of readability, rather than improving it.

Most of the languages I know of with this sort of rule are esolangs, languages designed to demonstrate that you can indeed operate with a limited set of functionality. In particular, the esolangs where every character is an opcode have a tendency to limit the number of arguments, simply because they need to keep the list of opcodes short.

1
  • This is the best answer. Jun 23, 2016 at 14:08
1

You will need two things:

  • Closure
  • Composite data type

I will add a mathematical example to explain the answer written by Jörg W Mittag.

Consider the Gaussian function.

A Gaussian function has two parameters for its shape, namely the mean (center position of the curve) and the variance (related to the pulse width of the curve). In addition to the two parameters, one also need to provide the value of the free variable x in order to evaluate it.

In the first step, we will design a Gaussian function that takes all three parameters, namely the mean, variance, and the free variable.

In the second step, we create a composite data type that combines the mean and variance into one thing.

In the third step, we create a parameterization of the Gaussian function by creating a closure of the Gaussian function bound to the composite data type we created in the second step.

Finally, we evaluate the closure created in the third step by passing the value of the free variable x to it.

The structure is therefore:

  • Evaluate (computation)
    • ParameterizedGaussian (closure: the formula, plus some bound variables)
      • GaussianParameters (composite data type)
        • Mean (value)
        • Variance (value)
    • X (the value of the free variable)
1
  1. In just about any programming language, you can pass some type of list, array, tuple, record, or object as the only argument. It's only purpose is to hold other items instead of passing them to a function individually. Some Java IDE's even have an "Extract Parameter Object" feature to do just that. Internally, Java implements variable numbers of arguments by creating and passing an array.

  2. If you really want to do what you're talking about in the purest form, you need to look at lambda calculus. It's exactly what you describe. You can search the web for it, but the description that made sense to me was in Types and Programming Languages.

  3. Look at the Haskell and ML programming languages (ML is simpler). They are both based on lambda calculus and conceptually only have one parameter per function (if you squint a little).

  4. Josh Bloch's Item 2 is: "Consider a builder when faced with many constructor parameters." You can see how verbose this gets, but it's a delight to work with an API that's written this way.

  5. Some languages have named parameters which is another approach to make huge method signatures much easier to navigate. Kotlin has named arguments for instance.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.