Implicit conversion with auto constructors

There are some implicit conversions with literals which are currently magical, like

let a: Double = 3

and there's clunky code related to conversions like when you want to do an exponent with two integers and get back an integer:

Int(pow(Double(radix), Double(power)))

I propose to clean up code and eliminate the magic around explicit conversions by adding a simple compile step that when you have:

let x: SomeType = yWithAnotherType    // This gives an error right now
// Check if there's a constructor for SomeType like init(x: AnotherType) and if so desugar to
let x: SomeType = SomeType(yWithAnotherType)

This could also be done for method arguments too so for instance when you do:

let x: Int = pow(yInt, zInt)
// Desugars to:
let x: Int = Int(pow(Double(radix), Double(power)))

But this may be harder since pow might be overloaded and there may be more than one definition of it that could support implicit conversion.

I think at least the first idea deserves some thought as it would eliminate the need for a lot of common tedious manual conversions and there's no possible ambiguity if you already defined the type for the variable. This could also be used for concise code like:

let card: Card = 'A♥'
let hand: [Card] = ['3♦', 'A♥', 'K♥']
1 Like

Something like that is possible now (with string literals on the right-hand side) if you adopt the ExpressibleByStringLiteral protocol.

7 Likes

Really interesting thanks. How about generalizing it so it's not only literals but also values from other variables to avoid excessive conversions in code? One thing I found easier in Kotlin is that you can just write code in sequences/streams/arrays and have them all implicitly convert to each other without worrying about doing it manually. Edit: This is wrong, kotlin doesn't do implicit conversions.

Hi @elhoov. This idea has been suggested many times. It is usually rejected very quickly. However, this thread is interesting because it explores many changes that would need to be performed in the language. I think it's a must-read for whoever is looking after implicit number conversion.

On top of that, Martin is right: Swift comes with many ExpressibleBy...Literal protocols for the last use case you suggest.

4 Likes

There is no conversion here. For someone familiar with other languages it may seem like it converts Int into Double, but in reality Double is being constructed directly from the literal

I don't like the idea of implicit conversions. They seem like a recipe for bugs. When people make fun of JavaScript it is because of implicit conversions between types half of the time

12 Likes

and there's clunky code related to conversions like when you want to do an exponent with two integers and get back an integer:

Int(pow(Double(radix), Double(power)))

That’s by design. Each of those conversions could potentially lose precision, since not every Int can be represented as a Double and vice versa. Requiring explicit conversions reminds us to consider these cases, and it usually isn’t too clunky since mathematical operations don’t usually involve too many different types.

While there have been times I’ve wished for implicit conversions, overall I don’t think it’s a good idea as it will make code less explicit and harder to reason about. In my experience, most problems can be solved much more cleanly with generics/protocols/better API design.

9 Likes

In the (very) early times of Swift you could define a func __conversion() -> T for custom conversions, that feature was (deliberately) removed. (I am still looking for a reference.)

3 Likes

I see, forgetting about implicit conversions, is there any reason why desugaring to constructor calls on variables with explicit types would be bad as in my first example?

let x: SomeType = yWithAnotherType    // This gives an error right now
// Check if there's a constructor for SomeType like init(x: AnotherType) and if so desugar to
let x: SomeType = SomeType(yWithAnotherType)

It seems like there's no room for ambiguity or for problems since you already explicitly define the type and the compiler changes would be minimal. It's also a generalization of __conversion and ExpressibleBy...Literal as far as I can tell.

I don't see how
let x: SomeType = yWithAnotherType
is better than
let x = SomeType(yWithAnotherType)

Remember that you don't need to always write the explicit type annotations for variables

3 Likes

It's not much more concise than with just constructer

let x: SomeType = yWithAnotherType
let x = SomeType(yWithAnotherType)

And usage outside let/var context is, as other mentioned, readily rejected.

4 Likes

I'm aware, however my reasoning was that for longer expressions I thought it's subjectively seems more readable to me:

let x: SomeType = functionCall(param1, param2).anotherFunction(param3).map{ $0.attr } \
                                 .reduce(startingVal, functionToApply)
// VS
let x = SomeType(functionCall(param1, param2).anotherFunction(param3).map{ $0.attr } \
                                 .reduce(startingVal, functionToApply))

However right now I'm struggling to find a real world example so take it with a grain of salt. The ExpressibleBy...Literal mentioned here is close enough to what I wanted to write that this is a pretty small thing.

1 Like

It's worth adding that what I said about Kotlin was wrong, it doesn't do implicit conversions yet I remember it not making my life so hard (this may mean it's APIs have better design or I just have bad memory), maybe because of the builtin .toList() on sequences which covers the most common cases for me. I understand this can easily be added with an extension if I really want that functionality.

PS: Tried Kotlin for a weekend project and then attempted to replicate in Swift so don't have extensive experience in either.

If you want your exponents to look simpler, just define an operator to be your own custom sugar.

import Foundation

infix operator ^*: BitwiseShiftPrecedence

extension Int {
    static func ^* (left: Int, right: Int) -> Int {
        return Int(pow(Double(left), Double(right)))
    }
}

for i in 0 ... 4 {
    let x = 2 ^* i
} // 1,2,4,8,16

You don’t need to change language features to achieve most of what you’re going for here. Convenience is great, but sometimes the more explicit is also the more clear.

1 Like

Surely you mean BitwiseShiftPrecedence

1 Like

Kotlin generally doesn't do implicit conversion, nor does it have the literal conversion Swift does. It generally handles conversion via instance methods.

val foo: Float = 3 //Error, the integer literal doesn't conform to Type: Float
val foo: Float = 3.toFloat() //valid
val foo: Float = 3F //valid

As you said, you may be remembering interaction with generic map/filter/etc. functions, (which Swift also has), or possibly Kotlin's expression-level Type inference (unsure on terminology) e.g.

val foo: Any = 3
if(foo is Int) {
    // foo is now treated as an Int
    val added = foo + 4
}

However there is no conversion happening, it's compile time verification that you're checking the Type before using it.

Yes, thanks. I couldn’t find the list of Precedence groups, so I put the closest one I knew. I’ve updated the code.

I agree that it might be nice to spell type conversions without explicitly calling the initializer. But I think it would be preferable to use as SomeType. That way it's still explicit that there is a conversion happening, but in a less prominent position than the init call. That lets the rest of the expression, which is likely to be more important than the specific result type, stand out a bit more.

I think this fits in well with existing usage of as, which only includes bridging as far as I know. It would still only be allowed when there is a SomeType.init(...: FirstType) available, of course.

2 Likes

Is that automatic bridging supposed to work, or are you suggesting it as a solution? I tried it and it didn’t work. Is there any reason it couldn’t be a solution?

class foo {
    var name: String = ""
}

class bar {
    var name: String = ""
    init(with:foo) {self.name = with.name}
}

let f = foo()
let b = f as bar //Error

init is (currently) the way to spell Type conversions in Swift. I wish there was a better solution in the language, but I haven't seen anything convincing yet. Changing as from only casting to also Type conversion IMO isn't much better and confuses the operator.

let foo = Foo(with: bar)
//vs
let foo = bar as Foo //This is slightly less code, but is less clear.

Swift is intentionally Type explicit and generally avoids implicit conversion. I don't think that design choice will change anytime soon. So if someone wants to improve conversions they need to find a solution that's markedly better than the current approach, but has a similar level of clarity.

1 Like

No; currently it's only used (as far as I'm aware)* for type conversions between bridged ObjC and Swift types such as Array and NSArray. That's why I think it would be a good candidate syntax for this: it's underused, and the semantics seem close enough to me.

*Oops, no, I was forgetting about upcasting. let sup = subInstance as SuperClass

Terms of Service

Privacy Policy

Cookie Policy