Operator function like asXxx() for overwriting the type cast


#1

We already have the keyword as for type cast. And we also have many function look like asXxx() (such as asSequence(), asIterable()) for transferring object into another type, without any relationship in type system.
I came up with an idea: why not we just combine them? That is, if we can add modifier operator on a member or extension function asXxx():Xxx. When using keyword as, this function will be automatically called. If no function matching the type, then draw back to original type cast. The code can look like:

interface Point {
    var x: Double
    var y: Double
}

interface Vector {
    val size: Int
    operator fun get(index: Int): Double
    operator fun set(index: Int, value: Double)
}

// an extension operator function for type cast
operator fun Point.asVector() = object : Vector {
    override val size: Int get() = 2
    override fun get(index: Int) = when (index) {
        0 -> x
        1 -> y
        else -> throw IndexOutOfBoundsException()
    }
    override fun set(index: Int, value: Double) = when (index) {
        0 -> x = value
        1 -> y = value
        else -> throw IndexOutOfBoundsException()
    }
}

 // a simple implement of point
class PointImpl(override var x: Double, override var y: Double) : Point

fun main(args: Array<String>) {

    // an instance of Point
    val point = PointImpl(1.0, 2.0)

    // cast it into Vector, while Vector is not extending Point
    // actually function asVector() is called
    val vector = point as Vector
    
    //after the cast we can use it 'as' a vector 
    println("vector[0] = " + vector[0])
}

There is some points should be mentioned:

  • Meaning of keyword “as” is changed. With such an operation function, we can overwrite the behavior of type cast. And at the same time, the meaning of as keyword will be changed - during such cast, actual inheritance is not required. But, in my opinion, it’s more close to the real meaning of word “as”.

  • Yeah, it’s something like trait. Such a feature is quite like what is called “trait”. We can implement a type with another type just adding an extension function. But it’s not implicit since we need to explicitly indicate the cast with keyword as.

  • The smart cast should work. And so do “as?”. Using as? should check the existence of matching function, maybe at runtime. So we can write such code like:

    fun doSomething(point: Point){
        //cast using keyword "as?"
        (point as? Vector) ?: return
      	
        // with smart cast, we can just use point "as" a vector
        println("vector[0] = " + point[0])
        println("vector[1] = " + point[0])
    }
    
  • About naming. Usually, the function should be named as asXxx, in which Xxx is the class name of return type. But when meeting conflict on naming, the function can be named in other way, since the return type is actually defined what exact type to cast. Maybe need an annotation?

  • About Java interop. There is no such many problems in java interop, comparing with other scheme of “trait”, since it’s using a function for casting. And maybe is also possible to recognize such functions in Java and allow us to use it as an “operator” one (just like the some other operators).

I believe that this is just a rough idea, and may missed some potential problems. But I really think it make sense, because it’s the best idea I’ve ever seen to make something like “trait”. And even more, since it let us to overwrite type casting, there can be more use cases.

So guys, how do you think about it? :slightly_smiling_face:


#2

I think you are not the first to suggest something like this. I personally don’t like the idea of adding something like this. It neither adds new functionality nor makes the code more readable. It just hides information, because you are no longer able to differentiate between casting to a different type or generating a wrapper object.
Also there is the problem that often when we talk about casting a object form one type to another we think about cast-functions that generate a new distinct object. asXXX however normally means that the objects represent the same state and changes on one will be reflected in the other (at least in kotlin).
That means that we would probably also need to create a similar operator to which would work in that way.

About smart casts: I think this feature has a small problem with smart casts. Mainly the syntax you use in your example.
Although (point as? Vector) ?: return should work to smart cast point to a Vector it’s the wrong syntax to use form an idiomatic viewpoint. if(point !is Vector) return is far more readable and expresses the intention much more clearly IMO. The problem is that I don’t think your proposal should change the way the is operator works, therefore making your code fail.

Also there is the minus 100 points rule


#3

Maybe you misunderstand the meaning of the operator as?.

  • If we use is as you say, we can only check the type of it, ignoring the possibility of using it with a wrapper.
  • In my case, we only care about whether this object “can” work as a vector, no matter it is realized with a wrapper or being a subtype. In this case we can care more about the functionality but not implementation. And that’s why we hide the information of warping.

There’s some use cases, like transferring components between different frameworks, for example, transfering java.awt.Point into javafx.geometry.Point2D (just an example :upside_down_face:).

And about “no new functionality”, I think it more like a standard on type transferring, but not a new function, after all, we have already many function looks like asXxx(). And after that we can have more function based on this standard, like checking the compatibility at runtime.


#4

No I don’t think I do. I understand why you chose to use as? instead of is in your example . I just think that (point as? Vector) ?: return as a standalone statement is bad coding style, because it is a convoluted way of expressing what you are trying to do. I don’t like the idea of is and as no longer being paired as they should be.

But we already have a standard for that. Functions like that are called asXxx(). There is no reason why they would need a special calling syntax. The only real advantage to your proposal is that combined with smart cast you can treat your value both as Vector and as Point at the same time. This might however lead to more confusion instead of making the code more readable.

Those use cases are already covered by the existing standard.


Another point to consider is this.

// let's take your Vector and Point interfaces from above
fun foo(p: Point) = TODO()
fun foo(v: Vector) = TODO()

fun someFun() {
    val p = getSomePoint()
    (p as? Vector) ?: return
    
    // now p is both Vector and Point
    foo(p)
}

Which of the 2 functions do we call?


#5

This is actually done in Groovy (using to instead of as) for implicit casts. The result is somehow disappointing. There is a lot of confusing behavior and almost no added value.