JtR*_*JtR 5 lambda computer-science programming-languages
java支持当前版本的6个lambda表达式或"匿名函数"吗?有没有我在java中无法用支持lambda表达式的编程语言做的事情?我知道java是完整的,所以你可以在其中做"任何事情".
为什么匿名内部类包装函数不能表示lambda演算中定义的函数?
实际上什么是匿名函数以及如何说某些语言支持匿名函数?
Jör*_*tag 17
正如我已经在上面的评论中暗示的那样,问题实际上取决于你如何定义"支持".在您的问题中,您已经提到Java是Turing-complete,因此Java"支持"(对于某些"支持"的定义)所有其他编程语言都支持的内容.
Java 确实支持匿名函数:只需在Java中为λ演算编写解释器,并将匿名函数作为字符串传递.
但是,我发现使用匿名函数的工作太多了.所以,对我来说,有趣的问题不是Java是否支持匿名函数,而是当我想使用匿名函数时,Java是否支持我.IOW:Java是否易于使用匿名函数,它是否指导我,它对我有帮助吗?
让我们做一个简单的实验:实现该map函数并使用它来递增列表[1, 2, 3, 4, 5]的每个元素1.
以下morph是mapHaskell中实现(这是我将要调用的函数以便不与现有的内置函数冲突)的方式:
morph _ [] = []
morph f (x:xs) = f x : morph f xs
Run Code Online (Sandbox Code Playgroud)
就这样.短而甜:用任何东西变形空列表只是空列表,并且变形具有至少一个元素的列表将变形函数应用于第一个元素并将其与变形列表的其余部分的结果连接.
正如您所看到的,编写一个接受函数作为参数的函数非常简单,非常轻量级.
假设我们有一个列表l:
l = [1, 2, 3, 4, 5]
Run Code Online (Sandbox Code Playgroud)
我们现在可以像这样调用变形:
morph (\x -> 1 + x) l
Run Code Online (Sandbox Code Playgroud)
再次,将匿名函数传递给我们的高阶函数非常简单,非常轻量级.
它看起来几乎像λ演算.实际上,如果您使用Haskell IDE,具有Haskell模式的文本编辑器或Haskell漂亮的打印机,它实际上将显示如下:
morph (?x ? 1 + x) l
Run Code Online (Sandbox Code Playgroud)
如果我们使用运算符部分,它会变得更容易,这允许我们传递部分应用的运算符:
morph (1+) l
Run Code Online (Sandbox Code Playgroud)
或者,我们可以传递预定义succ函数,该函数返回整数的后继函数:
morph succ l
Run Code Online (Sandbox Code Playgroud)
虽然这当然不是匿名函数,但它是一个命名函数.
在Scala中,它看起来非常相似.主要区别在于Scala的类型系统比Haskell更复杂,因此需要更多类型注释:
def morph[A, B](l: List[A])(f: A => B): List[B] = l match {
case Nil => Nil
case x :: xs => f(x) :: morph(xs)(f)
}
Run Code Online (Sandbox Code Playgroud)
它仍然非常轻巧.基本上,我们所要做的就是将f参数声明为类型A => B(即从类型A到类型的函数B),这实际上是语法糖Function1[A, B].
现在我们只需要我们的清单:
val l = List(1, 2, 3, 4, 5)
Run Code Online (Sandbox Code Playgroud)
变形:
morph(l) {_ + 1}
Run Code Online (Sandbox Code Playgroud)
这再次利用了Scala的一些语法糖.在匿名函数中,您可以不使用参数列表; 如果每个参数只使用一次并按照它们的定义顺序,你可以简单地将它们称为_.
但即使是完整的形式也不会太重:
morph(l) {(e) => e + 1}
Run Code Online (Sandbox Code Playgroud)
如果我经历了制作morph某个类的实例方法的麻烦并List根据Pimp My Library模式定义了从该类的隐式转换,我甚至可以编写类似的东西
l morph {_ + 1}
Run Code Online (Sandbox Code Playgroud)
当然,Scheme不应该对匿名和高阶函数有任何问题.这是morph:
(define (morph f l)
(if (null? l)
null
(cons
(f (first l))
(morph f (rest l)))))
Run Code Online (Sandbox Code Playgroud)
这是我们的清单:
(define l '(1 2 3 4 5))
Run Code Online (Sandbox Code Playgroud)
我们的匿名函数用法:
(morph (lambda (e) (+ e 1)) '(1 2 3 4 5))
Run Code Online (Sandbox Code Playgroud)
module Enumerable
def morph
[].tap {|r| each {|e| r << yield(e) }}
end
end
Run Code Online (Sandbox Code Playgroud)
This is extremely lightweight. We didn't even have to define a parameter for the function, because in Ruby, every method has an implied function parameter, called a block.
l = [1, 2, 3, 4, 5]
Run Code Online (Sandbox Code Playgroud)
Calling it is almost as lightweight as Scala
l.morph {|e| e + 1 }
Run Code Online (Sandbox Code Playgroud)
I can sort-of replicate the operator sections from the Haskell example by grabbing a reference to the + method of 1:
l.morph(&1.method(:+))
Run Code Online (Sandbox Code Playgroud)
Ruby also has a pre-defined succ method for integers which we can pass using the Symbol#to_proc trick:
l.morph(&:succ)
Run Code Online (Sandbox Code Playgroud)
Some people criticize blocks in Ruby, because every method can only take a single block, and methods taking more than one function are much uglier, but it's actually not that bad. Here's the same code as above, but without using blocks:
module Enumerable
def morph(f)
[].tap &-> r { each &-> e { r << f.(e) }}
end
end
l = [1, 2, 3, 4, 5]
l.morph -> e { e + 1 }
l.morph(1.method(:+))
Run Code Online (Sandbox Code Playgroud)
ECMAScript is a direct descendant of Scheme, so it's no surprise that it can handle our problem nicely, although with some amount of syntax clutter:
Array.prototype.morph = function (f) {
var r = [];
this.forEach(function (e) { r.push(f(e)); });
return r;
}
Run Code Online (Sandbox Code Playgroud)
The main distraction here is the generally ugly syntax, not so much the handling of higher-order functions.
Let's build our list (well, array):
var l = [1, 2, 3, 4, 5];
Run Code Online (Sandbox Code Playgroud)
And call the morph function (actually, a method in this case) passing an anonymous function as an argument:
l.morph(function (e) { return e + 1; });
Run Code Online (Sandbox Code Playgroud)
Now we're moving closer to our ultimate target language. Here's C#:
Array.prototype.morph = f => {
const r = [];
this.forEach(e => r.push(f(e)));
return r;
}
Run Code Online (Sandbox Code Playgroud)
Not too bad. Note the type of the function: morph. This is a pre-defined type which is part of the core library, just like Func<A, B> in Scala or Function1[A, B] in Haskell. (This is an important distinction to Java.)
Thanks to type inference and collection initializers, creating the list isn't all too painful:
const l = [1, 2, 3, 4, 5];
Run Code Online (Sandbox Code Playgroud)
And passing a lambda which consists only of a single expression is basically as lightweight as Ruby, Scala, Scheme or Haskell and even more lightweight than ECMAScript, because you don't need the a ? b or function keywords:
l.morph(e => e + 1);
Run Code Online (Sandbox Code Playgroud)
But even using the "full" syntax isn't too bad:
public static IEnumerable<B> Morph<A, B>(this IEnumerable<A> l, Func<A, B> f)
{
IList<B> r = new List<B>();
foreach (var e in l) r.Add(f(e));
return r;
}
Run Code Online (Sandbox Code Playgroud)
(You'll notice that I made return an extension method, which means I can call it like Morph in addition to l.Morph(f).)
var l = new List<int> { 1, 2, 3, 4, 5 };
Run Code Online (Sandbox Code Playgroud)
At first glance, this isn't too bad, actually. In fact, it looks pretty much exactly like the C# version. But why can't I write Morph(l, f)? Why do I have to write f(e)? In all other languages I could use the same (or in the case of Ruby, almost the same) syntax for calling a function that was passed as an argument as I would for calling any other function, procedure or method.
I know it isn't much, but it leaves this kind of bitter taste that somehow functions aren't quite first-class. Also, as we'll see further on, there is one of those small annoyances at every single step along the way, and even though every single one of them is insignificant by itself, they do add up.
Here's our list:
l.Morph(e => e + 1);
Run Code Online (Sandbox Code Playgroud)
And this is how we call our f.apply(e):
l.Morph((e) => { return e + 1; });
Run Code Online (Sandbox Code Playgroud)
This is pretty heavy stuff. I mean, all I'm doing is calling a method and passing two arguments. Why does that explode into four lines? In all other languages it was just a simple one-liner. Of course, I could remove all line breaks and it would still be valid:
static <A, B> List<B> morph(List<A> l, Function1<A, B> f) {
List<B> r = new ArrayList<>();
for (A e: l) r.add(f.apply(e));
return r;
}
Run Code Online (Sandbox Code Playgroud)
But I think you see what I'm getting at. The actual operation that is being performed, incrementing each element by 1, is pretty much invisible between all that noise.
Note also that in some of the other languages I actually used anonymous functions inside the definition of the morph function, for example in Ruby and ECMAScript and it was no big deal. If I were to do that in Java, it would lead to even more clutter and cruft and an explosion of lines.
So, even at this point we are seeing that working with higher-order and anonymous functions in Java is way more cumbersome than in pretty much any other mainstream (and not so mainstream) language.
But we haven't even gotten to the really ugly part yet: what is that morph type there? Where did that come from?
Well, I actually had to write that type myself!
List<Integer> l = Arrays.asList(1, 2, 3, 4, 5);
Run Code Online (Sandbox Code Playgroud)
This is, of course, a so-called SAM interface, i.e. an interface or an abstract class with a Single
Abstract Method. Which is the closest thing to a function type Java has. In some sense, a function is just an object with a single method, so that's perfectly fine. The fact that function types are represented via SAM interfaces isn't the problem either. In fact, that's basically how they are are represented in Scala (in Scala, Function1<A, B> is just syntactic sugar for f(a), so any object with an f.apply(a) method is essentially a function), Ruby (in Ruby, apply is just syntactic sugar for f.(a), so every object with a f.call(a) method is essentially a function) and similarly in C#.
The problem is that I had to write it, that it wasn't already there.
Not only did I have to write it myself, I had to come up with a name for it. And I had to come up with a name for the method. None of which I had to do with any of the other languages here. Well, actually, I just stole the names from Scala, so the actual "coming up with the names" part wasn't so difficult.
What's really important are the implications of having to come up with a name. Java has a nominal type system, i.e. a type system based on names. And thus the fact that I had to come up with a name myself means that everybody else also has to come up with names. And because their names are different than mine (and if they aren't, then that will be a compile error), that means that I cannot pass the same function to two different libraries. Say, for example, I want to pass the same filtering function to my gridview and to my ORM. But the gridview expects, say, a call with a single method javax.swing.Predicate<T> whereas my ORM expects an apply(T el) with a single method org.sleepy.Predicate<T>.
Note that these two types are really exactly the same, it's just that they have different names and thus I cannot pass the same function to both libraries. This is not a hypothetical example. During the recent discussions about Project Lambda for Java, someone counted how many duplicate instances of a apply(T el) type there were already in Java SE 6, and IIRC the number was in the double digits.
It is perfectly possible to solve this problem in a nominal type system. After all, there aren't dozens of incompatible copies of a Predicate type, simply because Sun put a single one in the library, and everybody uses that. They could have done the same with List, but they didn't, which leads to a proliferation of identical, yet mutually incompatible types not only in third-party libraries but even within the JRE. (For example, Function is arguably a function type, as is Runnable. But why do they have to be special-cased?) In .NET, it works just fine, because Microsoft put one single type into the runtime. (Well, actually not quite one single type, but close enough.)
Because there is no single function type in the JRE, there are also only very few methods which take a function type. This is another thing that makes first-class and anonymous functions hard to use in Java. Once you have one, there's not much you can do with it. You can't filter an array with a predicate function, you can't transform a list using a mapping function, you can't sort a gridview using a comparator function.
This is also one of the reasons why I am so disappointed with some of the iterations of Project Lambda. They keep dropping the introduction of Function Types from the project, although the lack of function types is IMHO one of the biggest problems. The ugly syntax can be fixed with IDE trickery, the lack of standard function types can't. (Not to mention all those people who use the JVM and the JRE but don't use Java. They don't benefit one bit from adding syntactic sugar for anonymous inner SAM classes to the Java language, simply because they do not use the Java language. What they need are function types, and an updated collection library which uses function types.)
So, we're up to four problems now:
Comparator and f.apply(a) (BTW, that's the identity function, i.e. the function which does absolutely nothing, and it takes up 58(!) characters),And when I talk about "modelling overhead", I'm not just talking about one new Function1<A, A>() { public A apply(A a) { return a; }} type. Enter primitive types …
Have you noticed how I used Function1s and not Integers in my code above? Yes, that's right, the single biggest design screwup in the history of programming languages, Java's Original Sin, the bane of every Java programmer's existence, has once again come to bite us in the ass: primitive types.
You see, in Scala, there is exactly one class that represents a function with int arguments. It's called n. So, there is exactly one class FunctionN[T?, T?, …, Tn, R] for functions without any arguments, one class Function0[R] for functions with one argument, one class Function1[T, R] for functions with three arguments and so forth, all the way up to something around 20, I believe.
In C#, there are exactly two classes that represent a function with Function3[A, B, C, R] arguments: n and Func<T?, T?, …, Tn, R>. This is because there is no type which represents "no type". So, you cannot declare a function which doesn't return anything using C# (Action<T?, T?, …, Tn> is a modifier, not a type), and thus you need a separate type (void) to represent functions that don't return anything. So, you have two classes Action and Func<R>, which represent functions that don't take any arguments, two classes Action and Func<T, R> which represent functions of one argument and so forth, again up to circa 20. (In Scala, a function that doesn't return anything simply has the return type Action<T>, so you can just have a Unit, for example.)
In Java, however, you need 10×9n types to represent a function of n arguments. Let me demonstrate that with just one argument:
morph(l, new Function1<Integer, Integer>() {
@Override public Integer apply(Integer n) {
return n + 1;
}
});
Run Code Online (Sandbox Code Playgroud)
That's 90(!) different types just to represent the concept of "something that takes one argument".
And, of course, if I want to write something which takes a function as an argument, I need to have a corresponding number of overloads as well, so if I want to write a method which filters some values based on a predicate, I need 9 overloads of that function which take Function2[Int, Int, Unit], Function1_T_boolean, Function1_byte_boolean, Function1_short_boolean, Function1_int_boolean, Function1_long_boolean, Function1_float_boolean, Function1_double_boolean and Function1_boolean_boolean.
(By the way: this still ignores checked exceptions. Technically, we also need 2n copies of every single one of those 90 interfaces, where n is the number of different types of checked exceptions that exist in Java.)
So, that's reasons number 5 and 6: a massive explosion in the number of types and correspondingly the number of methods.
If you put all of that together, then I guess you would agree that anonymous inner classes in Java are much more cumbersome than anonymous functions in, well, pretty much every other programming language.
But there's still more!
We haven't even talked about closures yet! While closures are orthogonal to first-class and anonymous functions, they are also one of the most important (and interesting) use cases of anonymous and first-class functions. Inner classes (whether anonymous or not) are closures, but as @Jon Skeet already pointed out, they are severely limited.
In conclusion, I would say that No, Java does not support anonymous functions.
Oh, I almost forgot: there was another question somewhere:
What actually is an anonymous function
That's easy. A function with no name.
and how you can say that some language supports anonymous functions?
If it makes it easy to work with them.
Java doesn't: there is a difference between supporting anonymous functions and being able to emulate a subset of the features of anonymous functions by encoding them into anonymous inner SAM classes.