DEV Community

Discussion on: Daily Challenge #121 - Who has the most money?

Collapse
 
jbristow profile image
Jon Bristow • Edited

Ridiculously over-engineered Kotlin version:

data class Student(val name: String, val fives: Int, val tens: Int, val twenties: Int)

private val Student.total: Int
    get() = fives * 5 + tens * 10 + twenties * 20

sealed class StudentAccumulator {
    abstract val money: Int
}

object Empty : StudentAccumulator() {
    override val money = 0
}

data class ErrorFound(val message: String) : StudentAccumulator() {
    override val money = 0
}

data class MaxFound(val name: String, override val money: Int) : StudentAccumulator()
data class TieFound(override val money: Int) : StudentAccumulator()

fun generateStudentFolder(compareFn: (StudentAccumulator, Student) -> Int) = { acc: StudentAccumulator, student: Student ->
    when (acc) {
        is ErrorFound -> acc
        is Empty -> MaxFound(student.name, student.total)
        is MaxFound -> acc.consider(student, compareFn)
        is TieFound -> acc.consider(student, compareFn)
    }
}

fun MaxFound.consider(student: Student, compareFn: (StudentAccumulator, Student) -> Int) = when (compareFn(this, student)) {
    -1 -> MaxFound(student.name, student.total)
    0 -> TieFound(student.total)
    else -> this
}

fun TieFound.consider(student: Student, compareFn: (StudentAccumulator, Student) -> Int) = when (compareFn(this, student)) {
    -1 -> MaxFound(student.name, student.total)
    0 -> ErrorFound("More than one maximum")
    else -> this
}

fun Iterable<Student>.findMax() =
    fold(
        Empty as StudentAccumulator,
        generateStudentFolder { a, b -> a.money.compareTo(b.total) }
    )

fun Iterable<Student>.findMaxName(): String {
    return when (val result = findMax()) {
        is ErrorFound -> throw Error(result.message)
        is Empty -> throw Error("No max found")
        is MaxFound -> result.name
        is TieFound -> "all"
    }
}

fun main() {
    val studentsA = listOf(Student("a", 1, 1, 1))
    println(studentsA.findMaxName())
    val studentsB = listOf(
        Student("a", 1, 1, 1),
        Student("b", 1, 1, 1)
    )
    println(studentsB.findMaxName())
    val studentsC = listOf(
        Student("a", 1, 1, 1),
        Student("c", 2, 1, 1),
        Student("b", 1, 1, 1)
    )
    println(studentsC.findMaxName())
}

It's typesafe, and we could change the comparator out if we wanted to. A little kludgey in the fold section (I should probably involve Option to avoid people accidentally calling money on the Empty and Error types. 🤷‍♀️

While I love the sealed class idea in Kotlin, type erasure and lack of true pattern matching really hampers the readability. If I was able to rely on StudentAccumulator auto-casting to its internal types, then I could remove a when statement that just dispatches out to the same overloaded call with the proper type.

Collapse
 
jbristow profile image
Jon Bristow

Ok, I fixed it. It's not too much uglier with proper Option protection...

import arrow.core.Option
import arrow.core.getOrElse

data class Student(val name: String, val fives: Int, val tens: Int, val twenties: Int)

private val Student.total: Int
    get() = fives * 5 + tens * 10 + twenties * 20

sealed class StudentAccumulator {
    abstract val money: Option<Int>
}

object Empty : StudentAccumulator() {
    override val money = Option.empty<Int>()
}

data class ErrorFound(val message: String) : StudentAccumulator() {
    override val money = Option.empty<Int>()
}

data class MaxFound(val name: String, override val money: Option<Int>) : StudentAccumulator() {
    constructor(student: Student) : this(student.name, Option.just(student.total))
}

data class TieFound(override val money: Option<Int>) : StudentAccumulator() {
    constructor(student: Student) : this(Option.just(student.total))
}

fun generateStudentFolder(compareFn: (StudentAccumulator, Student) -> Option<Int>) =
    { acc: StudentAccumulator, student: Student ->
        when (acc) {
            is ErrorFound -> acc
            is Empty -> MaxFound(student)
            is MaxFound -> acc.consider(student, compareFn)
            is TieFound -> acc.consider(student, compareFn)
        }
    }

fun MaxFound.consider(student: Student, compareFn: (StudentAccumulator, Student) -> Option<Int>) =
    compareFn(this, student).map { result ->
        when (result) {
            -1 -> MaxFound(student)
            0 -> TieFound(student)
            else -> this
        }
    }.getOrElse { ErrorFound("Bad comparison.") }

fun TieFound.consider(student: Student, compareFn: (StudentAccumulator, Student) -> Option<Int>) =
    compareFn(this, student).map { result ->
        when (result) {
            -1 -> MaxFound(student)
            0 -> ErrorFound("More than one maximum")
            else -> this
        }
    }.getOrElse { ErrorFound("Bad comparison.") }

fun Iterable<Student>.findMax() =
    fold(
        Empty as StudentAccumulator,
        generateStudentFolder { a, b ->
            a.money.map { it.compareTo(b.total) }

        }
    )

fun Iterable<Student>.findMaxName(): String {
    return when (val result = findMax()) {
        is ErrorFound -> throw Error(result.message)
        is Empty -> throw Error("No max found")
        is MaxFound -> result.name
        is TieFound -> "all"
    }
}