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