I've been catching up on Advent of Code over my holiday break. I've been using it as a playground to experiment with languages outside of my comfort zone; mainly F#, C#, and Kotlin. The last problem I solved was to find valid identifiers (longs) within a range. I solved this in Kotlin using a class called Range that would tell me if other identifiers were within it. This worked, but it left me asking what this data structure would look like in each language.
I actually found the Kotlin implementation to be more verbose than I expected. While Kotlin supports concise method bodies which greatly help with density, the rest of the declarations require a lot more than the other implementations (except for F#). For platform compatibility you need to annotate Kotlin data classes with @JvmRecord. This tells the Kotlin compiler to generate a Java record instead of a standard POJO with getters and setters.
One surprising thing that I didn't expect was just how much more verbose argument declarations were than both C# and Java. Since Kotlin doesn't have a dedicated "immutable data holder" keyword you need to annotate the properties with val to ensure immutability; in all 3 of the other implementations the respective compilers will ensure immutability out of the box. Kotlin also switches argument name and type declarations to be the reverse of C-style languages: name: Type. While it is only a single character, the extra : just isn't needed.
Kotlin's saving grace is concise method bodies and inferring the return type of the methods. It allows you to drop the declaration of the return type (albeit for less clarity), but you still have to declare the functions with fun so you don't really gain anything in terms of reducing verbosity.
@JvmRecord
data class Range(val start: Long, val end: Long) {
fun contains(l: Long) = start <= l && l <= end
fun isWithin(other: Range) = this.contains(other.start) && this.contains(other.end)
fun isOverlapping(other: Range) = this.contains(other.start)
fun count() = end - (start - 1)
}I found this implementation to be the least verbose and most clear out of them all. C# has a dedicated keyword for declaring immutable data types via the record keyword allowing you to not have to specify the immutability of each property. Using C-style declarations also allows them to not have the extra colon noise and C#'s implementation of concise method bodies allows you to clearly see all of the functions and their return types. It does have the downside of having to declare the functions as public because default visibility for methods in C# is private.
record Range(long Start, long End)
{
public bool Contains(long l) => Start <= l && l <= End;
public bool IsWithin(Range other) => this.Contains(other.Start) && this.Contains(other.End);
public bool IsOverlapping(Range other) => this.Contains(other.Start);
public long Count() => End - (Start - 1);
}This was the most verbose and the least clear implementation. F# tries to make it hard to use objects because it is a functional language. It overloads the syntax for declaring methods with member and always having to refer to member properties via this. Note: this implementation uses first and last instead of start and end because end is a reserved keyword in F#.
type Range = {
first: Int64;
last: Int64;
} with
member this.contains(l: Int64) = this.first <= l && l <= this.last
member this.isWithin(other: Range) = this.contains(other.first) && this.contains(other.last)
member this.isOverlapping(other: Range) = this.contains(other.first)
member this.count() = this.last - (this.first - 1)Java's implementation is clear, but also verbose due to the lack of concise method bodies. It has a dedicated keyword record for the compiler to enforce immutability of the data, however, you're stuck having to implement full method bodies. One gain over C# it has is not having to specify public on the methods because Java's default visibility is package-private. You can easily see the function's return type, name, and arguments.
record Range(long start, long end) {
boolean contains(long l) {
return start <= l && l =< end;
}
boolean isWithin(Range other) {
return this.contains(other.start)
&& this.contains(other.end);
}
boolean isOverlapping(Range other) {
return this.contains(other.start);
}
long count() {
return end - (start - 1);
}
}In 2018 the Java designers proposed a JEP for concise method bodies. This would allow Java to greatly reduce it's verbosity and come out as a clear winner, however, 8 years later it's yet to move out of draft status.
record Range(long start, long end) {
boolean contains(long l) -> start <= l && l <= end;
boolean isWithin(Range other) -> this.contains(other.start) && this.contains(other.end);
boolean isOverlapping(Range other) -> this.contains(other.start);
long count() -> end - (start - 1);
}