Understanding Kotlin Concepts Through a Tennis Scoring System
Here’s the complete implementation followed by an explanation of key Kotlin language features:
Entire Code Implementation
(hiker.kt)
package hiker
const val PLAYER_1 = 1
const val PLAYER_2 = 2
fun answer(points: List<Int>): String {
return when (points.size) {
0 -> "love"
1 -> "fifteen"
2 -> "thirty"
3 -> "forty"
else -> handleComplexCases(points)
}
}
private fun countPoints(points: List<Int>): List<Int> {
val player1Points = points.count { it == PLAYER_1 }
return listOf(player1Points, points.size - player1Points)
}
private fun handleComplexCases(points: List<Int>): String {
val (player1, player2) = countPoints(points)
return if (player1 == player2 && player1 >= 3) "deuce" else "TBD"
}
(HikerTest.kt)
package hiker
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
class BasicScoringSpec : StringSpec({
"Return 'love' when no points scored" {
answer(emptyList()) shouldBe "love"
}
// ... other basic scoring tests ...
})
class DeuceScenariosSpec : StringSpec({
"Identify deuce at 3-3 points" {
val points = List(3) { PLAYER_1 } + List(3) { PLAYER_2 }
answer(points) shouldBe "deuce"
}
// ... other deuce tests ...
})
Key Kotlin Concepts Explained
1. Compile-Time Constants
const val PLAYER_1 = 1
- What: Immutable values known at compile time
- Why: Safer than magic numbers, improves readability
- Usage: Score identification markers that never change
2. When Expression
fun answer(points: List<Int>): String {
return when (points.size) {
0 -> "love"
// ... other cases ...
}
}
- Type-Safe: Compiler checks for exhaustive coverage
- Expression Body: Directly returns a value
- Clean Alternative: Replaces complex if-else chains
3. Collection Operations
points.count { it == PLAYER_1 }
List(3) { PLAYER_1 }
- Higher-Order Functions:
count()
with predicate lambda - List Factory: Create lists with repeated elements
- Immutability: Default to read-only collections
4. Destructuring Declarations
val (player1, player2) = countPoints(points)
- Tuple Unpacking: Directly assign components to variables
- Works With: Lists, data classes, pairs
- Safety: Compiler verifies component count
5. Testing DSL
class BasicScoringSpec : StringSpec({
"Return 'love'..." {
answer(emptyList()) shouldBe "love"
}
})
- StringSpec Style: Tests read like natural language
- Infix Notation:
shouldBe
for fluent assertions - Grouping: Logical separation into multiple specs
6. Type Inference
val player1Points = points.count { ... } // Inferred as Int
listOf(player1Points, ...) // Inferred as List<Int>
- Compiler Smarts: Deduces types from context
- Reduced Ceremony: No explicit type declarations needed
- Safety: Maintains strict typing while being concise
7. Function Visibility
private fun countPoints(...)
- Encapsulation: Hide implementation details
- Scope Control: Only expose public API (
answer()
) - Module Safety: Prevent accidental external usage
8. Immutable Variables
val points = List(3) { ... } // Can't be reassigned
- Principle: Prefer
val
overvar
by default - Thread Safety: Avoid unexpected state changes
- Predictability: Values remain constant after initialization
Why These Concepts Matter
-
Null Safety (Implicit in design)
- No nullable types used in this implementation
- Empty list handled explicitly (
points.size == 0
)
-
Extension Functions (Behind the scenes)
count()
is a stdlib extension for collectionsList(n) { ... }
uses factory functions
-
Lambda Expressions
List(3) { PLAYER_1 } // Lambda initializer points.count { it == PLAYER_1 } // Predicate lambda
-
Standard Library Utilization
- Leverage built-in functions instead of manual loops
- Use type-safe operations for collection manipulation
This implementation demonstrates Kotlin’s philosophy of pragmatic, readable code while maintaining strong type safety and modern language features.