Wondering what you think about <https://youtrack.j...
# javascript
e
Wondering what you think about https://youtrack.jetbrains.com/issue/KT-70081 My opinion is externals, being they map to a more flexible language, should not strictly adhere to the Kotlin type system checks.
c
That doesn't sound like a good idea to me. The point of externals is to represent, in Kotlin, what the external language looks like. This is important so the Kotlin compiler can understand what goes on outside. If we allow having externals that don't respect Kotlin invariants, this means that the Kotlin code built on top of it will be unsafe. That sounds like a big no no to me.
1
e
Well, externals per-se are unsafe imo. At that level you're dealing with JS code directly. You're not shielded by the Kotlin compilation process.
If I can't properly represent JS libraries, that's a bigger issue.
c
But if the types are represented incorrectly, your function will pass it down to further Kotlin code. So it pollutes the entire codebase with incorrect types. This is the main reason I'm not using TypeScript, I don't want Kotlin to end up the same.
e
The difference is in Kotlin working with external JS libraries is made much more obvious. It's pretty much comparable to the Rust unsafe layer imo. But then, how would you deal with the types I posted on the issue? Do you sacrifice proper typings?
c
What's wrong with
Array<out IZoweTreeNode>
? That's the Kotlin equivalent of what you're doing, no?
e
You can't do that on
Copy code
var children: Array<IZoweTreeNode>?
Or on
Copy code
var schema: IProfileSchema
override var schema: ICommandProfileSchema
c
The difference is in Kotlin working with external JS libraries is made much more obvious.
No, it's not. Once a type has been converted into the Kotlin type system, you can do whatever you want with it. You can pass it to other functions, etc, and these have no way to know that this is an external type that doesn't really correspond to what the Kotlin type says. The only way for pure Kotlin code to be safe, is if all boundaries only let through data that has at least all capabilities of the equivalent Kotlin type. If it's unclear, the boundary should be safe and be restrictive.
e
But again, how do you solve the problem I highlighted? It's easy to say "must not do that", but when dealing with the actual issue there should be a proper solution, and if there isn't, more flexibility has to be provided.
c
> You can't do that on >
Copy code
var children: Array<IZoweTreeNode>?
What do you mean? Playground
e
You have to apply it to an hierarchy
Copy code
external interface IZoweTreeNode {
  var children: Array<IZoweTreeNode>?
}

external interface IZoweJobTreeNode : IZoweTreeNode {
  override var children: Array<IZoweJobTreeNode>?
}
With `var`s you can't increase the specificity of the type on the override.
c
Ah, didn't see they were
var
s. There is too much immutability here, I don't see how this example could ever be sound.
Copy code
const job = new IZoweJobTreeNode();
job.children = [new IZoweTreeNode()];
Is this code allowed? • If it's allowed, it's unsound (the type signature of
IZoweJobTreeNode.children
is not respected) • If it's not allowed, the language doesn't respect the LSP, so this isn't an inheritance relationship at all
e
I don't see how this example could ever be sound.
Even if it isn't, that's the JS library responsibility, I should be able to deal with the library anyway
FWIW, the library could even override
children
and say it's a
string
, the externals should reflect that.
c
Even if it isn't, that's the JS library responsibility, I should be able to deal with the library anyway
That's what
dynamic
is for.
e
Yes, but then you're losing type information, which you'd have on TS.
c
If it's not safe to represent as a Kotlin type, then it shouldn't be possible to represent it as a Kotlin type, otherwise it will spread in the entire codebase.
Yes, but then you're losing type information, which you'd have on TS.
That type information is incoherent and unsound. Nothing of value is lost.
e
That type information is incoherent and unsound. Nothing of value is lost.
I mean, you're losing library usability, which basically is everything you need when developing software
It should be a developer decision, not a toolchain decision, to select what's safe or unsafe to use.
Imagine Rust without unsafe.
c
The Rust
unsafe
equivalent in Kotlin is
dynamic
, not
external
.
The developer decision is using an explicit
as
on a
dynamic
value. The compiler is right to forbid dangerous code when the developer doesn't explicitly recognize the dangers.
e
> forbid dangerous code when the developer doesn't explicitly recognize the dangers. Guess we should ban JS? I don't understand why we'd want to limit interop capabilities on K/JS tbh. It's obvious correctness isn't achievable when interoping with JS.
c
Imagine Rust without unsafe.
The concept that exists in Kotlin to represent the danger of JS is
dynamic
. Everything else should be safe. In this example, you have to be unsound, because the library you want to interop with is unsound. So, use
dynamic
.
e
So, use
dynamic
And then other n developers need to remember what the actual type is? Doesn't make much sense
c
You create a wrapper that makes it safe by adding checks and casts, and only expose that wrapper to Kotlin users.
Same as you'd do in Rust with a C binding that uses
unsafe
but doesn't expose it.
e
So I need to create another abstraction just for this. Again, lost time + productivity.
Imagine if you have to do that with auto-generated externals.
It really doesn't make sense. I'm working on a K/JS project targeting Node.js, and if I want to interop with a * JS * library I need an additional layer? Absurd imo
c
You're the one comparing everything to Rust, this is exactly the same in Rust…
e
We don't necessarily have to increase complexity when we know we're dealing with a dynamic language like JS, which requires, indeed, to be more flexible. What are the alternatives? 1.
dynamic
on the override: loses type information 2. extension property which calls `js(...)`: can't use the same property name. + it's a mess when externals are auto-generated as you need to do that manually. 3. wrap the object into another object: impossible when the library is complex + it's a very bad idea anyway as the library itself is the abstraction
If you want a safe/sound type system/hierarchy, then don't interop with JS libraries. Sounds fair enough.
c
We disagree on a fundamental level on whether the type information in this example is useful. Based on the fact that it is very incoherent with itself, I believe that it is not useful. Your argument is entirely based on your belief that it is useful and you'd like to have the same declaration in Kotlin. This means making Kotlin as unsafe as the target language. If I wanted to be as unsafe as TypeScript, I would write TypeScript. I expect my Kotlin code to have the same safety guarantees on all platforms, and the Kotlin language/library authors to insert runtime checks where it would otherwise be unsafe. Instead, Kotlin provides an opt-in way to disable type safety, specifically for these usages, which is
dynamic
.
e
This means making Kotlin as unsafe as the target language
But obviously you're not making the language unsafe, you're making externals more flexible for K/JS. Type information is crucial for productivity. Otherwise how do a team of developers do anything useful on a dynamic property?
It's like typing
any
in TS, when you actually know what the underlying type is. You'd ask yourself why you're doing that.
c
But obviously you're not making the language unsafe, you're making externals more flexible for K/JS.
You are making the language unsafe. With your proposal, there is now a variable with the Kotlin type
Array<IZoweJobTreeNode>
that can sometimes contain an
Array<IZoweTreeNode>
, which is an incompatible type. Because there are no runtime checks, this (false) type information can propagate to anywhere in the entire project.
And, indeed,
dynamic
is exactly as unsafe. This is the exact same thing as in Rust: values of a Rust type cannot be unsafe. An
unsafe
function must return a valid Rust type.
Now, you could create an extension function that casts your dynamic value to whatever type. And that will propagate throughout the entire codebase. But that was your explicit decision, it's not the compiler agreeing with you, that's what's important.
And yes, that is a major difference between TypeScript and Kotlin. In TypeScript, the compiler trust whatever you say. In Kotlin and Rust, the compiler checks that what you're saying makes sense, and if it doesn't, you have ways to explicitly bypass that (
dynamic
on JS, platform types on the JVM,
unsafe
in Rust).
e
Because there are no runtime checks, this (false) type information can propagate to anywhere in the entire project
We can say this about any JS external. You can type your property to
String
and have it be a number on the underlying code. So what is safe at this point?
c
It's at least coherent. As in, there's a chance that it's correct. In your example, it's 100% for sure wrong, because the type information isn't coherent with itself.
e
it's 100% for sure wrong
But who says it's not handled correctly on the library side? Why would it be wrong?
c
The library cannot handle it correctly. As it is written, the types allow a user to make legal operations that result in an incompatible runtime type than the declared types. This type declaration is not sound. See my example earlier in the thread.
As you said earlier, if it was read-only, then it could be safe.
It's not a question of what the library itself does; it's a question of what the user of the library is allowed to do. As it stands, the user of the library is allowed by the typesystem to create situations that are incompatible with the typesystem. This is an incoherence, the typesystem allows the user to disregard the typesystem. That's the problem.