12
$\begingroup$

In Java, there are "SAM-interfaces" which are used as "types" for lambda expressions. In Haskell, there are dedicated types for functions. In Rust, there are no types for functions, but instead there are traits for functions.

What are the differences between these approaches? For a new language, which one should be chosen?

$\endgroup$

5 Answers 5

9
$\begingroup$

It's worth noting that Rust's approach is actually pretty similar to Java's. In Java, a lambda expression is syntactic sugar for an anonymous class instance, implementing a specific SAM type (abstract class or interface). When writing a lambda expression, you end up with an object whose type contains a method whose body is the lambda you wrote. This object is a regular object that can be passed around, and this gives Java semantics akin to the "first-class functions" usual functional languages provide.

In Rust, when you write a closure, pretty much the same happens: an anonymous, internal type is created by the compiler, implementing one or more function traits (FnOnce, FnMut, Fn), the implementation of which contain a call() method whose body is the lambda you wrote. So, in essence, it's the same thing. Have a look at this example taken from Rust's reference.

fn f<F : FnOnce() -> String> (g: F) {
    println!("{}", g());
}

let mut s = String::from("foo");
let t = String::from("bar");

f(|| {
    s += &t;
    s
});

Is behaviorally equivalent to:

struct Closure<'a> {
    s : String,
    t : &'a String,
}

impl<'a> FnOnce<()> for Closure<'a> {
    type Output = String;
    fn call_once(self) -> String {
        self.s += &*self.t;
        self.s
    }
}

f(Closure{s: s, t: &t});

In Rust, the lambda expression (the closure) ends up simply being an object that contains a method corresponding to the lambda, exactly like in Java. The main difference being that in Java, it's harder to optimize (since most of the hard work happens through dynamic dispatch, where in Rust dynamic dispatch is opt-in).

Now, as to what approach to choose for a language, it depends on what you're trying to do. Many languages get away with not supporting "function objects" out-of-the-box, often by instead supporting good-enough workarounds (e.g. C's function pointers). Supporting closures can be really hard when you start asking yourself value lifetime questions. If you're trying to make a functional language, you'll have to support them or you'll quickly feel limited as to what functional idioms you're able to handle.

$\endgroup$
2
  • $\begingroup$ The fundamental difference though, between Java on the one side and both Haskell and Rust on the other, is that Haskell and Rust provide standardized types for functions, thus encouraging library authors to use those standardized types. In Java, there is no guarantee I can write a single predicate function and pass it to a collection filter method and a Swing dropdown list filter method because one of them may take a bool java.util.function.Predicate<T>.test(T t) and the other may take a (hypothetical) bool SwingFilterFunction<T>.condition(T t). $\endgroup$ Jun 29, 2023 at 5:06
  • $\begingroup$ BTW, Scala is very similar to Java: any object with a method named apply can be called like a function (so, while Scala "fixes" the name of the method, unlike Java, just like Java, it does not require inheriting or implementing a specific interface), but like Rust and unlike Java, Scala also supplies a standard Function interface which is consequently used by all libraries. $\endgroup$ Jun 29, 2023 at 5:07
4
$\begingroup$

First class functions are cleaner

If your closures/lambdas are SAM interfaces, in production you often end up writing a lot of duplicate interfaces for the same functions. As a Java example, one library might have a StringPoster while another uses Consumer<String>, even though they are the same. In Kotlin, both would likely be (String) -> Unit, perhaps with type aliases, allowing for much greater consistency and clarity.

$\endgroup$
4
  • $\begingroup$ I don't think this is so much a problem of having SAM interfaces vs. first-class functions, but with the standard library providing standard interfaces. While the standard interfaces in java.functional were added at the same time as lambdas and streams, there is nothing inherently tying them together. They could have been provided from day 1. For example, in Scala, every object that has an apply method can be called like a function, but the majority of them still choose to inherit from Function even though it is not necessary. Conversely, library authors choose to take Functions … $\endgroup$ Jun 29, 2023 at 5:00
  • $\begingroup$ … as arguments, even though they could easily define their own types. It is basically just exploiting library authors' laziness. $\endgroup$ Jun 29, 2023 at 5:00
  • $\begingroup$ @JörgWMittag apologies, I dont get your argument. Can you rephrase it? $\endgroup$
    – Seggan
    Jun 29, 2023 at 13:42
  • $\begingroup$ IIRC this was actually an explicit decision in Java when they added lambdas. Besides integrating better into the existing language, they rationalize that complicated function types can easily lose their semantic meaning. And if you add new arguments to your StringPoster you don't have to edit the name of the type everywhere it appears. $\endgroup$ Jul 29, 2023 at 16:30
4
$\begingroup$

One advantage to the Java SAM approach is that while the implementation of the function-interface only needs to implement one method (and this can be made compact through lambdas), the interface itself may contain additional default methods for composing or otherwise modifying the behaviour.

For example, if a method takes a Predicate<T>, then the caller just needs to pass something that takes a T and returns a boolean... but once you have an instance of the Predicate, you can do things like:

  • newPredicate = predicate.negate()
  • newPredicate = predicate1.and(predicate2)
  • newPredicate = predicate1.or(predicate2)

Similarly, a Function<V,R> only needs to take a V and return an R, but once you have one, you can do:

  • newFunction = function1.andThen(function2)

And to be clear - it's not that these couldn't be done without SAM types. But it does mean that these kinds of operation can be tailored to the needs of a particular interface... e.g. the predicate case provides operations which make sense for a predicate, but not necessarily for an arbitrary function that returned a boolean.

$\endgroup$
2
  • $\begingroup$ Another neat thing about such functional interfaces is, if it makes sense to do so, you can write a proper class instead of just a lambda expression, and provide specialised implementations of negate, and and or which might be more performant than the default implementations. $\endgroup$
    – kaya3
    Jul 27, 2023 at 22:51
  • $\begingroup$ Yes... one util function I use regularly wraps a Function<V,R> and a Map<V,R>, invisibly caching the results of the former using the latter. Very handy for short-lived caching, e.g. avoiding unnecessary database hits while processing a list that may contain a lot of repeated references. $\endgroup$ Jul 31, 2023 at 0:23
2
$\begingroup$

I may know what's happening in Java and Rust.

It may worth noting that one needs to deal with closures (say, lambda expressions with local captures). This implies that for different lambda expressions (even if they have the same input/output type), we may need a different type to store its captures. So, it is good to pose this situation to the programmer: that the type of lambda expressions is an abstract thing which may have many implementations. Java and Rust are both under this principle.

$\endgroup$
2
$\begingroup$

I believe the important distinction is not so much whether functions are implemented as single-method objects or as a distinct function type, but whether or not standard types are provided.

There is an old joke that functional programmers say "objects are just poor man's closures" and OO programmers say "closures are just poor man's objects". On a more serious note, there is a deep connection hidden behind those quotes, that functions are isomorphic to objects with a single method and closures are isomorphic to objects with a single method and some instance variables.

Which means, if your language has some sort of concept of an object with attached methods, then it doesn't make sense to add functions as a distinct entity: you already have objects with a single method.

Conversely, if your language already has functions, you don't necessarily need objects with attached methods, you can have objects with properties that return functions, for example.

So, for a language like Java, Rust, or Scala, it makes sense to implement functions as objects with a single method. There are different degrees of "rigidness" that you can choose:

  • In Rust, a type that wants to act as a function must implement a specific trait with a specific method.
  • In Scala, a type that wants to act as a function must have a method named apply but doesn't need to implement a specific trait.
  • In Java, a type that wants to act as a function must have a single (abstract) method of arbitrary name and doesn't need to implement a specific interface.

All of these are valid design choices.

There is, however, a big difference between Rust and Scala on the one side and Java on the other: Rust (by necessity, since the trait is part of the definition of what it means to be "a function") and Scala (by choice) provide a standardized interface in their standard libraries. Java, until Java 8, did not.

Without a standardized interface in the standard library, every library author came up with their own function interfaces. Heck, even within the JRE, different parts of the JRE came up with different interfaces.

So, for example, you might have one library with a method

List<T> filter(MyPredicate<T> pred)

with

interface MyPredicate<T> {
  bool check(T value)
}

whereas another library may have

void validate(Validator<T> v)

with

interface Validator<T> {
  bool validate(T value)
}

This wouldn't be so much of a problem in a structural type system where names don't matter, but with Java's nominal type system, this is a problem: I cannot create a single predicate object and pass it to both methods. (Well, technically, I could implement both interfaces and both methods and have them forward to a single implementation, but you can see how that can become messy.)

Lambda literals are structurally typed, so I can write the same lambda literal for both, i.e. I can write

list.filter(elem -> doSomethingWith(elem));
value.validate(elem -> doSomethingWith(elem));

but I can not write something like:

SomeCommonFunctionType predicate = elem -> doSomethingWith(elem);

list.filter(predicate);
value.validate(predicate);

Because there is no type common to both functions.

Java has attempted to provide common function types in the java.util.function package, but by the time this package was created, there were already dozens if not hundreds distinct function types in common use.

In contrast, Scala provides standard function interfaces even though the only requirement in the language specification is that objects must have a method named apply. This is perfectly valid (runnable) Scala code, even though Foo does not inherit from one of the function interfaces:

class Foo:
  def apply(bar: String) = bar

val foo = Foo()

foo("Hello")

But it would be preferable (and most Scala programmers would do this) for it to implement Function1:

class Foo extends (String => String):
  def apply(bar: String) = bar

val foo = Foo()

foo("Hello")

By the way: String => String is not a special distinct function type, it is merely syntactic sugar for Function1[String, String].

In Scala, even Arrays and Maps implement the function interfaces: Arrays are functions from indices to elements, Maps are functions from keys to values.

Of course, this does not just apply to functions. You can make the same argument about every interface. For example, imagine Java didn't have the Iterator interface and every collection implemented iteration differently. Or if it didn't have the List interface and different list implementations had different method names for their operations.

If you take this argument to the extreme, then you come to the absurd conclusion that a standard library must provide every possible interface. That is obviously insane, so we need to find a balance.

However, specifically for functions, I would argue that they are fundamental and widely-used and thus deserve to be standardized.

$\endgroup$
2
  • $\begingroup$ "it doesn't make sense to add functions as a distinct entity: you already have objects with a single method" I disagree. From my experience in Kotlin, which is heavily OO, function objects are very welcome to me. A lot of clutter can be reduced, for example avoiding boxing and not having to search for the right BiTriConsumerProducerFunction. $\endgroup$
    – Seggan
    Jun 29, 2023 at 13:48
  • $\begingroup$ "Lambda literals are structurally typed" ─ I don't think this is entirely accurate, but I see where you're coming from. Lambda literals in Java are subject to a kind of type inference, so that type-checking of lambda expressions somewhat resembles type-checking in a structural type system. However, Java's type system doesn't have any rules about structural equivalence for function types (or any other types), it's just that the structure is used when determining the type in the first place. $\endgroup$
    – kaya3
    Jun 29, 2023 at 15:00

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .