Named Tuples

There is a proposal by Martin Odersky to bring named tuples to Scala Pre SIP: Named tuples - Language Design - Scala Contributors
image

Would a similar proposal make sense in Kotlin?

Motivation:
Named tuples relate to data classes, in a similar way lambdas relate to classes with a single method, in other words, allows to interface Classes by its shape/structure instead of its Class Name

State of Art

5 Likes

Personally I think that such structured anonymous data classes would be much more useful compared to current standard Pair or Triple types. Actually those types would become obsolete if an universal tuple syntax was introduced. In some cases without looking into documention you don’t even know what a data on left and right side even mean when using Pair as an result type.

In addition to that named destructuring would also be a nice bonus. Right now we only have a position-based one, but when combined with this new tuple syntax it would become a necessity.

3 Likes

To be honest, I don’t entirely understand the case in Scala (but I didn’t read the whole topic). For me the main benefit of tuples is that we get a strongly-typed anonymous data structure. With this type Person = ... we lose the main point, it is very similar to data/case classes.

However, I see benefits of something like:

fun divmod(dividend: Int, divisor: Int): (quotient: Int, remainder: Int) { ... }

More importantly, I think that would fit the language nicely. Kotlin favors explicit code over a short code. I guess this is the reason why it doesn’t really provide tuples (Pair and Triple can be hardly called a full support for tuples). But named tuples as above are explicit and I think it would be nice if we could skip the class declaration in such cases.

5 Likes

But you could easily reach that with a data class, e.g.

  data class Person(val name :String, val age :Int)

  val bob = Person(age= 33, name= "Bob")

The only addition would be the key words val.

One could argue that val can be made optional in the default constructor of a data class, because all arguments must become instance variables anyway.

1 Like

A named tuple is structural whereas data class is nominal that’s why you example uses a named constructor instead of just building the object fields.
The underlying idea would be to stop thinking in terms of classes/constructors and try thinking in shapes, same like typescript or Ocaml / F#

Suppose you have the following

data class Person(val name :String, val age :Int)
data class Animal(val name :String, val age :Int)

val bob = Person("Bob",33)
val max = Person("Max",3)

// Accepts only Animals even though bob have the same internal "data"
fun test(instance: Animal):: Unit {}

test(bob) // fails
test(max) // works
typealias Person =  (name :String, age :Int)
typealias Animal =  (name :String, age :Int)

val bob: Person = (name: "Bob", age: 33)
val max: Animal = (name: "Max", age: 6)

// Here you can Pass Either Person or Animal as long a "data" matches the structure you want
fun test(instance: (name :String, age :Int)):: Unit {}

// both calls are valid
test(bob)
test(max) 

But I don’t see any benefit in your usage: A Person is NOT an Animal (sorry Darwin ;-). If you were just looking for a name and an age, why didn’t you introduce something like:

interface NamedBeing {
  val name :String
  val age :Int
}

data class Person(
   override val name :String,
   override val age :Int
) :NamedBeing {}

fun test(being :NamedBeing) {...}

val bob = Person("Bob", 33)
test(bob) // this is Ok

data class Animal(
   override val name :String,
   override val age :Int
) :NamedBeing {}

val fifi = Animal("Fifi", 5)
test(fifi) // this is Ok, too

If you really hate types, you can play with GoLang’s interfaces, but don’t be surprised when an integer acts as a web server (the integer is the port number, the server name defaults to “localhost”). I consider this abuse of types: Intro++ to Go Interfaces · npf.io (from 2014, not sure though whether Golang is still in that state).

2 Likes

Actually is the opposite, names tuples are statically structurally typed, fully type safe, so there are plenty of reasons to love them, the point is that people that come from functional languages do not think in OOP or classes, which is more limiting as verbose, why would i have a 3 types for animal, person or alien, when i can just have one with the required data, does not make any sense, if your point is that animal data would be different from a person (age and name) that’s kind of foolish if you think on mathematical sets, they are the same, besides being fully structurally typed like in typescript/ocaml in contrast of js/python

I think that we first have to find out whether you want a type alias or a “new” type definition: 2 new type definitions (e.g. data class Person(...)) always lead to different types, even if they store the same data. Think of the type like having an additional tag. Type aliases (e.g. typealias Human = Pair<String, Int>) of the same underlying type definition (= Pair<String, Int>) are interchangeable, as well as the alias with the original type.

If you wish to introduce names for the components, then you should either go with a “new” type, because the component names are type specific, or introduce an (extension) function for the underlying type, e.g.

  fun Pair<String, Int>.name() = first

The funny thing is that the above function definition has the same effect as

  fun Human.name() = first

for the type alias, because every Pair<String, Int> is trivially a Human (what you suggested) and then this can provide its name.

Btw: What is “statically structurally typed”? I have heard the notion of product type that you probably refer to as “structurally”. Many functional programming languages are statically typed, as well as Kotlin (and Java) are.

Also it seems that Kotlin is a bit more verbose than TypeScript, i.e. we don’t cram

  function test(instance :{name: string, age :int})

all into one line. Instead, you would either go for a standard tuple (instance :Pair<String, Int>) or introduce a type/typealias beforehand. The most un-kotlinish is the following construction in TypeScript:

  function test(instance :{name :string, age :int}) {...}

  test({name: "Bob", age: 32})

Why? Because it has very much a scripting feeling. Even if you first define const hans = { name: "Hans", age: 32 }, the IDE will have a hard time to figure out that you could/should call the previous function test(...) with that object/tuple. (Yes, it is possible for IntelliJ Ultimate to figure out that in this case all types are correct.) It simplifies the understanding for humans and the job of the IDE a lot if you first introduce a specific type, e.g. Person. (“Simplifying the job for the IDE” is also implying that the IDE can suggest functions that match if the types are more explicit.)

Rem.: The NamedBeing in the example from last time is not necessarily an Alien, but Person as well as Animal are valid subtypes (because their type definitions both contain : NamedBeing). Note that in this case all 3 are different types, i.e. a Person is not an Animal (even though you could construct one from its data), v.v. Person is a proper subtype of NamedBeing even though in the example they have the same data, but that could easily change.

When dealing with a function definition, it makes sense to think first whether you want an actual Person, (an Animal) or just a NamedBeing. This becomes apparent when you wish to claim that you require a Human but then want to hand in an Animal.

To give you an example from the real world: I want to fun marry(spouse :Person) rather than just any spouse :NamedBeing or spouse :Pair<String, Int>. Latest when it turns out that name= "Fifi", I will become suspicious. But I also want the compiler to sort out these cases beforehand (i.e. statically), not have a crash on the wedding day.

I think you might be misunderstanding OOP a bit? If your problem space considers an animal, person, and alien all the same because they have the same properties, then don’t have an animal, alien, and person class, just have a Blah class that has all the fields you want.

Or, if animal, person, and alien are different, but at this point in the code they are the same since they all have age and name, make an interface that defines that, and have alien, person, and animal all implement the interface.

Maybe I’m misunderstanding OOP, or maybe I’m being overly simplistic or pragmatic, but I would say, create the types you need. If you need to distinguish between alien, person, and animal, then you probably shouldn’t be returning an anonymous tuple of name and age. But if you just care about the structure of the data, then make one class with the structure you want and just use it wherever you need that data structure. It’s your code, you’re free to do what you want.

3 Likes

It’s a critic to OOP, not a misunderstanding, there was a time when everything outside java was wrong, then had to learn other stuff, mind changes and things seem different, can understand that it’s hard to thing out of the box if you mental model it’s pure OOP, same way nobody could see beyond the OOP approach of swing event handlers with clases inheriting ActionListener before java 8 introduced lambdas, now it’s the most obvious thing that lambdas are a “better” approach for event listeners, if you just want a function then pass a function, why would you care about inheritance and interfaces like ActionListener when handling swing events, when you can just pass a function as handler, if you use lambdas then you are avoiding OOP using functional syntax sugar, which btw was copied from other languages which have named tuples like structures
Basically if you like lambdas more than making classes with a single method, the same argument goes for named tuples
As named tuples relate to data, same way lambdas relate to functions