По словам Нила Гафтера , история с замыканиями по-прежнему широко открыта.
Как человек, который пишет много кода, используя механизмы, подобные замыканиям … в виде множества встроенных внутренних классов … У меня есть несколько идей о том, что я хочу в решении. Я думаю, что сегодня пишу какой-то мощный и элегантный код, но эта элегантность в функции подрывается некоторой серьезной неловкостью в его выражении в виде Java-кода.
Это действительно сводится к краткости. Я могу выполнить практически все, что мне нужно, используя внутренние классы и объекты-держатели, такие как AtomicInteger и друзья. Но в итоге получается больше кода, чем хотелось бы.
То, что я хочу (заимствуя термин Стю ), состоит в том, чтобы подчеркнуть суть моей логики и убрать церемонию : наименование интерфейса (оно должно быть известно из контекста), типы параметров (только имена, пожалуйста, ), список исключений и т. д.
Прежде всего, давайте ограничим проблему. Замыкания будут определены в терминах интерфейса , интерфейса, который содержит один метод. Попытка использовать краткий синтаксис с интерфейсом, который содержит несколько методов, будет просто ошибкой компилятора.
Во-вторых, закрывающий блок должен иметь свободный доступ на чтение / запись к параметрам и локальным переменным в включающем методе. Этого легко добиться с помощью синтаксического сахара: переменные можно преобразовать в ссылки на объекты-держатели, такие как AtomicInteger, которые хранятся в куче. Сегодня общие поля должны быть окончательными, чтобы указывать, что безопасно делиться ссылками на объект между основным методом и любыми внутренними классами.
Сложная часть совместного использования локальных переменных таким образом заключается не в реализации синтаксического сахара, а в обновлении информации, предоставляемой отладчику, с тем чтобы отладчик мог отменить изменения синтаксического сахара и создавать переменные в включающем методе или в закрывающий блок, кажется, локальный.
Наконец, синтаксис. Я думаю, что Groovy имеет правильный синтаксис здесь. Важная часть заключается в том, чтобы компилятор действительно помогал, а не жаловался со стороны. Сегодняшний Java-компилятор имеет всю информацию о типах, но просто использует ее для создания причудливых сообщений об ошибках о том, что вы должны были набрать. Следует использовать эту информацию о типах, чтобы избежать необходимости всей дополнительной типизации (то есть ввода с клавиатуры, а не необходимости типов в смысле слова Ruby / Groovy).
Когда замыкание передается другому методу, тип параметра определяет все, что должен знать компилятор. Аналогично, когда закрытию присваивается переменная, переменная определяет интерфейс замыкания. Таким образом:
List<Widget> widgets = ... ; Collections.sort(widgets, { w1, w2 -> return w1.getWidgetId().compareTo(w2.getWidgetId()); });
Универсальный тип списка, Widget, информирует универсальный тип Comparator. Таким образом, w1 и w2 полностью типизированы, как экземпляры Widget. Тип возврата замыкания привязывается как int (также из интерфейса Comparator).
Примечание: я также хотел бы видеть много других упорядочений Java, таких как неявный Groovy-стиль
return
.
Другими словами, вышесказанное может быть расширено компилятором до следующего с точки зрения компиляции:
List<Widget> widgets = ... ; Collections.sort(widgets, new Comparator<Widget>() { public int compare(Widget w1, Widget w2) { return w1.getWidgetId().compareTo(w2.getWidgetId()); } });
Видите, суть все еще там, сравнение идентификаторов виджетов. Но теперь это закрыто всей этой церемонией об именах интерфейсов, именах методов, возвращаемых значениях, универсальных типах и так далее.
Предупреждение: есть крайние случаи, когда нам нужно определить тип интерфейса замыкания. Это происходит, когда метод, который должен быть передан, закрывается. Компилятор должен иметь возможность уменьшить число кандидатов на основе количества параметров, проверенных исключений, создаваемых в замыкании, и других факторов … но может потребоваться реализация альтернативного синтаксиса. Например:
ExecutorService service = ...; Future<?> = service.submit(Runnable { performExpensiveOperation(); });submit() is overridden to accept a Callable as well as a Runnable. Callable can return a value. Again, minimal syntax here: the interface name followed by the implementation of the closure method. This compares to the Groovy
as
keyword.Now, it’s easy to get carried away and put in stuff like Python style co-routines (the
yield
keyword), or want a syntax to allow the closure to force a return value from the enclosing block or method. <sarcasm> Yep, let’s add a few more alien concepts to the language, people love that. </sarcasm>.These are also not function objects, so you can’t easily do magic things such as currying (currying is a way of pre-supplying some of the parameters to a function, such that a new function is created that takes fewer parameters). An «interface» that’s curried is a whole new interface and that’s OK by me.
I’m more more modest. I don’t particularly want to add new features to the Java language, just new syntax for the compiler, to let it do the ugly plumbing. That’s kind of my theory for Tapestry’s relationship to Java and the Servlet API as well, and it works.
A few side notes:
Annotations could be used to help the compiler out. For example, if a common interface should be a closure, but has multiple methods, an annotation could be used to tell the compiler which method of the interface is applicable to use as a closure; any other methods (in the inner class) would be no-ops or (perhaps guided by additional annotations) failures.
Annotations on fields and parameters (or perhaps on the method) could help the compiler decide how to share visible variables: do we need to use a thread-safe approach (such as something based on AtomicReference) or simple, non-synchronized holder objects? The compiler could do some escape analysis as well to chose the best solution, but in many cases, it’s on the coder’s shoulders to make the right decision.
From what I can tell, my ideas are closest to the Concise Instance Creation Expressions proposal by Bob Lee, Doug Lea, and Josh Bloch. However, in my opinion, the compiler can do much more in terms of providing type information. I think CICE stumbles in that it allows for creation of classes, not just interfaces, and gets bogged down in defining closure types and parameter types … again, things that (with the mandate of single-method interfaces), the compiler can do autonomously.
Likewise, CICE requires that variables visible to the inner block be marked «public». This to me is something that the compiler can analyze; it can identify any assignments to variables and promote such variables to stored-on-the-heap status. I don’t see the need for final; the compiler has plenty of ability to determine if a parameter or local variable is ever updated within the body of a method (and the body of any inner classes or closures of that method).
My basic concept is: Less is More. Support fewer cases but do so more cleanly, more concisely, and more understandably. Simplify. Let the compiler do more work. Reduce the density and complexity of the Java code. Expose the essence.