MPS 2021.1 Help

Closures

Introduction

Closures are a handy extension to the base language. Not only they make code more consise, but you can use them as a vehicle to carry you through the lands of functional paradigm in programming. You can treat functions as first-class citizens in your programs - store them in variables, pass them around to methods as arguments or have methods and functions return other functions. The MPS Closures Support allows to you employ closures in your own languages. In fact, MPS itself uses closures heavily, for example, in the collections language.


This language loosely follows the "BGGA" proposal specification for closures in Java. However, you don't need Java 7 to run code with MPS closures. The actual implementation uses anonymous inner classes, so any recent version of Java starting with 1.5 will run the generated code without problems. Only the closures runtime jar file is required to be on the classpath of the generated solutions.

Function type

{ Type1, Type2... => ReturnType }

Let's start with a trivial example of function type declaration. It declares a function that accepts no parameters and returns no value.

{=> void}

Subtyping rules

A function type is covariant by its return type and contravariant by parameter types.

For example, given we have defined a method that accepts {String => Number} :

public void accept_Number_from_String ({String => Number} function) { ... }

we can pass an instance of {Object => Integer} (a function that accepts Object and returns int) to this method:

this.accept_Number_from_String ({Object o => o.hashCode();});

Simply put, you can use different actual types of parameters and the return value so long as you keep the promise made in the super-type's signature.

Closure literal

Closure literal is created simply by entering a following construct: { <parameter decls> => <body> }. No "new" operator is necessary.

The result type is calculated following one or more of these rules:

  • last statement, if it's an ExpressionStatement;

  • return statement with an expression;

  • yield statement.

Note: it's impossible to combine return and yield within a single closure literal.

Closure invocation

The invoke operation is the only method you can call on a closure. Instead of entering

closure.invoke(p1,p2);

To invoke a closure, it is recommended to use the simplified version of this operation - parentheses enclosing the parameter list.

closure(p1,p2);

Invoking a closure then looks like a regular method call.

Some examples of closure literal definitions.

{int => int} fib = { int n => n <= 1 ? n : invoke(n-1) + invoke(n-2); } {int => int} fact = { int n => int res = 1; }; while (n > 1) { res = res * n--; } res; {=> sequence<int>} closure = {=> yield 1;};

Recursion

Functional programing without recursion would be like making coffe without water, so obviously you have a natural way to call recursively a closure from within its body:

{int => long} fact = {int n => if (n == 1) { return 1L; } else { return n * invoke(n - 1); } }; println("Factorial of 10 = " + fact(10));

A standalone invoke within the closure's body calls the current closure.

Closure conversion


For practical purposes a closure literal can be used in places where an instance of a single-method interface is expected, and vice versa.

public interface Worker { String doWork (int amount); } ... Worker worker = { int amount => "Done " + amount + " work";};

The generated code is exactly the same as when using anonymous class:

Worker worker = new Worker() { public String doWork(int amount) { return "Done " + amount + " work"; } };

Think of all the places where Java requires instances of Runnable, Callable or various observer or listener classes:

println("Reported from the main thread " + Thread.currentThread()); final Runnable runnable = { => println("Reported from a thread " + Thread.currentThread()); }; Thread t = new Thread(runnable); t.start();

As with interfaces, an abstract class containing exactly one abstract method can also be adapted to from a closure literal. This can help, for example, in smooth transition to a new API, when existing interfaces serving as functions can be changed to abstract classes implementing the new interfaces.

Yield statement

The yield statement allows closures populate collections. If a yield statement is encountered within the body of a closure literal, the following are the consequences:

  • if the type of yield statement expression is Type, then the result type of the closure literal is sequence<Type>;

  • all control statements within the body are converted into a switch statement within an infinite do-while loop at the generation;

  • usage of return statement is forbidden and the value of last ExpressionStatement is ignored.

sequence<int> sequence = new sequence<int> ({=> yield 1;}); yield 2; yield 3;

Functions that return functions

A little bit of functional programming for the functional hearts out there:

{ int , int => int } add = { int x , int y => x + y ; } ; { int => int } plusThree = { int x => x + 3 ; } ; { int => int } curriedPlusThree = this . curry ( add , 3 ) ; assert plusThree . invoke ( 1 ) equals curriedPlusThree . invoke ( 1 ) ;

The curry() method is defined as follows:

public { int => int } curry ( final { int , int => int } fun , final int y ) { return { int x => fun . invoke ( x , y ) ; } ; }

Runtime

In order to run the code generated by the closures language, it's necessary to add to the classpath of the solution the closures runtime library. This jar file contains the synthetic interfaces needed to support variables of function type and some utility classes. It's located in: %MPS_HOME%/core/baseLanguage/jetbrains.mps.baseLanguage.closures.runtime.jar

Differences from the BGGA proposal

  • No messing up with control flow. This means no support for control flow statements that break the boundaries of a closure literal.

  • No "early return" problem: since MPS allows return to be used anywhere within the body.

  • The yield statement.


[1] Closures for the Java Programming Language


[2] Version 0.5 of the BGGA closures specification is partially supported


[3] This is no longer true: only closure literal to interface conversion is supported, as an optimization measure.

Last modified: 23 March 2021