I convinced ChatGPT from OpenAI to write me Kotlin code that uses a Ktor client to pull down the "<h...
y
I convinced ChatGPT from OpenAI to write me Kotlin code that uses a Ktor client to pull down the "https://kotlinlang.org/docs/whatsnew14.html" and asked it for an example of what the code would output (I have to be very wordy with it or else it gets angry and reminds me that it can't run code lol). Eventually, I made it "send a request" to "https://kotlinlang.org/docs/whatsnew19.html", but that produced the same output, just with Kotlin 1.9 as the version number. Finally, I convinced it that Kotlin 1.9 actually includes Union Types (๐Ÿ™ ) and made it go to the
<h2>
tag that talks about Union types, and print out it and its siblings, and this is what it produced: See ๐Ÿงต for some other interesting observations about it, including Kotlin-specific ones, if anyone is curious
It seems very easy to get ChatGPT to co-operate as long as you frame your questions as the output of some code. For instance, to get it to generate a random number, something it normally wouldn't do, I made it write a PyTorch script that takes some user input and tokenizes it. Then, via multiple messages, I made it send that input into a Dungeons and Dragons Large Language Model (fictional obviously) and asked it what the output would be if the input was "you're a lonely wizard that walks into a cave. You stumble upon a wild beast. Roll strength with advantage" which made it roll a D20 with advantage. I could then tell it to change that input to be more specific like "Roll a D100 for strength without advantage" which is just a random int between 1 and 100.
I think it's probably just my bias as a programmer that I've had the most luck getting it to answer my prompts by framing it as code. It also can't really handle multiple code changes in the same prompt, so you need to be slow and easy with it. Also, if it gives you incomplete output for your code, telling it to "continue the output" has had a 100% success rate for me
You can also even correct it by being like "this output doesn't seem correct. Can you check that X and Y are right" e.g. if it missed a couple lines in your output you can be like "can you check that the output included the appended lines" and it'll likely fix it very quickly. It's very good at not making the same mistake twice, as long as it understood what the mistake actually was and responded well to it the first time around
Now onto Kotlin specific stuff. It seems to have a much harder time generating Kotlin code than something like Python. Maybe I'm getting rate limited, or it's just my network speed or something, but Kotlin code seems to take much longer to generate than Python code. I think this might be maybe due to their just being less total volume of Kotlin code out there, especially for ktor vs pytorch.
It does seem to understand Kotlin quite well though. It imports all the required libraries when it needs them. Hell, it even told me what lines I need in my build.gradle so that I can add Ktor and kotlinx.html. It also makes very clean Kotlin code. It adds comments, formats it well, and even uses scoping functions. Obviously that's because that's how most Kotlin code out there looks, but still it's quite interesting. It makes some syntax errors from time to time, but it seems to usually be just 1 or 2 characters, and those usually get fixed just randomly after subsequent attempts.
This was the final code that it generated:
Copy code
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.http.HttpMethod
import kotlinx.html.*
import kotlinx.html.parser.parse

fun main() {
    // Create an instance of the HttpClient class
    val client = HttpClient()

    // Make a GET request to the specified URL
    val response = client.request<HttpResponse>(
        method = HttpMethod.Get,
        host = "<http://kotlinlang.org|kotlinlang.org>",
        path = "/docs/whatsnew19.html"
    )

    // Parse the response data as HTML
    val responseData = response.readText()
    val doc = parse(responseData)

    // Find the <h2> element that talks about Union Types
    val h2 = doc.body?.getElementsByTag("h2")?.find { h2 ->
        h2.text.contains("Union Types")
    }

    // Print the contents of the <h2> element and its siblings to the console
    h2?.let { h2 ->
        println("<h2>: " + h2.text)

        h2.nextSibling?.let { sibling ->
            do {
                if (sibling is Element) {
                    println("<${sibling.tagName}>: " + sibling.text)
                }
                sibling.nextSibling?.let { nextSibling ->
                    sibling = nextSibling
                }
            } while (sibling.nextSibling != null)
        }
    }

    // Close the HttpClient instance when you are finished making requests
    client.close()
}
I made it run the request for each kotlin version from 1.0 to 1.9. It left the HttpClient outside the loop, which is good practice. When I asked it why it did so, it said:
The client is created outside of the
for
loop because it is more efficient to create the client once and reuse it for each iteration of the loop. Creating a new instance of the
HttpClient
class for each iteration of the loop would be wasteful and would slow down the code.
Additionally, the
HttpClient
class is designed to be used as a singleton, which means that it should only be created once and then shared among different parts of the code. Creating multiple instances of the
HttpClient
class can cause issues with memory management and can lead to unexpected behavior.
In general, it is best practice to create the
HttpClient
instance once and then reuse it for each request that you need to make. This will help to improve the performance and efficiency of your code, and will make it easier to manage and maintain.
Using that loop technique actually worked pretty well to generate slightly different descriptions for Union Types (I had to say "continue the output" quite frequently though). Here's the highlights:
Kotlin 1.9 introduces support for Union Types, a new type system feature that allows you to define a type that can either be one of several other types. This is useful in situations where you want to express that a value can have multiple different types, but you don't want to use type hierarchies or interfaces.
Union Types are similar to TypeScript's type unions, and they are represented in Kotlin using the
|
operator. For example, you could define a type that can either be a
String
or an
Int
like this:
val foo: String | Int
You can then assign a value of type
String
or
Int
to the
foo
variable, and the compiler will check that the value is one of the allowed types.
Union Types are covariant, which means that if you have a Union Type that is a subtype of another Union Type, then the subtype can be used wherever the supertype is expected. For example, if you define a type
Foo
that is a subtype of
Bar
, then you can use
Foo
wherever
Bar
is expected.
Union Types are also nullable, which means that if you define a type that is nullable, then it can either be the specified type or
null
. For example, if you define a type
Foo?
that is a Union Type of
Foo
and
null
, then you can use
Foo?
wherever
Foo
is expected, and you can also assign
null
to a variable of type
Foo?
.
Union Types are a powerful and flexible feature that can help you to express complex type relationships in your code. They are especially useful in situations where you want to express that a value can have multiple different types, but you don't want to use type hierarchies or interfaces.
In Kotlin 1.10, Union Types are supported in the type system and the language. This means that you can define Union Types using the
|
operator, and the compiler will check that the values you use are one of the allowed types. You can also use Union Types in type declarations, function signatures, and other places in your code where you need to specify a type.
For example, you could define a function that accepts a value of type
String
or
Int
, and returns a value of type
String
or
Boolean
, like this:
fun foo(x: String | Int): String | Boolean {
if (x is String) {
return x.length > 10
} else {
return x.toString()
}
}
In this example, the
foo
function accepts a value of type
String
or
Int
, and returns a value of type
String
or
Boolean
. The function checks the type of the
x
parameter, and then returns a value of the appropriate type.
You can also use Union Types in type declarations, like this:
typealias Foo = String | Int
In this example, the
Foo
type alias is defined to be a Union Type of
String
and
Int
. This means that you can use the
Foo
type wherever you would use
String
or
Int
, and the compiler will check that the values you use are one of the allowed types.
Union Types are a powerful and flexible feature that can help you to express complex type relationships in your code. They are especially useful in situations where you want to express that a value can have multiple different types, but you don't want to use type hierarchies or interfaces.
v
Amazing! When will 1.10 be available for use/testing? Having done some extensive Typescript programming in the past few months, I really miss its union type. It is probably the best feature of Typescript.
1461 Views