"Crutches in language design", accidental complexity and feature overlap

A ‘crutch’ in language design is a solution to a problem of accidental complexity. Such problems wouldn’t exist with (a) the foresight to choose a better abstraction and (b) support for desired abstraction need without sacrificing other valuable properties (such as performance).

dmbarbour's new Freedom and Crutches blog post details an interesting point of language design. I found it very relevant to LtU and would be interested in more examples of crutches.

Named parameters are given as example. They help to alleviate long parameter lists, but would be unnecessary if the offending functions were reformulated with better abstractions, packing more parameters together in semantically meaningful compounds. Named parameters, he says, also have negative effects on the language:

For example, designers choosing named parameters rarely consider the impact on first-class functions and point-free function composition.

It's interesting that neelk recently used a very related argument against N-ary function types. If the alternative to n-ary functions is tuples, the alternative to named parameters is passing a record with named field. It is quite difficult, however, to do that "without sacrificing performance" -- you would need value tuples and structures, which brings important implementation subtleties.

What other crutches do you know? How should they be avoided?

I'm wondering if there are also cases of two language features that are, taken independently, not crutches, being both beneficial, but combined together create a crutch-like feature overlap.

Edit: when discussed a given language feature, please try to provide some concrete example of (one of) its intended use case(s). It always help discussion to be less abstract, and it's often interesting to ask oneself: "what is the core of this example? could a simpler facility accomplish this more specific case?".
Of course, I'm very bad at providing examples myself. Do as I say, not as I do.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Autoboxing

Autoboxing is a monster crutch added to Java 5. It's mostly unnecessary with reified generics (read: real parametric types).

The real "real"

I don't really disagree with your hating on Java's autoboxing, but I most strongly object to the implication that only reified generics deserve the name "real parametric types." In fact, I would say exactly the opposite. The desire for reification makes me highly suspicious that something non-parametric is going on...

I meant "not erased"

I meant "reified" as opposed to "erased". Type erasure in Java leads to all kinds of problems, not the least of which is that primitives cannot be used as type arguments. Obviously having programmatic access to a representation of the type arguments isn't required to have full parametric types. Unfortunately it seems the Java/C# community have the terminology all muddled up now and I never seem to be able to get across my precise meaning.

Blame in the wrong place

The ability to do a type erasing implementation of parametric types pretty much coincides with having "real" parametric types. The problem with Java is that they decided to implement type erasure using "Object", which includes most, but not all, values. If there were an actual universal type used in the implementation, type erasure would work fine.

Universal representation

Sure. A universal type implies a universal representation, which is too inefficient for some important use cases. Coercion to the universal type implies boxing (allocation) for some values for some normally non-allocating operations, which can lead to further performance troubles, e.g. in real-time settings. The conclusion I reached is that for languages that already have primitives, some polymorphic specialization is required to avoid the performance troubles with parametric types.

Named parameters

One important feature of named parameters is that they allow the unplanned extension of any function (see Keyword and Optional Arguments in PLT Scheme, p. 1). Obviously, problems with point-free composition simply do not arise in Lisp-like languages without partial application.

Warning: Semi-incoherent rambling ahead!

I think that instead of banning named parameters, we need to study them more closely. Why should passing a sequence of arguments - and nothing else - be the right thing? Would Unix be thinkable without named arguments? Data structures that blend both positional and named elements have been popularized by languages like JavaScript and Lua. Maybe it's natural for a calling convention to also have these two parts?

Thank you for the reference.

Thank you for the reference. If time allows, I'll have a deeper look at the paper, which seems, like everything PLT/Racket, very solid.

The paragraph you refer to, I suppose, is the following:

Keyword and optional arguments support tasks that need more arguments than fit comfortably into procedures, but where radiacally different forms—such as unit or class in PLT Scheme— are too heavyweight conceptually and notationally. At the same time, keyword and optional arguments offer a smooth extension path for existing procedure-based APIs. Keyword arguments can be added to a procedure to extend its functionality without binding a new identifier (which always carries the danger of colliding with other bindings) and in a way that composes with other such extensions.

The way I understand the "smooth extension" is that you can add an optional argument if you later see a need to refine some API (allow a different mode that would behave slightly differently), without having to change the existing call sites thanks to the use of a default value.

They also make a very interesting point about "composition". The way I understand it, they refer to the potential exponential explosion of the number of functions you must declare if you follow dmbarbour's advised approach, providing separate functions for slightly different modes. I think, however, that for this latter point the argumentation of dmbarbour still applies : abstract all those slightly different modes than need to be composed into a "configuration parameter" that reifies all those choice into a single value with a rich algebra of combinators to active and deactivate configuration options. This could be non-trivial to design in a sufficienly expressive (configuration options with parameters), typed way, tough.

I really think we should separate the two aspects of "named parameters" and "optional parameters", which I think have different justifications. dmbarbour points apply -- or my remark about passing structures/dictionaries with named fields -- most efficiently, I think, to named parameters. Can optional parameters be separated from the "named" aspect? It's true that naming provides a simple and efficient way to know which parameters are missing or have been added; is there another support for optionality?

Data structures that blend both positional and named elements have been popularized by languages like JavaScript and Lua. Maybe it's natural for a calling convention to also have these two parts?

Why should data structures and calling conventions duplicate work? "Just pass a data structure" looks like a good enough calling convention to me.

Obviously, problems with point-free composition simply do not arise in Lisp-like languages without partial application.

I very much doubt this is true -- or only with an overly restrictive idea of 'problems with point-free composition'. ML languages also don't have partial application if you look at them closely.

There was precisely a discussion of this -- in which you were involved -- in the LtU thread you linked; a bit too heated for my taste, but still raising the question and discussing solutions.

It's interesting regarding the former "data structure / calling convention" question to see that the solution to the problem of "define a generic function that takes a function and it(s) (named) argument(s) and apply it/them" is precisely to turn the arguments back into a data structure! This is the solution to all similar complications of the calling conventions, such as "arbitrary number of arguments" as in Lisp or Python.

Note that I'm not trying to contradict your points (or Flatt and Brazilay's). I agree that keyword parameters are not all bad, and that they also have advantages. The 'crutch' post is an occasion to think about, for some time, different ways to solve the same problems, without directly adding features to the language, or adding more orthogonal/general features.

I don't want to turn this topic into a new flamewar about named parameters only; it should be an example between others. I'll be very interested in any further discussion on this point, but please also take time to consider and discuss other aspects of language design that may or may not be considered 'crutches', so that the discussion is as rich and diverse as it could be.

You don't need named parameters for this...

...and furthermore, they have the rather nasty problem that you can only give *one* set of optional values for a function -- there's no way to have multiple sets of optional values.

Just as n-ary functions are subsumed by unary functions + tuples, so too are keywords subsumed by unary funvtions and records. Basically, once you have records and a functional record update notation, there is no need for keyword arguments.

To illustrate, here's a bit of Ocaml code (ironically, a language which has keyword arguments).

(* An named argument list as a record *)
type arglist = {foo: int; bar: string; baz: bool; quux: int option}

(* A function which takes 4 named arguments *)
let f a =
  if a.baz then
    a.foo + String.length a.bar
  else 
    (match a.quux with Some v -> v | None -> a.foo)

(* Two sets of default values! *)
let d1 = {foo = 6; bar = "Hello, World!"; baz = true; quux = None}
let d2 = {foo = 3; bar = "Yo!"; baz = false; quux = Some 67}

(* Calling a function means passing in a default record, plus overrides as needed. 
Note that the calls in x and y each use a *different* set of default values *)
let x = f {d1 with foo = 18; baz = false}
let y = f {d2 with quux = Some 3}

Excellent counter

Excellent counter example/alternative! Funny, but I've been working just this weekend on something similar, but with a little syntactic sugar sprinkled on top.(I've been reasoning along the lines of special tuple constructors instead, which one could likely relate semantically closely to your record based OCaml code).

My motivation was to permit default keyword arguments (accommodating the real world of 3rd party libraries) while avoiding N-ary functions and permitting easy application of such functions in HOF. I've also been reexamining Smalltalk's interpretation of keyword arguments (and in my ruminations, tuple arity) as simply components of a function's "real" name.

Funny coincidence

I was thinking of using this approach for a C conversion of my language. Not using it for default values at the moment, but I could.

The reason for doing it this way is that I want to be able to pass around functions with their arguments fully or partially set.

Not that nasty

In any language with default arguments, you can always change the defaults by wrapping f, calling f' if you want d2 defaults. This may have the downside of requiring you to specify every parameter of f, though, when defining f', depending on the language. And while it's certainly true that you can model keyword args with records, I don't think it's clear that using a higher order feature to accomplish something is always the right thing to do.

Disclosure: Named parameters are fundamental in my language.

Solves a different problem

This is of course a neat solution, but it doesn't address the problem described in the paper: named parameters as in Racket and Common Lisp let you extend a function's protocol post-hoc. Your approach works only if applied by the programmer a priori to every function.

Unconvinced

I could see how to use neelk's technique to add a parameter after-the-fact: if you consider that the function type, the record type definition and the set of default parameters are done at the same place -- which mirrors the way those three things are united in function definitions with optional arguments -- you only have to change this place to add new arguments with default values. The important thing is that existing call sites do not change.

Maybe you have a different kind of "post-hoc extension" in mind. To me, it only meant that if you extend the function definition with additional optional parameters, existing call sites do not need to be modified.

So you'd need to write all

The problem is that you'd need to write all functions with such a record parameter from the start. Ruby does this for you, in fact. And it has sugar for passing a record to a function. For example:

    def foo(args)
       args[:bar] + args[:baz]
    end

Now you can do:

    foo(:bar => 2, :baz => 3)

You can do optional parameters at the call site:

    defaults = {:bar => 2, :baz => 3}
    foo(defaults.merge(:bar => 4))

And/or at the callee:

    def foo(args)
       defaults = {:bar => 2, :baz => 3}
       args = defaults.merge(args)
       args[:bar] + args[:baz]
    end

This last bit is important and interacts beneficially with dynamic typing, as you often want to initialize a missing parameter based on the value of another one.

Lua is a good example

Yes, the Lua convention for named parameters follows naturally from table constructors and a little bit of syntactical sugar that allows such unary functions to dispense with the parens:


some_function { name = 'alice', age = 16 }

As observed, also popular in JavaScript and emerging organically from the language syntax, rather than a 'crutch' that was bolted on later.

Data structures that blend

Data structures that blend both positional and named elements have been popularized by languages like JavaScript and Lua. Maybe it's natural for a calling convention to also have these two parts?

Magpie, almost incidentally, works that way. I unified tuples and records in that any positional field is just a named record field with an inferred name. That mixed with the fact that all methods are multimethods that (generally) take records in Magpie means that methods can take a mixture of named and positional arguments. The fact that they are also multimethods means you can overload on those named arguments too. It leads to, I think, some nice looking code:

// mix positional and named:
collection insert("item", at: 3)

// overload by argument type:
"something" substring(from: 2, to: 5)
"something" substring(from: 2, count: 3)
"something" substring(from: 2)

I find implementing methods like substring in Magpie is a good bit simpler than most dynamic languages with named arguments since each overload is an actual distinct method with its own body and the language does the dispatch for you, instead of having a slew of if arg instance String branching inside the one monolithic method like you do in Python/JS/et. al.

unified tuples and records

I unified tuples and records in that any positional field is just a named record field with an inferred name.

The Oz language does something related: tuples are records whose field names are numbers: t.0, t.1... or maybe it starts from 1, I don't remember. It's easy to have comfortable tuply syntaxic sugar on top of that. Fields whose name is omitted are given numeric field names in left-to-right order.

I'm not sure what you mean by "inferred" here. Oz numeric field names are "implicit" more than "inferred" (there is no logic at all in the inference process). I would consider type-based field name inference to be more fragile and hence less desirable, but that's probably not what you do in an untyped language.

Standard ML

Standard ML perhaps is a more prominent example following that approach. Also, its library has long been using records to provide named parameters in exactly the way Neel was describing -- its structural record types make that very lightweight (although it lacks record updates to provide defaults).

"inferred" -> "implicit"

I'm not sure what you mean by "inferred" here.

Yeah, "implicit" would be clearer here. I meant "inferred" only in the informal sense. There's no complex magic going on.

Locking

How about object locks in a multi-threaded context?

Not that locks are bad per se but using them for anything but low level programming is awkward at best.

Synchronized vs Unsynchronized Objects

Hi,

I guess what you are refering to. Initial versions of Java only provided collection classes and string builders that were synchronized.

Although the synchronized statement has now a very good fast path in Java, still some performance penalty can be seen.

But fortunately the situtaion has improved in that new collection classes and string builders have been provided that are unsynchronized.

You can speed up your application by refactoring as follows:

Vector --> ArrayList
Hashtable --> HashMap
StringBuffer --> StringBuilder

Most of the APIs in the old classes are also found in the new classes,
except for a missing setSize() in ArrayList which could be painful.

Unfortunately this has led to a class library bloat. Don't know whether other approaches would lead to a leaner solution.

Bye

Issues with locks are well-known

Programs that use locks are not compositional, and static analysis for livelocks and deadlocks is more difficult (doesn't scale well). This is why they are 'awkward at best' for high-level programming. People might be willing to put up with locks for low-level programming, until they find bottlenecks and realize those bottlenecks can be solved by a scalable, lock-free data structure.

Rich Hickey and Cliff Click had a pretty long debate about this. Ultimately, they both agreed that giving locks to John Doe average programmers turned out to be a bad idea in practice. Cliff refers to this mistake as the JVM engineering decision to give locks the illusion of being cheap to acquire/release. It was a mistake, because programmers would then slap locks haphazardly everywhere.

VM design / Debugging Tools

What I admire at cliff click is that he adopts a high level view of VM design. And really critically thinks about functionality allocation inside and outside of the VM.

BTW: Not only John Doe is confronted with the problem. I know a very big company with a number highly qualified engineers, their VM still hangs in certain situations when it should quit on its own, when all GUI interaction is terminated and only daemon threads are running..

So its a problem at the macro level and not at the micro level. What can help? Language design? Or program verficiation and testing discipline? Where is the border between language design and program verification? What is feasible?

Maybe start with better debugging tools. That record the execution trace of all threads until the point of interest. So that we can look at it like watching a film. One shot stack traces for all threads does not give this information. Maybe John Doe would also use such tools.

Not very good post

Not very good post: one feature viewed by someone as a crutch is for someone else an important tool, how do we distinguish "good features" and "crutches"?

Let's see:
- default value for parameters: dmbarbour is against default value for parameters saying that you can change your API to reduce their use. Maybe, but is-it done in practice in language without this feature?
Having used C, my opinion is no, or not enough.
So languages without this feature are just a pain to use, they don't really have better APIs.

- named parameter: he is saying that it leads to APIs with too many parameters.
But:
1) again I've seen a lot of these kind of functions in C.
2) named parameter reduce the number of "wrong order of parameter" problems (and helps finding those issue during code review) even with functions with a small number of parameter.

[somewhat OT] OpenGL

I realize that glTexImage2D was just a convenient choice of a function with many parameters. Still, it is interesting to note that glTexImage2D and other OpenGL functions have this particular flavor for an important reason: portability.

OpenGL is designed so that it depends as little as possible on the details of the language, compiler and platform. Any compiler that can generate code that conforms to the platform ABI and "C" calling convention can call into OpenGL. If OpenGL had, e.g., made use of structs to bundle together "named" parameters, it might have a cleaner API, but then it would be at the mercy of how particular languages/compilers decide to lay out those struct members.

DirectX seems to be based on

DirectX seems to be based on COM for this reason, but then it doesn't need to be really portable either.

Patterns

Automatic memory management schemes bolted onto Objective-C and C++.

Java's inner classes instead of closures.

Grafting immutability in a language with aliasing.

Not sure if design patterns count as a crutch; they are not bolted on to the language per se, but they are fixes at some level to problems caused by the language design. For example, I think lack of tail calls gives rise to a plethora of crutches, such as having to use set/longjmp, or Protothreads etc.

Re: inners

Hilarious

Funny, there are no authors on the document....I wonder who wrote this. I have my suspicions :)

Simple made easy

"Simple made easy", a presentation by Rich Hickey (Clojure) at strangeloop has some interesting points about complexity in general.

At around minute 29 he ponders on easy vs. complex things. Objects & state are more complex than values. Loops more complex than set operations, and so are conditionals vs. rules, etc.

Maybe it's worth a read to avoid crunches in language design, by a language designer.

I saw the talk ref'd on

I saw the talk ref'd on hackernews but haven't been able to watch it yet do to the Chinese Great Firewall.

Some of it sounds biased. Loops are less simple than set operations, but they are not necessarily less easy. People get loops quickly, they are very concrete, and have to adopt an FP mindset to get set operations, which are more abstract. But maybe Clojure is optimizing for simple over easy?

Scheme makes loops more

Scheme makes loops more difficult than map and apply too, I think.

Crutches as a design technique!

I really like the concept of crutches! Thanks! In fact I like it so much I want to elevate it from a language design fault to a language design technique!

The idea is: I have some feature that doesn't quite do what I want in some circumstances so instead of thinking about it too much, I add another feature that does do what I want, but which overlaps with the first feature. In doing so I am creating a crutch, deliberately.

Having done this I now have a concrete and operational way to actually study the crutch generating constructions and try to generalise and abstract them to eliminate the crutch. And in particular I can defer that study for a while, creating use cases for later correction. Eventually as the number of cases builds up, the pressure to unify builds up, but so too does the amount of data, increasing the chance a pattern will emerge from the code (instead of my brain, which, recall, failed to see one originally).

Here's an example:

iter (proc (a:int) {print a;}) (list_upto 10);
for var i in 0 upto 10 do print i; done

This is my language Felix but similar code exists in Ocaml.
You can use a List iterator HOF but making a list of 10 integers is expensive, and the syntax is not very clean: the for loop is more readable and efficient but only works for integer sequences.

There's also a more general solution in Felix:

{
  var ch = mk_fchannel[int](); 
  spawn_fthread { forever { var i = read ch; print i; }; };
  for var i in 0 upto 10 do write$ ch,i; done
}

which subsumes the others and is very efficient, but is more verbose for simple cases.

The first two forms are a crutch because they're quite distinct implementations, instead of being mere syntactic sugar. Quite apart from needing to create this sugar the optimiser would need to be taught to recognize the continuation based solution and reduce it to a simple imperative loop.

The apparent lockup waiting for the value after 9 does not occur, since just after this point the generating loop drops off and the reference to the channel ch is lost, so the spawned fthread is simply garbage collected. I/O on fchannels is a synchronous exchange of control.

Whilst I do not currently have a good solution to this problem it is clear from the "crutches" the the ability to remove the explicit use of the channel is a thing to think about.