As Operator Overload

This is a feature I've been longing for for quite a while. We already have bridges between String and NSString and many other Swift and Objective-C types. Would be amazing to be able to overload the as operator and create custom bridges with our own types. This would be super useful for the same reason that bridging between String and NSString is.

Using the example of URLSession.dataTask(with:). Sure, we could create an extension on URLSession and create a different version of URLSession .dataTask(with:) that takes a protocol like URLSessionConvertible and then we make HTTPMethod conform to that protocol. The problem is that now we'll have two functions that basically do the same thing and a lot of boilerplate to accomplish something that the Swift compiler can handle for us. I'm sure a lot of us went through something like this when bridging between different APIs with the same intent.

I also believe the impact on the language would be minimal, since we're not adding anything exceptionally new. We're reusing the as keyword and using the same mental model used for regular operator overloads.

import Foundation

public enum HTTPMethod : String {
    case get = "GET"
    case post = "POST"
}

public struct HTTPRequest {
    public let method: HTTPMethod
    public let url: URL
}

public func as(request: HTTPRequest) -> URLRequest {
    var urlRequest = URLRequest(url: request.url)
    urlRequest.httpMethod = request.method.rawValue
    return urlRequest
}

let url = URL(string: "https://www.apple.com")!
let request = HTTPRequest(method: .get, url: url)

// Explicit use of the `as` operator.
let urlRequest = request as URLRequest
// Implicit use through type inference.
let task = URLSession.shared.dataTask(with: request)
5 Likes

Alamofire does this too, but since we allow errors to be thrown, we'd need a throwing as as well. In general, I'm not sure this is a good idea, as it's not really type coercion, otherwise you could apply it to any protocol that has such a method.

Sorry, I didn't understand what you meant here.

For fallible casts we could allow something like this:

public func as(request: HTTPRequest) -> URLRequest? {
    guard someCheck else {
        return nil
    }
    
    ...
}

guard let urlRequest = request as? URLRequest else {
    return
}

This might need a little more thought, though.

Unfortunately that's not good enough, as there are times when you'd want Error expression.

My point was just that type coercion and producing a value from a method are not equivalent, so I'm not sure using as for both of them just to save a few characters is a good idea.

Sorry, but I don't see the benefit of this approach vs naïve approach:

import Foundation

public enum HTTPMethod : String {
    case get = "GET"
    case post = "POST"
}

public struct HTTPRequest {
    public let method: HTTPMethod
    public let url: URL
}

extension HTTPRequest {
  public func asURLRequest() {
    var urlRequest = URLRequest(url: request.url)
    urlRequest.httpMethod = request.method.rawValue
    return urlRequest
  }
}

let url = URL(string: "https://www.apple.com")!
let request = HTTPRequest(method: .get, url: url)

let urlRequest = request.asURLRequest()  // vs request as URLRequest
// Implicit use through type inference.
let task = URLSession.shared.dataTask(with: request)

As swift does not support implicit cast, you would still have to always write something like foo as bar. I'm not sure a language change is needed to have that form over foo.asBar()

1 Like

Yeah, I'm not a type theory expert, so I couldn't say what bad implications we could have from that. Again, comparing with the bridges that we already have, like String and NSString. They're not the same type, but we do have bridging between them. So I can't see how this is different. Is String and NSString bridging a bad idea?

The benefit would be the same as you get with String and NSString bridging. Less noise.

I think it exists for Obj-C bridging mostly for historical reason. In older Swift version, implicit casting was supported.

I'm not sure it really reduce noise: string.asNSString() is only a single char longer than string as NSString, and even shorter if you use computed property instead.

1 Like

Oh, right. We don't have implicit casting anymore. Yeah, that really takes a lot of the power of this proposal. Happen to know why it was removed?

I think I like the general idea of a Type conversion operator, but I don't think as is the thing to use.

as doesn't ever convert a Type, it checks if something is already that Type. It's a meaningful semantic difference that IMO shouldn't be messed with.

However, I do like how clean that syntax looks for something like this:

let newNSString = myString to NSString
let newString = from newNSString

I'm not sure why, but for whatever reason to me NSString(myString) feels almost annoying and doesn't feel like a Type conversion.

I wonder if a more direct conversion approach here could also be inferred by or simplify protocols like ExpressibleBy*Type*Literal and *Type*Able

2 Likes

That ship has sailed: https://github.com/apple/swift-evolution/blob/master/proposals/0213-literal-init-via-coercion.md

To be clear, I think that as is used as a cast operator since Swift 1. It has always been used for type bridging and literal typing.

That's not true. String and NSString are not the same type. They're bridgeable and use the as operator to bridge between them. I'm not proposing anything really new. Just that we can overload the as operator to have the same behaviour that Swift and NSString already have.

Bridging is not a synonym for conversion.

A smaller number of classes have a special relationship with each other called toll-free-bridging . When a CFTypeRef is toll-free-bridged with a Foundation class, its pointer can simply be cast to the appropriate type (in either direction) and it can be passed to functions or methods which expect this type.

In order for toll-free bridging to work, the Swift class and the CF struct must share the exact same memory layout. Additionally, each CF function that operates on an instance of the class has to first check to see if needs to call out to Swift first. This complexity adds a maintenance cost, so we choose to limit the number of toll-free-bridged classes to a few key places:

  • NSNumber and CFNumberRef
  • NSData and CFDataRef
  • NSDate and CFDateRef
  • NSURL and CFURLRef
  • NSCalendar and CFCalendarRef
  • NSTimeZone and CFTimeZoneRef
  • NSLocale and CFLocaleRef
  • NSCharacterSet and CFCharacterSetRef

Additionally, some classes share the same memory layout in CF, Foundation, and the Swift standard library.

  • NSString , CFStringRef , and String
  • NSArray , CFArrayRef , and Array
  • NSDictionary , CFDictionaryRef , and Dictionary
  • NSSet , CFSetRef , and Set

It may be interesting to expose a general facility for bridging between known layout-compatible types, but what you are discussing here is conversion, and as does not convert between types—it is the type coercion operator.

2 Likes

Can you elaborate/clarify this? I'm unsure what initializers/coercion has to do with as's behavior.

This documentation is probably obsolete. I can't see how Swift String and NSString can share the same memory layout, as they don't even have the same base representation (UTF-16 vs UTF-8).

Moreover, Swift types are value types, and Obj-C type are reference, so they really can't share the same memory layout. While this comment was true for bridging between NS and CF types, this probably don't apply to Swift types.

0 as Int
0 as Float
0 as Double

Here, as is not use to check a type, but to define the literal expression type.

let i: Int = 0
let f = i as Float
let d = i as Double

Here as is used to convert an integer value from one type to another.

So while as and as? can also be used for type checking, this is not the only usage it has in the language.
What I mean is that if you fear the as semantic be messed up by the proposed change, you can be reassured, it has already be done a long time ago.

A lot of engineering went into making sure that Swift String will continue to bridge seamlessly with UTF-16 NSString instances. The String storage class is an actual subclass of NSString at runtime.

1 Like

Here, you are indeed attempting to use as to convert between types, and it is invalid in Swift because as is not a conversion operator but a coercion operator. The error is:

Cannot convert value of type ‘Int’ to type ‘Float’ in coercion

Yes, but String's storage is not String. You actually convert from a NSString to a String with a custom storage, so there is actually a conversion here IMHO.