Skip to content

In defense of unary functions #233

Open

Description

I'm creating this issue to address a line of reasoning primarily put forth by @mAAdhaTTah and others to a lesser extent.

I've been following most of the issues in this repository (a lot of reading, I know) and although I agree with a lot of good arguments made recently in favour of Hack, one argument I see being made over and over again is one which implies that the curried, data-last, unary functions exist solely to fascilitate some kind of pipe operation. The reasoning then continues to favour the Hack proposal because "as opposed to the F# variant, it doesn't make people choose a calling style".

For example here (#215 (comment)), @mAAdhaTTah writes:

[library authors] provide them because curried functions work better with pipe

And the remainder of his argumentation rests on the statement above.

An example of the continuation of this line of reasoning can be seen, among many other places, here in @tabatkins comment:

the library author has to choose this calling convention at the time they write the function, and library users have to know which convention the function is expecting in order to call it correctly. -- #206 (comment)

While this is indeed true for the F# operator, what I'd like to clarify in this thread is that it's also true for the Hack operator, and thus somewhat of a non-argument.

The pipe operator is a tool for code linearization. If point-free programming is the tool to achieve that, fine -- #206 (comment)

Is another comment that I view as an argument within the same line of reasoning. The implication here is that point-free programming accommodates the pipeline operator, rather than the pipeline operator accommodating point-free programming. Here's some more examples of this line of reasoning:

People aren't designing whole libraries in curried / point-free style so they can use them with .map. -- #215 (comment)

Introducing a third calling convention, asking library authors to design their APIs for that calling convention, asking users to specifically choose this calling convention, will further cleave functional JavaScript from the rest of the community & ecosystem -- #206 (comment)

A point-free approach is a tool for writing code in [a linear] way. -- #206 (comment)

These comments seem to stem from a belief that there is no virtue to function currying other than to tackle the code linearization problem. I'd like dispel this belief and in doing so, provide a counter argument to this line of reasoning.


I am one of the "library authors" referred to in various comments about my supposed motivation for providing curried functions. I am the author of Fluture and a core maintainer for Sanctuary - both libraries that leverage unary curried functions very heavily.

My motivation for using curried, data-last, unary functions is not purely for code linearization. In fact, code linearization could be left out of this consideration altogether. Let's examine some of the reasons for sticking with unary functions without going into code linearization.

  1. Firstly, the unary function can be treated as data like none other. Let me elaborate:

    • The untyped lambda calculus shows that you don't need any data types other than the unary function to achieve Turing-completeness. Functions are the data.
    • Functional programmers make use of function combinators to modify their functions as data.
    • Unary functions can themselves act as Functors, Monads, and many other algebras. In fact, many of the combinators from the gist linked above are actually also valid implementations of various algebras for the Function data-type, and as a result are widely spread in the JavaScript ecosystem (in Ramda, fp-ts, Sanctuary, etc).

    Being able to treat functions as data enables a form of meta programming not easily achieved in environments where functions vary a lot in their arity.

  2. Secondly, data-last, curried functions encourage code reuse. A simple example is the definition of increment by (partial) application of a curried add function to 1. But of course this principle extends to functions of far greater complexity.

  3. Thirdly, and perhaps more generally, curried unary functions are an incredibly simple unit of abstraction. This last point is difficult to bring across but it goes a bit like this: When working with simple units of abstraction, the cognitive load from reasoning about the abstractions themselves is lowered, leaving more head space to deal with the complexities of the code you're editing. Having experienced both styles of programming quite heavily, I can only really vouch from my own experience for what a major difference this makes. To summarize this last point: another reason for using unary functions is to optimize for simplicity of abstraction.

  4. EDIT: I added a final reason (composability) a ways down this thread: In defense of unary functions #233 (comment)

The three reasons listed above don't go into code linearization, but are enough for me to favour this style and provide libraries which encourage this style. This means that the assumption that library authors would have chosen non-curried, data-first function APIs if it wasn't for the code linearization problem is, at least in my case, false.


So even without trying to tackle the code linearization problem, people like me already choose to be in a world of curried, data-last, unary functions. The idea that unary functions are a means to an end, where the end is "code linearization" is incorrect. A functional programmer will use data-last curried functions either way, and the pipe functions and eventual |>-proposal grew from a need to facilitate code linearization within an ecosystem that already favours curried, data-last functions.

Solving the code linearization problem within this context has proven problematic:

  • Variadic function composition functions such as Ramda's pipe and compose, and FP-TS' pipe and flow are impossible to type properly or fully in TypeScript.
  • It's also impossible to provide type information for these functions in Hindley-Milner (the type language typically used to document functional libraries) without inventing syntax.
  • The .pipe method used in Fluture and RxJS provide a solution for code linearization which, while not suffering from the problems listed above, is limited only to the data type it's implemented on.

Now this, in my perspective, is what the need for a binary infix function application operator (|>) stems from. Not to solve code linearization within the greater JavaScript ecosystem (which has other solutions, such as fluent method chaining), but specifically to address the code linearization problem within the growing functional programming in JavaScript "niche".

This means that arguments made from the position that "thanks to the Hack pipeline operator, functional programmers can now finally give up on the inconvenience of using unary functions" completely miss the point. Unary functions are not an inconvenience we deal with for linear code, but rather are at the core of the functional paradigm. With that in mind, I hope you can see that the idea that Hack doesn't make people choose a calling style, where F# does, is also wrong:

  • It's true that the F# variant puts non- functional programmers in a position where they have to choose between ergonomic code linearization through tacit style, or an idiomatic data-first non curried API (if we ignore fluent chaining as an option); however:
  • The Hack variant puts functional programmers in same position for making the opposite choice between ergonomic code linearization through argument first style, or an idiomatic (as I've tried to show) curried API.

A small note on the topic of this thread: I am aware of good arguments against implementing a feature in JavaScript that accommodates a code style used only within a specific niche. We don't need to go into that here as it has been discussed in many other threads. I specifically wanted to address a common line of reasoning that I've seen used across the forum which I believe to be false, and haven't seen properly addressed. I'm looking forward to either change the perspective of those using this line of reasoning, or to be corrected myself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationquestionFurther information is requested

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions