Treating a Type as a Function

In the pitch exploring KeyPaths and sorting, the idea was raised of treating KeyPaths as closures in certain contexts. It got me thinking that there are certain types (e.g. KeyPaths, ValueTransformers, Lenses) which are really an object form of a transformation or function.

I wonder if it would be worthwhile to have a general way to have Swift treat a Type as a function with a specific signature, meaning that the instance/value itself could be used where you would put a closure or function with that signature.

It should have syntax similar to subscripts, in that you are defining a function without func, instead using a keyword like typeFunc (but I am not in love with that term, so feel free to bikeshed)

extension KeyPath {
    public typeFunc (input:Root) -> Value {  //Now a keypath can be seen/used as (Root)->Value
        return input[keyPath: self]
    }
}

struct Person {
    let name:String
}

let people:[Person] = //Array of people

let names = people.map( \.name ) //KeyPath matches the signature

More than one typeFunc could exist on each type, but only one for each signature. For example, a writable keypath might also expose a function with a signature (Root,Value)->Root or (inout Root, Value)->()

I imagine that this would be quite useful for things like lenses and prisms.

Let me give another concrete use case. I have a lot of types which will produce another type when asked. For example, I have a type-erased Expr struct that wraps a cluster of structs adhering to an Expression protocol, all of which gives you a value of T when you ask for it. If I could instead just store any of those structs in a ()->T variable or maybe a ([Identifier:T])->T variable, then I don't have to worry about writing type erasure thunks...

2 Likes

One semi-controversial addition: All types should work as ()->T

Slightly more controversial: Should types also work as (???)->T... That is, should they work for any function that outputs the type (as long as there are no inout inputs)? In other words, you could put a type anywhere that expects a function outputting that type, and it would act as a constant...

1 Like

Seems like a nice idea to explore. The function could even return a closure instead, so like:

func getter(source: Root) -> ()->Value {
  return { source[keyPath: self] }
}

func setter(source: inout Root) -> (Value)->Void {
  return { source[keyPath: self] = $0 }
}
1 Like

It'd be reasonable to add getter and setter properties to KeyPath. We also eventually want to extend the \ syntax to refer to partially-applied methods as functions in addition to key paths.

8 Likes

Implicit behavior like this can make code hard to understand, and also requires exponential type-checking behavior. IMO, the explicit syntax for turning a value into a function, { value }, is about as syntactically lightweight as you can get already, and makes it clear what's going on.

5 Likes

What do you think about having a general syntax which lets us do the same for lenses, etc..., as opposed to just having magic for KeyPaths?

Key paths are already lenses. It might be interesting to have a general ExpressibleByKeyPathLiteral mechanism.

7 Likes

I have had some thoughts about this after seeing it discussed during the @dynamicCallable proposal.

The version I have been thinking about (and actually have a draft pitch of here) is to permit instance methods named _ and have a type be a subtype of the types of all of the types of the instance methods it has named _.

Not Edit: I had just posted this in the "Sort Collection using Keypath" thread, when I noticed this thread appear.

If we add something like this, precedent from @dynamicCallable implies we should use an @attribute, potentially something like @callable or @functionConvertible.

That could potentially take two forms:

  1. Any method of a certain name (e.g. _, call, typeFunc) becomes "callable", so a type could declare multiple callable methods and become convertible with multiple function types:
@callable
extension KeyPath {
    func call(with root: Root) -> Value { ... }
    func call(with root: Root) -> Root  { ... }
    // in this hypothetical implementation, `KeyPath` could
    // represent either `(Root) -> Value` or `(Root) -> Root`
}
  1. @callable could be parameterized, so only one specific method would participate in the behavior and the type would only be convertible with one specific function type:
@callable(map(_:))
struct KeyPath<Root, Value> {
    func map(_ root: Root) -> Value { ... }
    // in this hypothetical implementation, `KeyPath` could 
    // only represent `(Root) -> Value`.
}

Either way, the call-site behavior of types expressing this attribute seems fairly reasonable:

\String.count("value")
// interpreted as \String.count.call(with: "value")

["a", "bb", "ccc"].map(\.count)
// interpreted as ["a", "bb", "ccc"].map(\.count.call(with:))

I'm not a compiler engineer by any means, but it seems like parameterizing the @callable behavior to one specific function signature could have better performance implications (with respect to type inference)

(partial cross-post from Sort Collection using KeyPath)

This works type-algebraically - it's just @autoclosure as an implicit conversion. You would next need to figure out where this coercion slots into the existing coercions we have for subtyping and optionals. For example

class Base {}
class Derived : Base {}

let d: Derived = // ...
let one: () -> Derived? = d
let two: () -> Base? = d
let three: (() -> Base)? = d
// example continues for 6 more combinations

Slightly more controversial: Should types also work as (???)->T...

This one doesn't work (unless the synthesized closure is just the constant function) - a useless conversion.

2 Likes

I am really happy to see this topic picking up steam. I think it should be added as quickly as possible to provide the “static” alternative to dynamic callable.

One alternative to using a specific name would be to use an attribute on callable members as well as (or instead of) the type itself. This would allow subscripts to be callable, for example.

I very much like the idea of supporting multiple callable members on the same type. We would need to define semantics for how they interact though when they have compatible signatures (such as a generic member and a more specific concrete member). For example, attributes that would lead to ambiguous conversion should produce an error at the attribute declaration rather than at the usage / conversion site.

Another question is how default arguments are handled. Can a callable member with defaults be converted to more than one function type?

1 Like

Yes, the idea is that it would be a constant function.

I don't think it is completely useless. For example, when I am using Expressions (which take a dictionary and give a value), about 70% of the time, I actually just want a constant (with the option of making it an expression). I could wrap it in a constantExpression (which is what I do now), but it would be much nicer to be able to just stick the value itself in there and have it act as a constant.

Basically, I would like to be able to avoid as much boilerplate as possible, and Swift currently demands a lot of boilerplate for this kind of thing.

I don't think any type should be able to be implicitly convertible to () -> T, only types that implement func _() { … ). However, I do think any (extendible) type should be able to be extended with such a function.


I am not entirely sure of that, @dynamicCallable was added primarily in response to the need for better cross-language interop. for TensorFlow.
I see it more like @objc, not 'really' a 'pure Swift' feature, more on the pragmatic side of Swift:
"This needs to be done", so stick it on under an @attribute, so it doesn't clutter the less pragmatic parts of Swift.

Whereas, what is being discussed here is the less pragmatic part of Swift thinking: "That is a really cool feature (that we don't anticipate being able to replicate indirectly soon), how do we implement this right"

This isn't the right time / place for an @attribute, if we need an @attribute, it can wait till we don't.


I think that when one would want to use an "instance as a function" where two usable signatures match the desired signature, one would be able to use (if necessary):

let f = ((x as (T) -> V) // select signature
  as (U) throws -> W) // desired signature

Also, is it acceptable to describe these things as "functor"s, IDK if that word has similar / different meanings elsewhere, but it sounds right.

1 Like

I wanted to bump this because we have had two recent proposals that would be helped by being able to have key paths treated like a function, and there seemed to be a generally positive reception to the base idea, with a few different ideas about what it should look like.

FWIW, I think I have come around to @Dante-Broggi's idea that all types should not implicitly be ()->T, but they should be able to be explicitly extended to cover those cases as desired. (The original idea was that you should be able to stick T in any arbitrary place that wants a closure returning T and have it act as a constant.)

Anyway, the end result of this pitch would be:

struct Person {
    let name:String
}

let people:[Person] = //Array of people

let names = people.map( \.name ) //KeyPath matches the (Person)->String signature

The bikesheded syntax we have seen so far:

  • Using _ as the function name (my current favorite):
extension KeyPath {
    public func _ (input: Root) -> Value { //Now a KeyPath can be seen/used as (Root)->Value
        return input[keyPath: self]
    }
}
  • Similar syntax to a subscript using a keyword like typeFunc (I'm not attached to the name):
extension KeyPath {
    public typeFunc (input:Root) -> Value {  //Now a KeyPath can be seen/used as (Root)->Value
        return input[keyPath: self]
    }
}
  • Extending the dynamic callable stuff
@callable
extension KeyPath {
    func call(with root: Root) -> Value { ... }
    func call(with root: Root) -> Root  { ... }
    // in this hypothetical implementation, `KeyPath` could
    // represent either `(Root) -> Value` or `(Root) -> Root`
}

...in all of the above syntaxes you could define multiple function signatures that the type can act as. You could disambiguate using as. For example let f = (x as (T) -> V).

2 Likes

Coincidentally, I was just about to start a thread about treating key paths as function types. Rather than any of the extensible mechanisms you propose here, which are more wide-reaching, I think most of the actual use cases we've been talking about here on this forum lately are covered by making the compiler implicitly convert KeyPath<Base,Value> to (Base) -> Value whenever that is required to type check an expression.

I have implemented such a coercion here: WIP [ConstraintSystem] Ability to treat key path literal syntax as function type (Root) -> Value. by gregomni · Pull Request #19448 · apple/swift · GitHub

This was previously discussed in this older thread: Key path getter promotion

8 Likes

I am happy to see this topic get bumped again! Progress to support key paths would be very nice, but only if it aligns with a broader design that supports arbitrary (static) callable types. I think part of the reason the use cases that come up usually have to do with key paths is that they already exist but have obvious gaps in utility.

Since (static) callable types do not exist yet people are not actively exploring designs that would make use of them, and therefore specific use cases do not come up as often. This does not mean they have less utility. Personally, I think filling this gap should be a priority now that the language supports dynamically callable types. It rubs me the wrong way that we have syntactic sugar that is only available when you sacrifice type safety despite there being an obvious type-safe equivalent.

5 Likes

Huge +1 to this.

I think KeyPath<T, U> should be interchangeable with (T) -> U, but I don't think it should be implemented as compiler magic blessed upon the KeyPath type.

Big fan of this design, since it mirrors @dynamicCallable for the most part:

One of the issues that came up before was that it may need variadic generics or some other features not-yet present in the language. On the other hand, the new ExpressibleByStringInterpolation design even allows multiple methods to be defined with varying:

  • generic parameters
  • number of arguments
  • types of arguments
  • argument labels

all with a single base-name, and they all get matched to some unspellable protocol requirement. We'd need to bikeshed it a bit (attribute vs protocol, conversion to closure-type, etc), but I think we can definitely do this.

This is a bit off topic, but speaking of literals, I think it would also be cool to have an ExpressibleByClosureLiteral protocol which leveraged compiler magic to support a variety of closure signatures. It would enabled user-defined types to be used with trailing closure syntax.

This feature along with static callable would put user-defined types on roughly equivalent syntactic footing with Swift's built-in function type.

2 Likes

Regarding the ability to use an arbitrary value as a type (as discussed in this thread), and ABI stability, I have wondered if the ABI supports converting a type to a function, using that function, and then getting the original value back out. Or in pseudo-Swift:

let foo: T = // …
let fooFunc = foo as (U) → V
let foo2 = fooFunc as! T

Is this currently possible in the ABI?
Would this be an additive change to the ABI?