Freedom and Crutches in Language Design

Language designers aim to optimize an experience for their prospective users, but the criteria and heuristics by which they ‘optimize’ are widely varied and inconsistent (often within a single designer). In this article, I’ll present two common points of contention and my opinion on them.

Freedom

First is picking a flavor of freedom:

  • Anarchy: A language by hackers, for hackers. The individual developer has privilege and power and ambient authority. Typing is a guard rail that developers are free to step across where it suits them. Ad-hoc language extensions and transforms are feasible by use of code-walking macros.
  • Liberating Constraint: The language abridges the authority and power of individuals in order to protect system-scale properties such as safety, security, consistency, resilience, optimizability, and immunity to deadlock.

Both of these can improve developer productivity. Anarchy gives the individual more power in an immediate sense – there is rarely any need to redesign a poorly chosen set of abstractions to fit some forgotten corner case. Liberating constraint aides the developer by ensuring high-quality libraries, and supporting local reasoning about application behavior without reading the details.

This isn’t a binary choice. Some language designers will choose the constraint route but add an escape hatch, like unsafePerformIO in Haskell. Given a large set of system properties, a language designer might choose to protect some of them and leave others to developers.

I quite firmly favor liberating constraints, without any ambient escape hatches. I believe individual developers will achieve greatest productivity and flexibility by having a massive pool of libraries and resources they can use securely, safely, and effectively – even in the absence of ‘trust’. Capabilities and RDP demands do offer me a secure mechanism to provide reflection, meta-object protocols, and other ad-hoc effects if necessary – but entirely within the rules of object capability model and RDP.

Crutches

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

One example of a language crutch is keyword and default parameters, which simplify development work with large parameter lists. The function glTexImage2D takes nine parameters, for example, and rather than:

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 256, 256, 0, 
  GL_RGBA, GL_UNSIGNED_BYTE, pData)

We could use:

glTexImage2D(target:GL_TEXTURE_2D, internal:GL_RGBA, 
  height:256, width:256, 
  dataFormat:GL_RGBA, dataType:GL_UNSIGNED_BYTE, theData:pData)

I’m assuming here that the `level` and `border` parameters default to 0. Obviously this latter form is more verbose, even with the defaults. But many would claim it easier to read, write, and maintain – properties that some language designers judge more important than terseness. Developers might still have access to ordered parameters, if that is their preference.

Okay, so this was an improvement according to some people. But this is effectively a band-aid for the real problems:

  1. glTexImage2D does not use a texture abstraction. A simple refactoring could capture height, width, border, data format, data type, and the buffer itself into a value abstraction of type `Texture`.
  2. Two parameters – level of detail number, internal format – are effectively optimization switches that, under ideal circumstances, could be avoided or at least separated.
  3. The ‘target’ type is very often GL_TEXTURE_2D. It would be trivial to shift the case of GL_TEXTURE_CUBE_MAP* into a separate function.

In practice, it is feasible to reduce the code to simply: glTexImage2D(texData), by separating the optimization parameters, shifting the cube maps to another function, and capturing the texture information in a dedicated value abstraction. I think most cases of large parameter lists have similar issues, such that better abstractions would avoid the issue. After all, DSLs are typically built of a medium-size vocabulary (e.g. 30-60 elements) of simple, composable functions and primitives.

Naturally, designers lack perfect foresight; languages and abstractions will be imperfect. There will always be some application for crutches or band-aids. The question is whether one should supply such crutches. Regarding crutches:

  1. The existence of a crutch reduces incentive to solve the problems. By nature, use of the crutch must be easier than solving the problem. Unattended problems tend to accumulate. Developers become more dependent on the crutch as the number of half-abstracted libraries grow.
  2. More features means more complexity. Due to feature interaction, this is a combinatorial concern, and thus the extra features will hide flaws and complications in the language design. For example, designers choosing named parameters rarely consider the impact on first-class functions and point-free function composition.
  3. Crutches tend to be ‘optional’ in a bad way: the decision has no semantic impact beyond apparent, immediate convenience to the user. They can easily become a source of decision fatigue.

Sadly, language designers and users tend to see use of a feature as proof of its success and utility, without ever recognizing the larger self-fulfilling prophecy or the implications of the fact that a language-crutch is so widely needed.

I do not favor ‘crutches’ in language design. In their absence, developers might instead use boiler-plate, frameworks, or design patterns. I’ve done my best to support proper abstractions in these roles: user-defined syntax (per module), reactive semantics (which covers many frameworks as plain old abstraction, and makes frameworks more composable), easy scatter-gather between data models (to maintain alternative views and abstractions), and securable interaction with ad-hoc foreign services (which can then cover corner cases).

I believe that avoiding crutches (and their obfuscating complexity) will make the weaknesses and pains of a language more obvious, make it easier to identify and resolve most problems by use of libraries, frameworks, and syntactic abstraction, and to catalog the solutions that did not abstract effectively or required too much external support. Developers will have greater awareness of weaknesses in their libraries, and greater incentive to fix them. As the libraries and services improve, so will developer productivity; they won’t be hindered by dragging a crutch.

This entry was posted in Language Design, Types. Bookmark the permalink.

19 Responses to Freedom and Crutches in Language Design

  1. John Shutt says:

    Your ‘crutch’ concept seems to me quite valid and useful (and thanks btw for ‘accidental complexity’, a term I hadn’t encountered which fits in neatly with some big-picture I’ve been wanting to express for about 25 years and will hopefully get to blogging about one of these times :-)). It’s worth noting that, as may happen with general classes of design problems, crutches aren’t recognizable in general by simple objective criteria — it may be important to learn the fingerprint of indirect symptoms of a crutch.

    Your factoring of freedom into two flavors seems to me to somewhat hide an option, by structuring it into invisibility. Which is interesting, because the option I have in mind is what one might call “guiding structure”. It’s not a “third flavor”, but rather, it’s one of three flavors in an alternative factoring; it borrows a bit from each of your two. The term “anarchy” may or may not imply absence of structure, while the term “constraint” may or may not imply fundamental limitation. But there is a coherent design strategy in which structure guides without fundamentally limiting. This strategy has its own peculiar flavor, its own signature of “pros and cons” (or perhaps “benefits and difficulties” is a more positive way of thinking about it :-)) that distinguish it from the two flavors you mention.

    • dmbarbour says:

      I agree that recognizing a crutch, distinguishing it from other features, isn’t very simple. Part of the problem is that requirements analysis, and recognizing essential complexity, is not simple.

      I also agree that there can be subtle guidance and systematic pressure, i.e. by controlling the paths of least resistance, ensuring the `right thing` usually intersects with the `easy thing`. This is actually a primary guiding principle of my language and API design philosophy, and of secure interaction design. But I don’t see guidance as a substitute for constraint, and guidance seems orthogonal to freedom.

      • John Shutt says:

        Well, we clearly don’t want to get bogged down in terminology, like how the word “freedom” should apply to PLs. But, now you mention it, although somewhere in the vicinity of the words “guidance” and “limitation” there may be two clearly different *strategies*, the language features they give rise to may be difficult or impossible to distinguish.

        The strategies are to impose a structure on things (with the intent that that structure will guide programming), or to impose limits on what can be done. Whatever you call those two strategies. But since both tend to be viewed in the language as type constraints, it isn’t obvious how to distinguish between a type constraint that’s meant to guide, versus one that’s meant to limit. I’ll try an example. With the Kernel environment type, (a) there is no way to enumerate all the bindings in an environment; and (b) an environment can only be mutated if one can acquire that environment as a first-class object. I feel (b) is more guidance rather than limitation. I’m less sure about (a); it seems a deeper shade of grey, leaving me really no basis to call it one or the other. Probably this too isn’t a binary choice —a feature may be a mix of the two, or perhaps a fundamentally entangled blend of them— and it’s hard to envision criteria for how far any given feature engages each.

      • dmbarbour says:

        I think it useful to accept a more multi-dimensional view of structure – not just “limits on what can be done”, but also on the who, when, where, how. Object capability model certainly creates some strong limits, but not so much on `what` can be done. Your ‘shades of grey’ are a lossy projection from a rich, multi-dimensional structure.

        The distinctions I make are more along the lines of soft vs. hard, weak vs. strong, preference vs. constraint, guidance vs. limitation. I’m quite willing to leverage hard techniques to protect the most critical system-level properties, but I prefer soft structure for everything else – e.g. ensuring that the preferred path is less verbose, or allowing explicit delay to absorb the rare multi-observer glitches that get past vat-layer snapshot consistency (e.g. where Alice’s view of Charlie is different from Alice’s view of Bob’s view of Charlie) rather than trying to make a strong guarantee against glitches.

      • John Shutt says:

        Just to float a thought. The subjective difference between guidance of limitation may be, at least largely, a matter of seamlessness as viewed from the inside. Say we have a Turing machine. If we’re not allowed to write on a blank cell on the tape (which would effectively make the machine a linear bounded automaton), that seems to be a limitation, because it seems kind of arbitrary when looked at within the framework of the machine model. But the fact we’re not allowed, say, to move the read/write head vertically as well as horizontally (so the tape is 1D instead of 2D) doesn’t feel like a limitation, because the thing being “prohibited” isn’t something that would occur to you to want to do unless you’d already stepped outside the machine model.

  2. I like keyword args. I find them useful even in functions with few parameters, often macros with one or two parameters and a body. For example:

    http://github.com/akkartik/wart/blob/460565a2e0/063http-get.wtst#L8

    I prefer to express the http server as (accepting stream :from port …) rather than (socket-accept port …)

    • dmbarbour says:

      I would suggest use of a parameter object, if that is your preference. A more first-class treatment of named parameters will avoid a lot of the hidden issues with first-class functions and composition.

      • I find that allowing any parameter to be used as a keyword arg helps sidestep composition issues. Function calls parse only keywords they recognize, otherwise they’re passed straight through. In particular, (define (foo args) (apply bar args)) works even when args includes keywords. Am I missing anything?

  3. Kevin says:

    “I quite firmly favor liberating constraints, without any ambient escape hatches.”

    I agree, but for a more fundamental reason. The problem with anarchy is that it is a big black box to the constraint satisfaction system. The only reason for “anarchy” and “escape hatches” is because properly modeling and changing the constraints of a managed system is too difficult or impossible. Make that easy and it can all be “liberating constraints”.

    Now, you can “make that easy” by essentially having a huge library that covers everything the programmer might need, but that answer is true of any language! If the language or library designers had already thought of everything, you wouldn’t need an escape hatch. The point is that no one can think of everything that another dev might need.

    The other way to “make that easy” is easy formal semantics along with automatic constraint satisfaction. Modeling the anarchy in a language that can be understood by the system allows automatic adaptation to manage it. The key is to lower the communication barrier and identify similarities already extant in the modeling system.

    Regarding named parameters, hopefully I’ve already addressed all your points on PiLuD. Please let me know if I missed any. I’ll just reiterate a few items here:

    (1) Naming parameters helps even with essential complexity. You would deprive people of that because you do not like its use in other cases.

    (2) By reducing the number of parameters of glTexImage2D, you are increasingly relying upon the name of the function to localize intention. Moreover, you actually are naming the parameters, you are just placing them in a deeper structure. You’re just requiring packaging and unpackaging of the named parameters.

    (3) Binding is not usually semantically relevant. If it were, the compiler could not inline functions thereby eliminating the binding altogether. Encapsulation as you define it (not changing the client) is often an unnecessary requirement and will become increasingly so in the future. The important point is maintaining semantics.

    In general, I think you are conflating the language (models) that the dev uses to communicate with the computer with the models that the computer uses to effect the dev’s intentions. They are clearly intimately related, but they need not be the same.

    I realize this is a different way of thinking about programming, but I think it is far better.

    Kevin

    • dmbarbour says:

      you can “make that easy” by essentially having a huge library that covers everything the programmer might need, but that answer is true of any language!

      I would not posit one huge library, but rather dozens of small ones.

      Unfortunately, it is not practical in every language. Many languages have a problem where layers of abstraction have a performance penalty, or hide too much power. Some languages force developers to tightly couple the details – e.g. due to insufficient support for generic programming or automatic memory management. Not all languages support composition. Consequences include that code becomes specialized based on its context, and less widely reusable.

      you are increasingly relying upon the name of the function to localize intention

      Localizing expression of intent is a good thing. Naming localized expression is the basis of abstraction. But reducing the number of arguments actually reduces dependency on glTexImage2D in particular – i.e. since I’ve reduced binding to details unique to creating a texture object, it is feasible to model a series of texture transformers. (Or, it would be if details such as allocations were less relevant.)

      easy formal semantics along with automatic constraint satisfaction

      I do pursue limited use of constraint satisfaction in language designs, e.g. for linkers or matchmakers and stable state models. But I do not believe that names will help such pursuits, unless you’re willing to get hand-wavy about semantics.

      • Kevin Edwards says:

        I didn’t expect you to use one big library, and from what I’ve read so far, your language will be far better than most.

        Localizing expression of intent is a good thing. Naming localized expression is the basis of abstraction.

        I agree. Names localize intent, so why not permit them? All I was saying is that when you reduce the number of parameters to 1, you can refer to it indirectly using the function name.

        But I do not believe that names will help such pursuits, unless you’re willing to get hand-wavy about semantics.

        Names are useful for humans. That’s all. Am I hand-waving? Is there something hand-wavy about the fact that inlining functions eliminates its binding style while maintaining semantics?

        I do prefer the abstractions you came up with. e.g. a Texture object. People would probably use that even if you allowed named parameters. In any case, if you want to reduce every function call to a single structured, unnamed parameter, with all the real dependencies named somewhere within that structure, go for it.

        Ah, well, it’s been fun and only slightly frustrating discussing it with you. 🙂

      • dmbarbour says:

        Names localize intent, so why not permit them?

        Because there are other criterion by which to judge the quality of a language. Developers need a means to express intent, but attempting to optimize on this one criterion may hinder achieving others. Named parameters seem to hinder composition, modularity, and language simplicity. There may be other means that achieve the same ends (perhaps clever use of typeful programming) without hindering composition.

        inlining functions eliminates its binding style while maintaining semantics

        Not quite. Inlining eliminates binding style while preserving behavior according to a semantics.

        Anyhow, this article isn’t really about named params, so I think this line of comments is at its end.

  4. dmbarbour says:

    There is more discussion of this article at Lambda the Ultimate.

    Some might claim a C language serves as a counterpoint: the large multi-parameter APIs precede existence of named parameters, after all. But I would note that C has other problems that hinder pursuit of the ideal abstraction – among them, a lack of first-class functions and function composition, lack of generics, tight coupling of representation and abstraction, difficulty to encapsulate construction of heap-allocated arguments without memory leaks, aliasing and mutable state to hinder cross-structure optimization or specialization, etc.

  5. renoX says:

    Named parameter increase the readability of functions calls: when two parameters have compatible types, it’s quite common to have a programmer inverting the values, the type system doesn’t help here but named parameter would reduce the number of mistake and make it quite easy to find the remaining mistakes during code reviews.

    • dmbarbour says:

      I agree that named parameters can resist transposition errors and potentially improve readability. But there are other ways to achieve these same ends that do not share the composition weaknesses of named parameters, including use of parameter objects with lenses or method chaining.

      • renoX says:

        Remember that some compilers includes ‘real crutches’ to try to detect parameter inversion in *memset*!
        Do parameter objects with lenses or method chaining help in this case?

      • dmbarbour says:

        C is a very weak language for abstraction. `memset` itself is a crutch.

  6. yttrill says:

    I hate to divert from the excellent notion of crutches, but named parameters need not lead to compositional weaknesses: they do not do so in Felix, where the use of the names is an aid to readability, saves getting the positional ordering right, and can help to resolve ambiguities in overloading .. but is not type information and has no impact on composition: the argument is still a tuple (not a record!).

    • dmbarbour says:

      Compositional strength means having useful compositional properties. For example, deadlock-freedom with mutexes is `non-compositional` because two individually deadlock-free subprograms can deadlock when composed. In non-compositional systems, valuable properties become context-dependent, and so code must often be rewritten to use it in a different contexts. This impacts code reuse – abstraction, modularity – in a negative way.

      If you lose access to named parameters when composing functions – i.e. if `f . g` does not have the same named parameters as `g` (for composition operator `.`) – then named parameters are both non-compositional and serve as disincentive for developers to use composition. (Relevantly, developers must choose between the benefits of composition and the benefits of named parameters.) Similarly, if you lose names when passing functions as first-class arguments (because it isn’t part of the type information) then developers have a disincentive to abstract functions. Second-class function overloading has many of the same problems.

      That said, I do recommend use of tuples or records rather than named parameters. And I’d suggest use of lenses, too, rather than plain old tuples or records. A lens is a first-class function pair to set and get elements of arbitrary depth in a complex tuple or record value, sort of like a `slice` of an array.

Leave a reply to yttrill Cancel reply