0

I am new to Kotlin, coming from JS (but am anyway a very amateur, self-taught programmer). In the past, my approach has been to just get it done but I have been loving the conciseness of Kotlin and would like to avoid unnecessary replications throughout similar elements in my program. I am wondering if I might be able to get some guidance with respect to child classes of a parent class. Currently, I am working with two types :

class Ratio(var num: Int, var den: Int) {
    var monzo: MutableList<Int> = calculateMonzo(num, den)

    var sizeInCents: Double = calculateCents(num, den)
    var centDeviation: Pair<Double, String> = calculateCentDeviation(num, den, monzo)
    var notation: Triple<String, String, String> = calculateNotation(monzo)
    var frequency: Double = calculateFrequency(num, den)
}

and

class Monzo(var monzo: List<Int>) {
    var num: Int = calculateRatio(monzo).first
    var den: Int = calculateRatio(monzo).second

    var sizeInCents: Double = calculateCents(num, den)
    var centDeviation: Pair<Double, String> = calculateCentDeviation(num, den, monzo)
    var hejiString: Triple<String, String, String> = calculateNotation(monzo)
    var frequency: Double = calculateFrequency(num, den)
}

Basically, Ratio() or Monzo() both represent the same "thing" (a fraction or a prime factorisation of a fraction) and are merely dependent on the user's preferred input. In the end, I calculate the "missing" information, then from that point, the properties and calculations are the same. It makes sense that these would be in a parent class e.g. Input(), but I am unsure about how one best sets up the parent class, then accesses those parent properties when calling an instance of one of the child classes.

3
  • 2
    Can't you just have one class with two constructors? Eventually add an enum if you need to know if it's Ratio or Monzo Commented Feb 25, 2021 at 15:16
  • 1
    Does this really need to store, return, and manipulate both forms of data (num/den and int list) in parallel?  For me, the neat solution would be to standardise on one form, have just one class which stores only that form, and then provide a way to create it from the other form (via a secondary constructor, or a factory method in the companion object) and to create the other form from it (via a method, or a property with a custom getter). Commented Feb 25, 2021 at 17:25
  • @Pawel and gidds I could certainly imagine a single standardisation. For instance, the prime factorisation "monzo" (List<Int>) is the more useful form and anything that is a ratio (num/den) can easily be converted into a monzo with a simple function. Is there any way you could give a very basic example of working with a secondary constructor? I feel a but shaky on the details... Commented Feb 25, 2021 at 19:13

1 Answer 1

1

Example of a single class with two constructors:

class Ratio private constructor(
    val num: Int,
    val den: Int,
    val monzo: List<Int>
) {
    constructor(monzo: List<Int>): this(calculateRatio(monzo).first, calculateRatio(monzo).second, monzo)

    constructor(num: Int, den: Int): this(num, den, calculateMonzo(num, den))

    val sizeInCents: Double = calculateCents(num, den)
    val centDeviation: Pair<Double, String> = calculateCentDeviation(num, den, monzo)
    val notation: Triple<String, String, String> = calculateNotation(monzo)
    val frequency: Double = calculateFrequency(num, den)
}

Since the last four properties are dependent on the first three, they should be initialized by the primary constructor. But having all three properties in a public constructor would be nonsensical since they are interdependent on each other's values. So make the primary constructor private and allow the two secondary constructors to pass the appropriate values.

I can't think of a clean way to avoid calling calculateRatio() redundantly. I suppose you could replace that constructor with an invoke function in a companion object like this:

companion object {
    operator fun invoke(monzo: List<Int>) = with(calculateRatio(monzo)) { Ratio(first, second, monzo) }
}

Also, all of your properties should be val instead of var because they are dependent on the initial values of the ratio. If you want num, den, and monzo to be mutable, you should write custom setters for them that recalculate everything. The properties that are dependent on the ratio in that case can be var, but with private setters.

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

6 Comments

Thanks for this --> so depending on the data type when using Ratio(), either the monzo constructor will be used or the num/den constructor? I get errors that num, den, and monzo must be initialised (in the code after the constructors). Do I need some kind of init block or default values? Does the code inside the constructor get executed before the remaining code outside?
I thought that moving the val num, val den, and val monzo to before the constructors, then pretending this. to each reference within each constructor would fix the problem, but it doesn't seem to...
I've tried playing around but keeping the same syntax. When I copy your example, which logically should work for what I need, I have no errors in the constructors nor in the val definitions themselves val num: Int, val den: Int, and val monzo: List<Int>, but in the remaining functions each time num, den, or monzo is called I get the error that the variable must be initialised. I guess I don't need to refer to the primary constructor since there is none, so at a bit of a loss...
Sorry, what I missed here is execution order. Property initialization occurs before constructor blocks, so it can't calculate all those dependent properties when either constructor hasn't assigned anything to those first three properties yet. So you need a primary constructor after all. See the updated answer.
Great, works perfectly, thanks for all your help. I need to read more about val vs. var -- the difference seems somehow more subtle than the difference between const and var in JS
|

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.