5

I'm studying Kotlin and as part of learning it I want to design a class that would represent a rational number, requirements:

  • Class should contain two immutable integer fields: numerator and denominator.
  • Class should contain valid equals, hashCode and toString implementations.
  • When class is initialized, numerator and denominator should be deleted by their GCD (it means that Ratio(1, 2) == Ratio(2, 4 /* or 4, 8 */) or Ratio(2, 4 /* or 4, 8 */).numerator == 1, .denominator == 2 etc.)
  • This class should contain mul method that takes another Ratio and returns multiplication result of the current ratio and the given one.

I tried to use data classes what looked suitable for that task, but I was stuck with inability to define a custom constructor (both numerator and denominator need to be deleted to their GCD).

Possible solution:

class Ratio(num : Int, denom : Int) {
    val numerator = num / gcd(num, denom)
    val denominator = denom / gcd(num, denom) // GCD calculated twice!
}

What is the simplest way to define a class constructor so that GCD is calculated once?

UPDATE

OK, it looks like I found the possible solution:

data class Ratio(num : Int, denom : Int) {
  val numerator : Int
  val denominator : Int

  {
    val gcd = calcGcd(num, denom)
    numerator = num / gcd
    denominator = denom / gcd
  }
}

but it renders that data qualifier useless - after this change Ratio class no longer has auto generated equals/hashCode/toString.

Verified on the latest version of Kotlin - 0.9.66

Program that reproduces that behavior:

data class Ratio(num : Int, denom : Int) {
  val numerator : Int
  val denominator : Int

  {
    val gcd = BigInteger.valueOf(num.toLong()).gcd(BigInteger.valueOf(denom.toLong())).intValue();
    numerator = num / gcd;
    denominator = denom / gcd
  }
}

data class Ratio2(val num : Int, val denom : Int)

fun main(args: Array<String>) {
  println("r = " + Ratio(1, 6).toString())
  println("r2 = " + Ratio2(1, 6).toString())
}

output:

r = Ratio@4ac68d3e
r2 = Ratio2(num=1, denom=6)

that's clear that Ratio no longer has auto generated toString method

1
  • have you reported the useless data issue to JetBrains? Sounds like a bug Commented Nov 11, 2014 at 13:02

2 Answers 2

3

OK, I found an answer (thanks to Andrey who pointed to the necessity to have private ctor in the described use case):

data class Ratio private (val numerator : Int, val denominator : Int) {
  class object {
    fun create(numerator : Int, denominator : Int) : Ratio {
      val gcd = BigInteger.valueOf(numerator.toLong()).gcd(BigInteger.valueOf(denominator.toLong())).intValue();
      return Ratio(numerator / gcd, denominator / gcd)
    }
  }
}

for some reason 'data' qualifier will be rendered useless if initializer blocks are used in the class, so if you want to have custom construction logic and retain auto generated hashCode/equals/toString methods you'll need to use factory methods.

Sign up to request clarification or add additional context in comments.

3 Comments

Some improvements over this concept: kotlin-demo.jetbrains.com/…
Thanks! I missed the fact that you can use invoke if you want to have custom ctor logic.
Also, be sure to define copy, otherwise there will be a way of breaking your invariant
0

How about:

class Ratio(num : Int, denom : Int) {
 private val theGcd = gcd(num, denom)
 val numerator = num / theGcd
 val denominator = denom / theGcd
}

EDIT: fair point about the useless field. An alternative could be to use a lazy-evaluated property. See the docs here http://kotlinlang.org/docs/reference/delegated-properties.html

Here's an (untested) go at this..

import kotlin.properties.Delegates

class Ratio(num : Int, denom : Int) {
 private val theGcd: Int by Delegates.lazy {
    gcd(num, denom) 
 }

 val numerator = num / theGcd
 val denominator = denom / theGcd
}

5 Comments

No, that's bad - that permanently adds redundant value to the class instance - and while that might be ok for toy samples, but if class layout gets bigger it will consume a much more memory than instance actually needs to have.
If your only problem with this is the extra field, it's easy to get rid of: class Ratio(num : Int, denom : Int) { val numerator: Int val denominator: Int { val theGcd = gcd(num, denom) numerator = num / theGcd denominator = denom / theGcd } } (gist.github.com/abreslav/173c32a30f9f94e1cd9a) If what you want is auto-generated equals/hashCode etc, you'd need a private constructor and a factory method
Thanks Andrey. Please, see my edits above - I wanted to use 'data' qualifier and after eliminating temp variable in initializer block I found that 'data' qualifier rendered useless.
@Andrey - Looks like I answered before seeing your latest updates :) Thanks for so quickly answering my question. I'll try that. However it was a complete surprise to me to find that an effect of having data qualifier vanishes after adding initializer block :) Surely you should have a some sort of Kotlin Lang Spec (similarly to JLS for Java) where all those details about language core will be explained.
The lazy property still occupies a field, of course.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.