Keywords

1 Introduction and Motivation

There is an astounding number of programming languages available. For example, in the Wikipedia, there is a list more than 700 programming languages [1]. New ones emerge almost every year [1]. However, they are based on a few programming paradigms, being the imperative (or procedural) paradigm, the object-oriented paradigm, logic-based (or rule-based) and the functional paradigm (or applicative) [2]. Programming languages are based on one or more of the paradigms, which they embrace in their own style. There is a growing number of non-professional programmers, e.g. scientists from various fields, who lack the formal computer science education. This Babylon of languages becomes very confusing for them. In this paper we focus on the functional paradigm, which is very old, at the same, it gains popularity nowadays: new languages emerge (like Clojure [3]) and other languages adopt its concepts (C++, Java) [4], while the functional paradigm is strongly rooted in most of the today’s mainstream languages, as well (Python, C#, F#, Ruby, Smalltalk and others). This situation is in a strong contrast to awareness of the programmers, where most of them remain with traditional Fortran-style programming (iteration, if-then-else). Our goal is to present the key concepts in the functional paradigm to help programmers see the essence in programming languages.

2 Methodology

We identify key principles from the literature and explain them. We denote the principles by identifiers for easy referencing. We demonstrate their implementation in the popular JavaScript language – the ES5 and ES6 versions of the language. The result are specific conclusions about the functional principles support in JavaScript, as well as a general conceptual framework that can be similarly used for analysing other functional programming languages and languages with functional features. We do not dive deeply into explaining the benefits of using these concepts, nor discussion of their appropriateness for various situations. This discussion may be found in the references provided.

3 Key Principles of Functional Programming

The formal foundation of functional programming is the Lambda calculus, also written as \(\lambda \)-calculus. It was formulated by mathematician Alonzo Church in the 1930 s as part of an investigation into the foundations of mathematics [5]. The Lambda calculus provides a simple semantics for computation, enabling properties of computation to be studied formally. We do not discuss these formal aspects of the Lambda calculus here, but we focus on the programming perspective.

The most fundamental principle is the notion of first-class functions (P1). It is an essence of functional programming that functions are first-class citizens that may be manipulated as data. Since functions are considered values in their own right, it is natural for them to appear as arguments or results of other functions. Functions that takes other functions as arguments or that return functions as results are said to be higher order, and we refer to them as “functionals” to distinguish them from ordinary functions [6]. Functions may be named or anonymous, which corresponds to the notion of a Lambda function. Higher-order functions can be used for example in functors, where functionals are mapped over a structure, which provides a “better” alternative to iteration [7].

The referential transparency (P2) states that function call can be replaced by its return value (obtained by calling the function with the same arguments). It makes the order and count of execution irrelevant. This can be done with so-called pure functions. This means that function can not impose side effects, like a mathematical function. An example of a side effect is printing out some message, waiting for an input, sending something through the network, or only accessing some variable outside the function scope (for example global variable or variable from parent scope). This poses severe limitations, however abiding the referential transparency has the following benefits [8]:

  • Purely functions are remarkably easy to parallelize.

  • Pure functions lead to a high degree of code encapsulation and reusability.

  • They are easier to reason about.

  • Pure functions are very easy to write unit tests for.

The referential transparency is tightly bound with the immutability of variables and values (P3). This means, that if we assign a value to some variable, we cannot reassign it. This variable will hold this value until the program stops. From this perspective, variables are more like identifiers for data values, than slots for some changing data. In some languages (like Haskell: [9]), immutability is strictly embedded in the language. However, in languages with imperative features, the situation is more complicated and immutability must be explicitly managed. It can be achieved in several dimensions. The first step is disabling reassigning another value to an already assigned variable. This is usually achieved by a keyword (const, val, final, etc.). The effect is that we cannot assign a new value to this variable, but we still can change the value itself. For example, when we define immutable (or constant) variable named user1 and value of this user1 will be the user with name “George”, we can not set user1 to another user later, but we still can change the name of “George” to “John”. Of course, this problem does not hold for values like integers or strings, just the compound types. The solution of this problem is to define immutable variables transitively. In this case, user object will have all properties defined as constants, so we can not change its name to “John”. In case that user contains another compound object as its property, this object have to be also immutable (contains only immutable properties). This leads us to a question of standard libraries. To support immutability, types defined in libraries must be immutable and functions in these libraries must support operations with immutable structures. These functions have to return a new object with updated values instead of changing the existing object. For example, a sort function is not allowed to change array to sorted array, but it has to leave the original array untouched and return a new sorted array.

A closure (P4) is simply a pairing of a function with its environment: the bound variables that it can see [10]. It means, that function can access values from the enclosing block (lexical scope). More specifically, not from the context in which they are called, but from the context in which are defined. Closures are used for making the code more clear and readable. They also provide encapsulation (like private members in the traditional object-oriented programming).

A recursion (P5) is used in functional programming instead of loops. It is a situation when a function calls itself. There is a special type of recursion called tail-recursion. A tail-recursive clause is a recursive clause of the form

$$p :- q_1, .., q_n, p,$$

where \(n \ge 0\), i.e. the last the last call in the body is a recursive call to itself. It is well known that tail-recursion can be replaced by iteration. This is because there are no more calls after the tail-recursive one, which means that its binding environment can, with a bit of care, be discarded and the space reused [11]. Thus, if a compiler supports tail recursion optimisation, it solves the stack overflow risk of recursion in case of tail recursion.

3.1 Additional Properties

Apart from the fundamental principles, we identified the following additional principles. Some of them also appear in other paradigms, like object-oriented programming or logic programming.

Lazy evaluation (P6) is a situation when a value of an expression is not calculated in the moment of declaration, but it is delayed until needed; It may also happen that the value is not evaluated ever [9]. The lazy evaluation is typically bound to pure functions, as side effects complicate the situation by the possibility that they may not be evaluated [12]. Lazy evaluation does not make much difference for atomic values, apart from possible small optimisation. However, their importance is substantial for collections. Lazy collections (usually lists) are a very common pattern for solving problems in functional style. They bring a possibility to work with virtually infinite streams by evaluating just the portions that are accessed [13,14,15].

Currying (P7) is another important additional principle of functional programming. Currying is a technique of transforming a function of multiple arguments into evaluating a sequence of functions, each taking one argument [16]. In fact, currying is the default mechanism in the Lambda calculus, while multiple-argument functions are technically just a “syntactic sugar”. The same is true for the Haskell programming language, while most of other languages default to multiple arguments and use currying mostly to achieve partially applied functions. It is a situation when we pass fewer arguments to a function that the function expects [10]. It is a powerful abstraction mechanism enabling creation of specialised versions of functions [10].

Pattern matching (P8) is a well-known concept not limited to the functional programming. Pattern matching provides the means to inspect and decompose nested data structures in a single statement [17]. Using this construct, a programmer can define different behaviour of a function based on distinct values and data structures without writing if-then-else constructs, which makes it a device of polymorphism.

Polymorphism (P9) [18] is a powerful abstraction principle, again not only limited to functional programming, it is also one of corner stones of object-oriented programming). Polymorphic functions are functions whose operands (actual parameters) can be of more than one single type. Polymorphic types are types whose operations are applicable to values of more than one single type [19].

To sum up, a proper functional programming language should implement the concept of functions as first class values, support closures, immutable variables and pure referential-transparent functions. Recursion is generally supported in all today’s languages including the imperative ones, however tail recursion optimisation is a welcome asset from the implementation perspective.

4 FP Analysis of JavaScript

Let us now explain how the identified principles are embodied in the JavaScript language (JS), arguably the most important language of the web [20]. JavaScript is designed as a dynamically typed scripting language. The current widely used edition is the ECMAScript 5th Edition (ES5) [21] and a new standard ECMAScript 6th edition (ES6) is available [22]. ES6 contains more direct support for functional programming constructs, but it is not fully supported in the current web browsers (in November 2016). The current adoption status may be checked in [23]. Currently, the code in ES6 for browsers is usually translated into the ES5 code for compatibility reasons.

4.1 ES5

ES5 is a shortcut for EcmaScript 5 from 2009 JavaScript standard [21]. In JavaScript, functions are first-class objects and can be assigned to variables, passed as function arguments or returned as a function result. Anonymous (Lambda) functions are supported in form of:

figure a

We may conclude that principle (P1): first-class functions is supported in JS. Functors are supported with arrays, but there is a small amount of standard functions based on them. They can be supported using libraries like Lodash [24] or Underscore [25].

Principle (P2): referential transparency is not guaranteed nor managed, as functions are not required to be pure and also (P3): immutability is not directly supported. There is no syntax for specifying immutable variables and no functions or objects from standard library that would provide support for immutability. So when we write:

figure b

The value of variable a will be [2, 3, 4, 5]. So the function sort sorts the original array instead of returning the sorted array as its return value and letting the original array unchanged. (In fact, this function returns the sorted array as a return value, nevertheless it sorts the original array anyway.) The only way, how to declare a (local) variable is using the keyword var Footnote 1, and variables can be reassigned. For example, this is a valid code:

figure c

There is immutable support for properties in objects:

figure d

When the writable attribute for a property is set to false, any attempt to change the value of the property fails [21].

Object obj now contains property key with value some data. This property can not be changed (writable: false) or deleted (configurable: false). The second option is freezing an existing object:

figure e

Because of standard functions which mutate data, most of the code in JS is not referential transparent – (P2) is not supported. There is a possibility to mitigate this situation by using libraries providing basic support for immutable structures and functions manipulating these structures (for example immutable.js [26]).

Functions are scope for variables. Objects may be created from JavaScript functions. Properties of these objects can be accessed using closures. Because these properties can be also changed, using closures breaks the referential transparency – If we invoke closure C, then change a variable that is used in this closure and then invoke C for the second time, the result of this closure may be different.

Nevertheless, closures (P4) are present and provide a powerful mechanism, which is leveraged in introducing the concept of modules, which is missing in the language. Modules are implemented in the AMD library ([27]) by enclosing the exported functions in another function, thus forming a closure and providing encapsulation.

Principle (P5): recursion may be used in JS, however tail call optimization is not present. Functions that recurse very deeply can fail by exhausting the return stack [20].

As for the additional principles, (P6): lazy values are not supported, (P7): currying is not supported in the language, but it can be achieved indirectly [28] or using libraries [29]). (P8): pattern matching is not present.

(P9): Polymorphism is supported by a prototype inheritance [30] in a rather object-oriented fashion. Polymorphism in JavaScript deserves a deeper explanation, which is out of scope of this paper.

To sum up, we can say, that ES5 supports first-class functions, but it lacks other properties of functional programming. Some of them can be simulated indirectly or using libraries. Functional style is not idiomatic in standard JavaScript, but a number of libraries and projects leveraging them seems to be rising.

4.2 ES6

ES6 is the new (2015) JavaScript standard and it is backward compatible. It brings several improvements, which are explained e.g. in [31]. Let us discuss the changes related to the functional principles here.

The first improvement is a more elegant syntax for anonymous (Lambda) functions: the //arrow functions//. The code:

figure f

can be shorten to

figure g

So we may say that ES6 syntactically supports (P1) better than ES5. Also, by using arrow functions, one can achieve a more expressive closure syntax (P4). Also, ES6 changes scoping of this from function scope to lexical scope. This change allows a direct closure code – we do not have to save this reference as in ES5. Instead of

figure h

it is now possible to write [32]:

figure i

ES6 also improves the support for immutability. In the language, there are now two new keywords for creating variables. Keyword let creates mutable variable but scoped lexical (instead of the functional scoped var keyword). Using const one can create a constant. The ES6 const keyword is used to declare read-only variables, i.e. the variables whose value cannot be reassigned [31]. So (P3) is supported, but not required. Immutable variable means that nothing else can be assigned to this variable, but properties of this object still can be changed.

ES6 also adds collections and new functions for immutable operations: find(), map(), reduce() do not change the original collection. This strongly supports referential transparent code (P3).

A support for tail call optimization has been added, which facilitates the usability of recursion (P5). A tail position call must either release any transient internal resources associated with the currently executing function execution context before invoking the target function or reuse those resources in support of the target function [22].

ES6 supports generators, which are essentially lazy collections. One can define an iterable sequence with generator function and the control flow can be paused and resumed, in order to produce a sequence of values (either finite or infinite). But this is not enough to support laziness (P6). This provides only a type of lazy collection, but there is still missing lazy function invocation or lazy values.

Currying (P7) is still not part of the language, the situation remains the same as with ES5: it is achievable using libraries.

As for the additional principles, ES6 introduced the destructuring assignment:

figure j

It is essentially (P8): pattern matching, but just in a limited fashion, as it can not be used to create polymorphic functions based on destructuring.

The model of (P9): polymorphism has not changed in ES6.

5 Conclusions

It may be an interesting observation that JavaScript in spite of its C-like syntax offers many fundamental functional programming principles in its heart and a lot can be achieved by libraries. Programmers may thus leverage a good portion of functional power and elegance in this popular language. ES6 is obviously a step towards better functional principles support. JavaScript is one of the languages, which is getting closer to purely functional languages. The following table summarizes concepts and their support in languages.

Property

ES5

ES6

First-class functions (P1)

+

++

Referential transparency (P2)

-

The immutability of variables and values (P3)

-\(^a\)

+

Closures (P4)

+

++

Recursion (P5)

+

++

Lazy evaluation (P6)

-

Currying (P7)

-\(^a\)

-\(^a\)

Pattern matching (P8)

-

Polymorphism (P9)

+

+

  1. \(^a\)can be reached using libraries.

As for the future work, the presented conceptual framework may be used to analyse other programming languages from the functional programming perspective.