FunctionalProtocols for Swift

An equivalent to Java Functional Interfaces in Swift?

I‘m locking for a functional interface equivalent in Swift. Swift is a Protocol-orientated programming language so this and known fr syntactic suger.

Function Interfaces in Java are the best feature of the language in my opinion and more swifty than the Swift solution(s). So I hope that the current features of Swift like macro's, Operator overloading, nested protocols are sufficient for this.

   protocol FunctionalProtocol{

       func get( param1: String) -> Int 

   }


   @functionalProtocol
   let myFunc: FunctionalProtocol = { (param1: String)  -> Int
       return param1.count + 2
   }

   let helloWorldCounter = myFunc(„Hello World“)

   print(„Count: \(helloWorldCounter)“)

I know that this is similar to closures, but mind that this should be a valid protocol type.

2 Likes

Would something like this do the job for you?

protocol FunctionalProtocol {
  func callAsFunction(_ param: String) -> Int
}

struct AnyFunctionalProtocol {
  let run: (String) -> Int 

  func callAsFunction(_ param: String) -> Int {
    run(param)
  }
}

let myFunc: any FunctionalProtocol = AnyFunctionalProtocol { param in
  param.count + 2
}

// etc...

I suspect you could come up with a macro to make the Any[X] type-erased wrapper unnecessary. Alternatively, you could ditch the protocol and only use the struct wrapper.

2 Likes

This solution looks nice.
I try to hide what I can.

Thanks

Haven’t touched Java for a long time and could be wrong, but isn’t it just a hack to have lambdas and types for them in Java? What are advantages of it?
Don’t get how it’s more swifty, when Swift already have functions as a first class citizens :thinking:

2 Likes

The benefit of Java functional Interfaces is that you don’t need to declare a class which implements the interface. You could initialize it with a lambda directly and use it like a adapter as well, without the ballast of full class implementation.

For example:

   public class Main{

       public static execute(Runnable action){
           action.run();
       }


       public static main(String args[]){
           
            final Runnable doSomething -> (){
                System.out.println(„Hello World!“);
            };


            execute(doSomething); // Hello World!
       }
   }

It looks like I had the wrong view of the common interface. Here what I actually wanted to do. Note that the Protocol isn't required for this.

/// verbose why
protocol Runnable {
  var action: () -> Void { get }
}

struct Runner: Runnable {
    let action = { () -> Void in
        print("Hello World!")
    }
}

/// short why
let onTheFlyRunner = { () -> Void in
    print("Hello World!")
}

func executeClosure(closure: () -> Void) -> Void {
     closure()
}

func main() {
    /// verbose why
    let runner = Runner()
    executeClosure(closure: runner.action) // Hello World
    
    /// short why
    executeClosure(closure: onTheFlyRunner) // Hello World

    /// Or this why
    executeClosure {    // Hello World
        print("Hello World!")
    }
}

main()
``
1 Like

Java's functional interfaces are not just a way to make lambdas present in the type system but a way to make them "feel at home" in there :slight_smile: Since the JDK is heavily interface based, with things like Runnable going back to day 1.0 versions really, what the JDK did there is to make interface with a single requirement be expressible with a lambda literal.

In other words, an API like this (I'll write it in Swift right away to show how we could make use of the same ideas technically):

func run(r: Runnable) { r.run() }

can be invoked by callers like so:

run({ () -> print("hi") })
// with more sugar it's just: run { print("hi") } 

So what Java did there is that "we have a whole JDK of such interfaces, Callable, Runnable etc, so rather than introduce new APIs accepting lambdas everywhere (including third party APIs). Let's just make any such protocol expressible with a lambda".

Funnily enough, Swift does have a well established pattern for this: the ExpressibleBy...Literal protocols.

Notably, we do not have a ExpressibleByFunctionLiteral protocol, which is exactly what Java's functional interfaces are.

It might be nice, but since Swift always has had function types, we don't really have the pushing need to introduce this conversion because of some existing code not adopting function types for ages to come... It may be still interesting though -- it makes it IMHO much better to document APIs. If we have a ThingyConsumer it documents itself a bit nicer than a huge function type, and it also can be nicely documented...

It should be noted that the @FunctionalInferface does not "enable" this feature -- this is always enabled, but the annotation helps you make sure an interface is possible to be used in this way. I.e. that if there are multiple requirements, they have default implementations etc.

On the other hand... you can just typealias ThingyConsumer = (Thingy) -> () to achieve the exact same result, without having to use a protocol there.

With protocols you'd likely also want to force using some ThingyConsumer so you don't pay wrapper costs etc...

All in all, it's definitely interesting to think about, but I am unsure if it pulls its weight... We'd have to think about more complex protocols with default implementations, and allow them to be expressed using a literal and see how often such pattern actually pops up in reality.

^ Just personal opinion. I have had a long time of JVM experience beforehand though, so figured I'll chime in on the reasons why this was done this way there.

14 Likes

Yeah, by „hack“ I meant be more adapted to Java. Thx for clearing up. :slightly_smiling_face:

Still

For that purposes I usually use typealiases or wrap function in a struct. So not sure if adding functional interface is worth it :thinking: as again, it feels like it was meant for Java. Haven’t seen one for Scala btw, I guess it’s also just function types there?

1 Like

Yeah, the Scala way is also just typealiases usually.

Though functions in Scala are again just classes under the covers, same as most things on the JVM (Function1[In, O], Function2[In1, In2, O] etc :wink:).

2 Likes

This could be just

typealias Runnable = () -> ()

and then

let runner: Runnable = {
  print("Hello World!")
}
executeClosure(closure: runner) // Hello World

Yeah, as you see I'm still not sold to Functional Interfaces in Swift :sweat_smile:

3 Likes

How @ktoso said:

it also can be nicely documented...

And in my opinion it's a bit less complicated to read / write so you have the auto completion.

typealias Runnable = () -> ()

func executeClosure(closure: Runnable) -> Void {
     closure()
}

let runner = {
  print("Hello World!")
}

executeClosure(closure: runner)

On the other hand the why without a typealias:

func executeClosure(closure: () -> ()) -> Void {
     closure()
}

let runner = {
  print("Hello World!")
}

executeClosure(closure: runner)

But in the end of the day is personal preference.
It could also be that I'm just more used to this.

After I write that though I kept writing about the typealias approach. It seems docc can already document typealiases, so that's the documentation argument doesn't change much really.

There's some issues around improving how typealiases are documented but that's more of a docc thing than necessity for new language feature.

Having that said; I do find myself sometimes wrapping functions in wrapper structs anyway, so perhaps this might be still an interesting feature -- on the fence about it heh :slight_smile:

2 Likes

I agree you @ktoso especially for frequently used function heads like () -> ().

There are a lot of delegates in Apple APIs that just have a single function requirement, such as e.g. NSCacheDelegate, NSImageDelegate etc. Conformers need to implement a single function.

I guess it would be nice-ish to be able to assign a closure where such a delegate is expected. idk.

2 Likes

I have thought about this too while trying to make closures conditionally Sendable and came to the same idea. Closures don't really participate in the generics system and therefore closures can't be conditionally Sendable today (or conform to any other protocol conditional). On the other hand, Protocols and structs/enums/classes play very nicely with the generics system but lack the convince described in this thread.

If we extend on this idea and combine it with other concepts this becomes even more powerful:

1. Safely capturing mutable variables in a Sendable closure

Some Sendable closures are guaranteed to never run concurrently but we can't express this today in the type system. Therefore it is never allowed that they capture any mutable state, even if it would be safe. However, protocols can express this through a mutating function which guarantees exclusive access and making it non-copyable gives the closure still reference semantics:

protocol SerialSendableFunction: ~Copyable, Sendable {
    mutating func callAsFunction()
}

// requirement: only the closure access this mutable variable
var counter: Int = 0
let closure: SerialFunction = {
    counter += 1
    print(counter)
}
/// would get translated to:
struct $SerialFunction: ~Copyable, SerialFunction {
    var counter: Int = 0
    mutating func callAsFunction() {
        counter += 1
        print(counter)
    }
}
let closure = $SerialFunction(counter: 0)

2. Inline storage for captured variables

As a side benefit of making it non-copyable we also reduce the allocation from one allocation for the closure context to zero because the variable is stored inline in the non-copyable struct. Closures have an optimization if nothing is captures or exactly one AnyObject but otherwise needs to allocate storage on the heap for them. (currently the compiler actually allocates each variable separately but this seems fixable)

This was also mentioned in the motivation of the Callable proposal.

In theory this should help the optimizer as well as it can now see through the closures more easily because they are now types and the compiler doesn't need to inline everything.

3. Closures with value semantics

Closures could in theory have value semantics if they are translated to a Copyable struct, even if capturing mutable variables. I think this would be very unintuitive at first and would probably require some special syntax at the call site to make it clear if a closure has value or reference semantics. However, I think it could still be very valuable to implement stateful but "restartable" operations. On example of this is a HTTPClient configuration for redirects e.g.:

protocol HTTPRedirectPolicy: Sendable /* implicit Copyable */ {
    mutating func callAsFunction(_ redirectURL: URL) -> Bool
}

var config = HTTPConfig()

var redirectCountPerRequest: Int = 0
// property is of type `any HTTPRedirectPolicy`
config.shouldFollowRedirect = { _ in {
   redirectCountPerRequest += 1
   return redirectCountPerRequest < 3
}
// converted to
struct $HTTPRedirectPolicy: Sendable /* implicit Copyable */ {
    var redirectCountPerRequest: Int
    mutating func callAsFunction(_ redirectURL: URL) -> Bool {
       redirectCountPerRequest += 1
       return redirectCountPerRequest < 3
    }
}
config.shouldFollowRedirect = $HTTPRedirectPolicy(redirectCountPerRequest: 0)

config.shouldFollowRedirect is now a template from which the HTTPClient creates a copy for each request. As I said earlier, value semantics would be rather unitive here as we are all used to reference semantics but nevertheless useful. Just an idea that builds on top of the rest that I expect to be the most controversial. Each individual feature would be valuable on its own so this doesn't need to be included for the rest to be useful.

Alternatives

Closures as Generic Type Constraints

Instead of this we could also make closures/functions participle in the generics system. In addition we would also need ~Copyable and mutating closures.

The other way around - Types as Closures

Once a defined closure grows in complexity or the same logic is used in multiple places, I often struggle to find a good place to put it. Being able to define a type that can be used at places where closures are used would be very nice as well! Types are a great place to put documentation. We could accept types that have a callAsFunction() method defined at places where closures are accepted. This almost works today by we need to add .callAsFunction to make it compile:

class Foo {
    var counter: Int
    init(counter: Int) {
        self.counter = counter
    }
    func callAsFunction() {
        counter += 1
        print(counter)
    }
}

func functionWithClosureArgument(_ function: () -> ()) {
    function()
}

// .callAsFunction required today
functionWithClosureArgument(Foo(counter: 0).callAsFunction)
// instead we could allow the type to be passed directly
functionWithClosureArgument(Foo(counter: 0))

This seems almost like a bug fix but I'm probably overlooking some overload or type inference ambiguities that would arise from this change.


Overall I think it would make perfect sense to bring closures and protocols closer together. ~Escapable is now also coming to protocols (and other types), a privilege that was previously reserved for closures. Ideally closures should just be syntax sugar.

3 Likes

Delegates are actually a problem for this, because most of them are weakly held! If you just pass a closure and it's implicitly converted to an object, nothing keeps it alive, and the object is immediately deallocated. We ran into this early on when people asked for "strong if struct, weak if class" references, which would certainly be convenient in many cases, but would also make it easier to hide retain cycles if (the structs contained class references | the closures capture class references). Closure callbacks are used elsewhere, though, so it's a compromise regardless.

3 Likes

NSCacheDelegate has just one method today but there is no guarantee there won't be more in some future OS version.

2 Likes

That is true but that would be a breaking change without a default implementation or without being @objc optional. However, it could make it ambiguous if all methods provide a default implementation which AFAICT is not a source breaking change today.

Regardless of this, I think protocols should opt into this behaviour by either naming the method callAsFunction or, if we think this is not enough (we could still have multiple callAsFunction methods but different signature), by annotating the method with some attribute explicitly.

2 Likes

Yes, I meant "optional methods" just didn't mention it. All or most of the protocol methods of NSCacheDelegate , NSImageDelegate brought up above are optional. FWIW, with this and pretty much all other protocols in Apple OS'es we would need to be compatible with Objective-C.

1 Like

Yeah, sendable and escaping are the only things to concern about. On the other hand they are due to how things done in the language, specifically memory management and concurrency. Functional Interfaces on the other hand are unique to Java and not sure it makes sense to copy approach directly.

Sometimes I wonder how Sendable would look like if Swift was designed today from scratch, but don’t have much of experience in language design even to imagine. :sweat_smile:

1 Like