Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inline classes #103

Merged
merged 4 commits into from
Jul 13, 2018
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
397 changes: 397 additions & 0 deletions proposals/inline-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,397 @@
# Inline classes
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd add a section about our arrays struggles


* **Type**: Design proposal
* **Author**: Mikhail Zarechenskiy
* **Contributors**: Andrey Breslav, Denis Zharkov, Ilya Gorbunov, Roman Elizarov, Stanislav Erokhin
* **Status**: Under consideration
* **Prototype**: Implemented in Kotlin 1.2.30

Discussion of this proposal is held in [this issue](https://github.com/Kotlin/KEEP/issues/104).

## Summary

Currently, there is no performant way to create wrapper for a value of a corresponding type. The only way is to create a usual class,
but the use of such classes would require additional heap allocations, which can be critical for many use cases.

We propose to support identityless inline classes that would allow to introduce wrappers for values without additional overhead related
to additional heap allocations.

## Motivation / use cases

Inline classes allow to create wrappers for a value of a certain type and such wrappers would be fully inlined.
This is similar to type aliases but inline classes are not assignment-compatible with the corresponding underlying types.

Use cases:

- Unsigned types
```kotlin
inline class UInt(private val value: Int) { ... }
inline class UShort(private val value: Short) { ... }
inline class UByte(private val value: Byte) { ... }
inline class ULong(private val value: Long) { ... }
```

- Native types like `size_t` for Kotlin/Native
- Inline enum classes
- Int enum for [Android IntDef](https://developer.android.com/reference/android/support/annotation/IntDef.html)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this suppose to work?
Of course, you can create one inline class for each int, but it's not clear how this help with enum case.
Some kind enums based on inline classes or sealed would be really helpful tho
Are inline enums part of this proposal?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe inline enum classes deserve another proposal, but I'll put here a bit information about them

- String enum for Kotlin/JS (see [WebIDL enums](https://www.w3.org/TR/WebIDL-1/#idl-enums))

Example:
```kotlin
inline enum class Foo(val x: Int) {
A(0), B(1);

fun example() { ... }
}
```

The constructor's arguments should be constant values and the values should be different for different entries.


- Units of measurement
- Result type (aka Try monad) [KT-18608](https://youtrack.jetbrains.com/issue/KT-18608)
- Inline property delegates
```kotlin
class A {
var something by InlinedDelegate(Foo()) // no actual instantiation of `InlinedDelegate`
}


inline class InlinedDelegate<T>(var node: T) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inline class must have a single read-only (val) property as an underlying value, which is defined in primary constructor

There is an inconsistency

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say that this is an open question. We can support mutable inline classes if they will be used only with delegated properties. Currently we're looking for use cases

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zarechenskiy,
having a delegate with a backing field is interesting, however rules exceptions are harder to explain and understand.

operator fun setValue(thisRef: A, property: KProperty<*>, value: T) {
if (node !== value) {
thisRef.notify(node, value)
}
node = value
}

operator fun getValue(thisRef: A, property: KProperty<*>): T {
return node
}
}
```

- Inline wrappers
- Typed wrappers
```kotlin
inline class Name(private val s: String)
inline class Password(private val s: String)

fun foo() {
var n = Name("n") // no actual instantiation, on JVM type of `n` is String
val p = Password("p")
n = "other" // type mismatch error
n = p // type mismatch error
}
```

- API refinement
```
// Java
public class Foo {
public Object[] objects() { ... }
}

// Kotlin
inline class RefinedFoo(val f: Foo) {
inline fun <T> array(): Array<T> = f.objects() as Array<T>
}
```

## Description

Inline classes are declared using soft keyword `inline` and must have a single property:
```kotlin
inline class Foo(val i: Int)
```
Property `i` defines type of the underlying runtime representation for inline class `Foo`, while at compile time type will be `Foo`.

From language point of view, inline classes can be considered as restricted classes, they can declare various members, operators,
have generics.

Example:
```kotlin
inline class Name(val s: String) : Comparable<Name> {
override fun compareTo(other: Name): Int = s.compareTo(other.s)

fun greet() {
println("Hello, $s")
}
}

fun greet() {
val name = Name("Kotlin") // there is no actual instantiation of class `Name`
name.greet() // method `greet` is called as a static method
}
```

## Current limitations

Currently, inline classes must satisfy the following requirements:

- Inline class must have a public primary constructor with a single value parameter
- Inline class must have a single read-only (`val`) property as an underlying value, which is defined in primary constructor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it impossible to use var? Is it technical limitation or design decision?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say it's both :)
Short answer is that because inline classes are identityless, they don't have state

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inline class Name(var s: String)

val name = Name("Alan")
name.s = "Bob"

Is name read-only or not?
How estimate the impact of change the Name.s property in a shared boxed type?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any problems with single-property class which violates other statements?

inline class Something(t: T) {
    init {
        // some init, checks
    }
    val value: U = transform(t)
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Miha-x64 This is the same as if we have private primary constructor + public secondary constructor, therefore we are going to prohibit such cases for now

Copy link

@ricmf ricmf Apr 17, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the reason for the single-value restriction?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ricmf It can only work on a single reference. If you have more than one property, you need two references, and that only works with a real enclosing instance (that is allocated, not inline). However, you can perfectly make an inline class enclosing a Pair if it makes sense.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it is possible to use multiple values, I am thinking about it.
It should work like JPA Embeddable class , JVM escape analysis should avoid extra allocation, probably this is out of scope and we should wait for Valhalla.

The problem become tricky in presence of annotations.

- Underlying value cannot be of the same type that is containing inline class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say, having undefined (recursively defined), e.g. generics with an upper bound equal to the class should be prohibited as well.
Or like this

inline class Id<T>(val x: T)
inline class A(val w: Id<A>)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with restriction about recursively defined generics, thanks!

About the example, it's OK if we'll map Id<T> always to Any, I'll add a note about this

- Inline class with undefined (recursively defined) generics, e.g. generics with an upper bound equal to the class, is prohibited
```kotlin
inline class A<T : A<T>>(val x: T) // error
```
- Inline class cannot have `init` block
- Inline class must be final
Copy link
Contributor

@LouisCAD LouisCAD Apr 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would open inline classes not be allowed?
Couldn't they be extended by other inline classes that would only be additional API wrappers that could add more restrictions and more functions?
Such restrictions added to an inline class extending another, open one could be:

  • Narrowing down the underlying value to a child type of the one defined in the parent inline class
  • Adding additional checks/logic to init blocks (if they are allowed)
  • Overriding open methods from the parent inline class

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How runtime should resolve which subtype of inline class it deals with?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Miha-x64 The inline classes have a boxed (non inline) type for cases where you use generics or super types like Any, so either it can be done statically, without autoboxing, or it is boxed, so behaves like a regular class, until it reaches an auto-unboxing place again.

To make an analogy to something existing, it would work the same as primitive number types, who have their boxed types extending the Number class.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LouisCAD to assign Int value to a Number variable, you must box it, which eliminates the whole purpose of inline classes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Miha-x64 Does boxing an Int somewhere in your program eliminates the whole purpose of primitive, non boxed integers in your whole program? I don't think so, and the same applies for inline classes.
If you always use them in ways that force autoboxing, then, yes, its pointless, but you'll usually not write autoboxing code everywhere.

- Inline class can implement only interfaces

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe explicitly clarify that inline class cannot extend other classes?

- Inline class cannot have backing fields
- Hence, it follows that inline class can have only simple computable properties (no lateinit/delegated properties)
- Inline class cannot have inner classes
- Inline class must be a toplevel class

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm, that's pretty limiting, why is that?

Use-case: inside a class I want to use an inline class as an implementation detail, but I don't want other classes to know about it

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, this is too limiting, thanks!
It's definitely can be a nested member, I'll think more about it


Sidenotes:

- Let's elaborate requirement to have public primary constructor and restriction of `init` blocks.
For example, we want to have an inline class for some bounded value:
```kotlin
inline class Positive(val value: Int) {
init {
assert(value > 0) "Value isn't positive: $value"
}
}

fun foo(p: Positive) {}
```

Because of inlining, method `foo` have type `int` from Java POV, so we can pass to method `foo` everything we want and `init`
block will not be executed. Since we cannot control behaviour of `init` block execution, we restrict it for inline classes.

Unfortunately, it's not enough, because `init` blocks can be emulated via factory methods:
```kotlin
inline class Positive private constructor(val value: Int) {
companion object {
fun create(x: Int) {
assert(x > 0) "Value isn't positive: x"
return Positive(x)
}
}
}

fun foo(p: Positive) {}
```

Again, method `foo` have type `int` from Java POV, so we can indirectly create values of type `Positive`
even with the presence of private constructor.

To make behaviour more predictable and consistent with Java, we demand public primary constructor and restrict `init` blocks.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not allow init blocks and private constructors by enforcing boxed type when used from Java? This would work similarly to inline functions that are inline when invoked in Kotlin, but not when called from Java or the debugger.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this approach is nice, some kind of "closed world" for inline classes.
But note that inline functions cannot be virtual, while inline class types can be used everywhere. To enforce boxing for Java we'll have to duplicate methods, plus there are some issues with arrays (if we'll allow them, we'll have to box each element of an array on the border)

I'd say that we should think more about this approach, at least to understand our design space

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zarechenskiy It should not duplicate methods, but rather, add a method that takes the boxed type that is visible in Java, which unboxes the value and call the static method that takes the unboxed value.
In case the defined method is also inline, the method with the boxed type would contain all the logic directly, since no method for the unboxed type would be generated (since they would all be inlined at call places).


### Other restrictions

The following restrictions are related to the usages of inline classes:

- Referential equality (`===`) is prohibited for inline classes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But why? Shouldn't it just do referential equality check on underlying object?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artem-zinnatullin That would be dangerous. Let's say two different inline classes both wrap an int which happens to have a value of 0. It would be considered equals reference while not representing the same thing at all?
Allowing reference equality would defeat the purpose of inline classes.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point 👍

I was thinking about it from Java perspective where I'll still be able to do reference comparison, I think it's fine to prohibit === from Kotlin side unless someone comes up with a good use-case.

- vararg of inline class type is prohibited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why vararg is not allowed?

Copy link
Contributor

@gildor gildor Apr 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general would be good to provide some basic comments about particular limitations/restrictons

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because it's not clear what type should it have. I'll add a section about arrays and then cover this question

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @zarechenskiy, can you expose some clarification about List<T>, Array<T> and vararg T?

```
inline class Foo(val s: String)

fun test(vararg foos: Foo) { ... } // should be an error
```

## Java interoperability
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also add some short description/examples of how these wrappers look like and where the methods' bodies are

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we need JS and Native interoperability sections as well :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At some point we'll have it :)
For now, I'm going to put more information about common and expect/actual inline classes


Each inline class has its own wrapper, which is boxed/unboxed by the same rules as for primitive types.
Basically, rule for boxing can be formulated as follows: inline class is boxed when it is used as another type.
Unboxed inline class is used when value is statically known to be inline class.

Examples:
```kotlin
interface I

inline class Foo(val i: Int) : I

fun asInline(f: Foo) {}
fun <T> asGeneric(x: T) {}
fun asInterface(i: I) {}
fun asNullable(i: Foo?) {}

fun <T> id(x: T): T = x

fun test(f: Foo) {
asInline(f)
asGeneric(f) // boxing
asInterface(f) // boxing
asNullable(f) // boxing

val c = id(f) // boxing/unboxing, c is unboxed
}
```

Since boxing doesn't have side effects as is, it's possible to reuse various optimizations that are done for primitive types.

### Type mapping on JVM

Depending on the underlying type of inline class and its declaration site, inline class type can be mapped to the underlying type or to the
boxed type.

| Underlying Type \ Declaration Site | Not-null | Nullable | Generic |
| --------------------------------------------- | --------------- | ---------------- | ---------------- |
| **Reference type** | Underlying type | Underlying type | Boxed type |
| **Nullable reference type** | Underlying type | Boxed type | Boxed type |
| **Primitive type** | Underlying type | Boxed type | Boxed type |
| **Nullable primitive type** | Underlying type | Boxed type | Boxed type |

For example, from this table it follows that nullable inline class which is based on reference type is mapped to the underlying type:
```kotlin
inline class Name(val s: String)

fun foo(n: Name?) { ... } // `Name?` here is mapped to String
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be String?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, here we know that null can be associated only with the type Name?, because underlying type of the inline class Name is a non-null reference type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is similar to what we have for IntArray/LongArray/...: IntArray? is just an array of primitive types

```

Also, if inline class type is used in generic position, then its boxed type will be used:
```
// Kotlin: sample.kt

inline class Name(val s: String)

fun generic(names: List<Name>) {} // generic signature will have `List<Name>` as for parameters type
fun simple(name: Name) {}

// Java
class Test {
void test() {
String name = SampleKt.simple();
List<Name> ls = Samplekt.generic(); // from Java POV it's List<Name>, not List<String>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Kotlin code Name is a parameter type.
In Java code Name is a return type.
Name is a parameter or a return type?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, indeed, Name and List<Name> should be used in return type position in Kotlin, thanks!

}
}
```


#### Generic inline class mapping

Consider the following sample:
```kotlin
inline class Generic<T>(val x: T)

fun foo(g: Generic<String>) {}
```

Now, type `Generic<String>` can be mapped either to `java.lang.String` or to `java.lang.Object`.
To make the whole rule more consistent, currently we propose to map `Generic<SomeType>` always to `java.lang.Object`,
i.e. we'll map upper bound of type parameter.

* Sidenote: maybe it's worth to consider inline classes with reified generics:
```kotlin
inline class Reified<reified T>(val x: T)

fun foo(a: Reified<Int>, b: Reified<String>) // a has type `Int`, b has type `String`
```

Generic inline classes with underlying value not of type that defined by type parameter are mapped as usual generics:
```kotlin
inline class AsList<T>(val ls: List<T>)

fun foo(param: AsList<String>) {}
```

In JVM signature `param` will have type `java.util.List`,
but in generic signature it will be `java.util.List<java.lang.String>`

## Methods from `kotlin.Any`

Inline classes are indirectly inherited from `Any`, i.e. they can be assigned to a value of type `Any`, but only through boxing.

Methods from `Any` (`toString`, `hashCode`, `equals`) can be useful for a user-defined inline classes and therefore should be customizable.
Methods `toString` and `hashCode` can be overridden as usual methods from `Any`. For method `equals` we're going to introduce new operator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, how will overridding toString(), hashCode() and equals() work?

inline class UserId(private val id: String) {
    override fun toString() = "UserId($id)"
}

fun test(userId: UserId) {
    System.out.println(userId) // ?
}

It'll require a wrapper class which is what inline class is trying to remove

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artem-zinnatullin It'll be a static method that'll be called at invocation places, so still no additional allocation. If you put the inline class instance in a more generic type, it'll automatically be boxed (like Int and other primitives are), so it'll call the virtual version of toString() and alike methods.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would System.out.println() or any other runtime-linked use-site know about any of these methods?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@artem-zinnatullin userId will be boxed into an instance of UserId wrapper class, then this object instance is being passed to println

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha, I guess it falls into nullable/platform-type case.

Still don't like this whole wrapping story, gotta think about it more.

that represents "typed" `equals` to avoid boxing for inline classes:
```kotlin
inline class Foo(val s: String) {
operator fun equals(other: Foo): Boolean { ... }
}
```

Compiler will generate original `equals` method that is delegated to the typed version.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also makes sense to generate component1 function.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I consider harmful expose the private value for the UInt example, or the component1 must have the same visibility of the value attribute

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please elaborate? Do you have a use case? Maybe it's a point to allow inline data classes

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, you're right, auto component1 is useless.
Vice versa, it may be used by hand, for example:

inline class IntPair(private val value: Long) {
    operator fun component1() = value ushr 32
    operator fun component2() = value and 0xFFFFFFFF
}


By default, compiler will automatically generate `equals`, `hashCode` and `toString` same as for data classes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, are data inline classes allowed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, they can be allowed... I'm not sure it makes sense, I'll add a note

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Related - is there a reason sealed classes can't support inlining as long as every subclass is also inline?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@hzsweers Since there's no instance, how would you do is checks at runtime for use in a when or boolean expression?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would assume that they would work the same way is checks would work in normal use (I didn't see that listed as a listed caveat and assumed that meant it was supported, though admittedly I don't see how)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expect is check to not be supported on inline classes since they are identity-less, as said here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LouisCAD Could you please elaborate? Type check should work the same as it works for primitives.
<expr> is InlineClassType
There are two options:

  • type of expr is statically known to be an inline class type, then type check is trivial
  • type of expr is statically unknown, so if it's actually of an inline class type then it should be boxed, therefore we do type check for boxed type

Copy link
Contributor

@LouisCAD LouisCAD Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zarechenskiy Does that mean that anytime you put an inline class value to a reference that is higher in the class hierarchy (like Any), it is boxed, therefore, no longer inline for this usage?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@LouisCAD Yes, same as for primitives

Copy link
Contributor Author

@zarechenskiy zarechenskiy Apr 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can discuss this problem in the issue. We had some design about what you are thinking :)
But it has too much restrictions, you can check opaque types in Scala, for example

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an interesting conflict with this section about equals/hashCode and implementing interfaces.

Let me quote:

Compiler will generate original equals method that is delegated to the typed version.

and

Inline class can implement only interfaces

Which means that comparing two different inline classes that use same underlying type and implement same interface can result in wrong behavior because of comparing underlying values which can be indeed equal.

Example:

interface Bytable {
    fun toBytes(): ByteArray
}

inline class UserId(private val id: String): Bytable {
    override fun toBytes(): ByteArray = "userId:$id".toByteArray()
}

inline class OrderId(private val id: String) : Bytable {
    override fun toBytes(): ByteArray = "orderId:$id".toByteArray()
}

fun f(bytable1: Bytable, bytable2: Bytable) {
    assert(bytable1 == bytable2)
}

fun test() {
    f(bytable1 = UserId("123"), bytable2 = OrderId("123"))
}

fun main(args: Array<String>) {
    test()
}

If you change inline classes to data classes, test will fail.
But I think it'll pass with inline classes which is interesting.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm intrigued as to how exactly unsigned types will work under this proposal.

I can see how one could do something like this:

val u = UInt(42)

but how then would you access the value property if it's private or is the idea that u itself would be treated as the value?

Also, if you wanted to create a UInt that was greater than Int.MAX_VALUE what value would you present to the primary constructor? Presumably one would not be expected to work out the appropriate negative Int value corresponding to this so would Int literals be extended to permit higher values up to UInt.MAX_VALUE?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think number literals for unsigned values are part of this proposal, but I guess if they are implemented they need to allow for higher values than there signed parts. How else are you gonna get values for ULong. For UInt you could use a function converting a long. That being said, maybe you are doing something wrong if you have to hardcode numbers that big.


## Arrays of inline class values

Consider the following inline class:
```kotlin
inline class Foo(val x: Int)
```

To represent array of unboxed values of `Foo` we propose to use new inline class `FooArray`:
```kotlin
inline class FooArray(private val storage: IntArray): Collection<Foo> {
operator fun get(index: Int): UInt = Foo(storage[index])
...
}
```
While `Array<Foo>` will represent array of **boxed** values:
```
// jvm signature: test([I[LFoo;)V
fun test(a: FooArray, b: Array<Foo>) {}
```

This is similar how we work with arrays of primitive types such as `IntArray`/`ByteArray` and allows to explicitly differ array of
unboxed values from array of boxed values.

This decision doesn't allow to declare `vararg` parameter that will represent array of unboxed inline class values, because we can't
associate vararg of inline class type with the corresponding array type. For example, without additional information it's impossible to match
`vararg v: Foo` with `FooArray`. Therefore, we are going to prohibit `vararg` parameters for now.

#### Other possible options:

- Treat `Array<Foo>` as array of unboxed values by default

Pros:
- There is no need to define separate class to introduce array of inline class type
- It's possible to allow `vararg` parameters

Cons:
- `Array<Foo>` can implicitly represent array of unboxed and array of boxed values:
```java
// Java
class JClass {
public static void bar(Foo[] f) {}
}
```
From Kotlin point of view, function `bar` can take only `Array<Foo>`
- Not clear semantics for generic arrays:
```kotlin
fun <T> genericArray(a: Array<T>) {}

fun test(foos: Array<Foo>) {
genericArray(foos) // conversion for each element?
}
```


- Treat `Array<Foo>` as array of boxed values and introduce specialized `VArray` class with the following rules:
- `VArray<Foo>` represents array of unboxed values
- `VArray<Foo?>` or `VArray<T>` for type paramter `T` is an error
- (optionally) `VArray<Int>` represents array of primitives

Pros:
- There is no need to define separate class to introduce array of inline class type
- It's possible to allow `vararg` parameters
- Explicit representation for arrays of boxed/unboxed values

Cons:
- Complicated implementation and overall design


## Expect/Actual inline classes

To declare expect inline class one can use `expect` modifier:
```kotlin
expect inline class Foo(val prop: String)
```

Note that we allow to declare property with backing field (`prop` here) for expect inline class, which is different for usual classes.
Also, since each inline class must have exactly one value parameter we can relax rules for actual inline classes:
```kotlin
// common module
expect inline class Foo(val prop: String)

// platform-specific module
actual inline class Foo(val prop: String)
```
For actual inline classes we don't require to write `actual` modifier on primary constructor and value parameter.

Currently, expect inline class requires actual inline and vice versa.