Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [epsilon-dev] First-class EOL lambdas

Hi all,

 

I have discussed the support for lambdas in EOL with Dimitris today and have reached the following conclusions:

 

  • The refactoring of first-order operations to use CheckedEolFunction (and its extensions) is useful for the purposes of conciseness, maintainability, consistency and correctness.
  • The ability to assign lambda expressions to variables is a niche feature with limited use cases and non-trivial to support and maintain.

 

As a result, I will be abandoning the work on the ability to re-use and explicitly construct lambdas. The rationale is perhaps a bit clearer at this point now that we are aware of what this involves: potentially a new syntax, documentation of the new types (i.e. Predicate, Function etc.), an elegant way to construct lambdas (such that the type of the lambda is known), documentation and user-friendly error reporting. However when we consider the use cases for this feature, we find that basically it is to get around the inability of the EOL grammar to distinguish between parameters to a lambda and parameters to a method which takes both a simple parameter and lambda _expression_. In cases where re-use is required, users can define their logic within EOL operations and invoke them from the lambda expressions. Furthermore, it is uncommon to find lambda expressions being assigned to variables even in Java; especially if they are the simple sort of expressions that EOL’s first-order operation call syntax supports.

In the future, I’m sure an elegant workaround for the heterogenous parameters problem (see LambdaExpressionTests.eol. testHeterogenousDoubleParameters()) will be found; I can already think of ways to handle this without modifying the grammar or introducing explicit lambda construction.

 

For now, I will remove support for explicit lambda creation from EOL but still keep the changes to FirstOrderOperation, and merge the lambda-support branch into master. Should we decide to support such a feature in the future, at least the framework is there and we have some understanding of what would be involved in implementing this.

 

Thanks,

Sina

 

From: Horacio Hoyos
Sent: 10 December 2018 10:12
To: Epislon Project developer discussions
Subject: Re: [epsilon-dev] First-class EOL lambdas

 

Hi Sina,

 

The generalisation of lambdas worked out nicely, IMHO. The only think I would try to improve is the error reporting. As I see it, receiving a “… expected 'CheckedEolFunction' but got '2'”.”  is not very informative to the user given that in the domain on EOL there is no such thing (i.e. implementation details are permeating to the user). Is there any way to wrap this around more Epsilon user friendly error messages, such as:

 

Invalid lambda _expression_ in select, expected “ iterator | condition ” or LambdaFactory.Consumer. 

 

The idea is that the messages are related to the book and existing documentation so its is easier for the user to understand and debug the problem. I am guessing we would need a separate section of the book on the LambdaFactory and an extended or secondary table of FirstOrderOperations with their expected Lambda types.

 

Regarding the creation of Lambda expressions as variables for reuse, I would prefer a new set of grammar constructs as opposed to the “Native-like” approach. The grammar constructs should be flexible with respect to the lambda name, i.e. to easily support possible future new lambdas and to avoid having too many keywords:

 

var myLambda = [unary | i => i*10];

var otherLambda = [consumer | i => i.name = x];

 

The second cases raises a question though, scope. What is the scope of the new lambdas? In the existing implementation if we have

 

    ….select(i | i.name = x)

 

x would be resolved iteratively until the “root” context is reached, is this the same for the new lambdas?

 

Ans second, are these new lambdas valid expressions?, e.g.

 

    …select( i | myLambda < 10)

 

 

Cheers,

 

 

 

 



On 7 Dec 2018, at 22:44, Sina Madani <sinadoom@xxxxxxxxxxxxxx> wrote:

 

Hi everyone,

 

(Apologies for the length of this e-mail!)

 

I was discussing the notion of the LambdaFactory and the ability to assign lambda expressions to variables in EOL for re-use (see my previous e-mail) with Horacio earlier this week and came across an inconsistency: lambda expressions created from the LambdaFactory could not be used with the built-in first-order operations.

I therefore decided to have a go at refactoring FirstOrderOperation and subclasses so that all EOL lambdas (i.e. FirstOrderOperationCallExpressions) are converted into native Java lambdas before processing. This not only ensures a consistent user experience, but also reduces much of the boilerplate in the implementation of first-order operations too; where such code has previously resulted in subtle bugs due to forgetting to leave the FrameStack scope, for example.

Instead, a call to FirstOrderOperation.execute(...) parses a CheckedEolFunction from the first _expression_ using the LambdaFactory which can then be used by subclasses.

Although java.util.function.* did not make the design decision of making all of its functional interfaces a subclass of java.util.Function, for ease of processing and consistency there is nothing stopping us doing this in EOL, along with supporting checked exceptions. Hence, the state variable in FirstOrderOperation can always be a CheckedEolFunction since other functional interfaces can be expressed as specialisations, so for example:

 

Predicate<T> extends Function<T, Boolean>

Supplier<T> extends Function<Void, T>

Consumer<T> extends Function<T, Void>

UnaryOperator<T> extends Function<T, T>

...etc.

 

For a demonstrative example of the refactored implementations, here is the new CollectOperation:

 

public class CollectOperation extends FirstOrderOperation<Object> {

    @Override

    public Collection<?> executeImpl() throws EolRuntimeException {

        Collection<Object> result = EolCollectionType.isOrdered(source) ?

            new EolSequence<>(source.size()) : new EolBag<>(source.size());

        

        for (Object item : source) {

            result.add(function.applyThrows(item));

        }

        return result;

    }

}

 

(The generic type is the expected return type of “function”).

Notice how not only do we have the source collection already set up, we also have no references to context, FrameStack, iteratorType and none of the usual scope.enterLocal(_expression_, Variable.createReadOnlyVariable(iterator.getName(), ...)..). We can simply focus on the actual transformation logic with all of the repetitive code factored out.

 

With some minor changes in OperationCallExpression and FeatureCallExpression, from the user’s perspective all lambdas are now equivalent and can be used in both native code (e.g. Streams) and EOL FirstOrderOperations (e.g. select). Tests for these have been implemented and all previous tests plus the new ones are passing.

To pre-emptively answer the inevitable question regarding error reporting, invalid parameters will be reported as usual with as much relevant detail as possible. For example, in the following code:

 

Sequence{1..10}.select(2);

 

The user gets an EolIllegalOperationParametersException:

“Invalid number [or types] of arguments for operation 'select': expected 'CheckedEolFunction' but got '2'”.

 

Of course it would be preferable to be more specific with regards to the expected type where possible. In most cases, the appropriate type is actually a ‘CheckedEolPredicate’. Currently this lax approach leads to a less user-friendly (i.e. non-EOL) exception when an incorrectly specified function is used. For example:

 

var hasher = LambdaFactory.func(d | d.hashCode());

Sequence{1..10}.exists(hasher);

 

results in:

“Internal error: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.Boolean”.

 

There are multiple solutions to this: one way is to enforce type-checking of the functions to their specialised forms prior to execution, another is to catch the ClassCastException during execution and rebrand it as EolRuntimeException with the appropriate message and Epsilon stack trace.

 

An interesting implementation detail is that lambda expressions created through the LambdaFactory really are native lambdas, as demonstrated by the output of:

 

var raiseMagnitude = LambdaFactory.unary(i | i * 10);

raiseMagnitude.getClass().getName().println();

 

being:

org.eclipse.epsilon.eol.function.LambdaFactory$$Lambda$13/728258269.

 

Regarding the construction of lambdas from the user’s perspective, Horacio and I discussed this and there is no obvious or simple solution for making it more elegant. Whilst it would be preferable to support such a feature at the language level as opposed to exposing a built-in wrapper object to the user, there is the question of type inference. As I mentioned, all lambda expressions with zero or one iterators can be inferred to be a CheckedEolFunction, but whilst this is fine for EOL operations it is not the case for Natives since Consumer, Supplier, Predicate etc. do not extend Function. We could infer CheckedEolFunction by default unless the user specifies the type, like:

 

var printer = (e | e.println()) : Consumer;

 

but either way this requires a change to the grammar as well as adding the types to the editor. I would argue that the minimal impact approach both for users and Epsilon developers is to stick with the operation approach; either by the LambdaFactory object or, if this is not pretty, having the operations on LambdaFactory being contextless, e.g.:

 

var raiseMagnitude = unary(i | i * 10);

 

If anyone has any thoughts on this, please let me know. If there are no suggestions for further discussion I propose to merge these changes from the lambda-support branch into master.

 

Thanks,

Sina

_______________________________________________
epsilon-dev mailing list
epsilon-dev@xxxxxxxxxxx
To change your delivery options, retrieve your password, or unsubscribe from this list, visit
https://www.eclipse.org/mailman/listinfo/epsilon-dev

 

 

 


Back to the top