Last week’s post was dedicated to OOP. Despite popular belief, the exercise was solved using neither accessors i.e. getters and setters, nor shared mutable state.
The solution’s implementation was based on traditional OOP constructs offered by the Kotlin language: classes, inheritance and overriding. Other languages may offer different ways to do OOP.
This is the 8th post in the Exercises in Programming Style focus series.Other posts include:
- Introducing Exercises in Programming Style
- Exercises in Programming Style, stacking things up
- Exercises in Programming Style, Kwisatz Haderach-style
- Exercises in Programming Style, recursion
- Exercises in Programming Style with higher-order functions
- Composing Exercises in Programming Style
- Exercises in Programming Style, back to Object-Oriented Programming
- Exercises in Programming Style: maps are objects too (this post)
- Exercises in Programming Style: Event-Driven Programming
- Exercises in Programming Style and the Event Bus
- Reflecting over Exercises in Programming Style
- Exercises in Aspect-Oriented Programming Style
- Exercises in Programming Style: FP & I/O
- Exercises in Relational Database Style
- Exercises in Programming Style: spreadsheets
- Exercises in Concurrent Programming Style
- Exercises in Programming Style: sharing data among threads
- Exercises in Programming Style with Hazelcast
- Exercises in MapReduce Style
- Conclusion of Exercises in Programming Style
Objects in JavaScript
In JavaScript, an object is just a map whose values are either properties or functions.
var foo = {}; (1)
foo.bar = "bar"; (2)
foo.baz = function() { (3)
return this.bar;
};
console.log(foo.bar);
console.log(foo.baz());
1 | Define a new object i.e. a map |
2 | Add a property |
3 | Add a function |
We could write the equivalent code in Kotlin as the following:
val foo = mutableMapOf<String, Any>()
foo["bar"] = "bar";
foo["baz"] = { foo["bar"] }
println(foo["bar"])
println((foo["baz"] as () -> String)())
They both are quite similar, apart from the this
reference in JavaScript.
This is not possible in Kotlin, as there’s no way the lambda can reference objects other than by their name.
For this reason, the JavaScript version can be declared in the following concise way, while the Kotlin version cannot as we need a reference to the map:
var foo = {
bar: "bar",
baz: function() {
return this.bar;
}
};
Code sample
Here’s a sample to understand how the above snippet can be generalized to solve the assignment:
private const val DATA = "data"
private const val INIT = "init"
private const val WORDS = "words"
internal fun extractWords(obj: MutableMap<String, Any>, filename: String) { (1)
obj[DATA] = read(filename)
.flatMap { it.split("\\W|_".toRegex()) }
.filter { it.isNotBlank() && it.length >= 2 }
.map(String::toLowerCase)
}
val dataStorageObj = mutableMapOf<String, Any>()
dataStorageObj[INIT] = { extractWords(dataStorageObj, filename) } (2)
dataStorageObj[WORDS] = { dataStorageObj[DATA] } (2)
(dataStorageObj[INIT] as () -> Unit)() (3)
1 | Declare the function independently for readability purpose |
2 | Add functions to the map-object |
3 | Call the function |
It’s also possible to use Kotlin’s extension functions feature:
internal fun MutableMap<String, Any>.extractWords(filename: String) {
this[DATA] = read(filename)
.flatMap { it.split("\\W|_".toRegex()) }
.filter { it.isNotBlank() && it.length >= 2 }
.map(String::toLowerCase)
}
val dataStorageObj = mutableMapOf<String, Any>()
dataStorageObj[INIT] = { dataStorageObj.extractWords(filename) }
dataStorageObj[WORDS] = { dataStorageObj[DATA] }
(dataStorageObj[INIT] as () -> Unit)()
The road to immutability
As a personal exercise, I tried to make the code immutable.
The first step is to use a Map
instead of MutableMap
.
In the first step, we need to reassign the reference each time, thus we end up using the var
keyword instead of the val
one:
internal fun Map<String, Any>.extractWords(filename: String) = this +
(DATA to read(filename)
.flatMap { it.split("\\W|_".toRegex()) }
.filter { it.isNotBlank() && it.length >= 2 }
.map(String::toLowerCase))
var dataStorageObj = mapOf<String, Any>()
dataStorageObj = dataStorageObj + (INIT to { dataStorageObj.extractWords(filename) })
dataStorageObj = dataStorageObj + (WORDS to { dataStorageObj[DATA] })
dataStorageObj = (dataStorageObj[INIT] as Callable<String, Any>)()
Now that’s done, we should initialize the object in one call chain, so we can again use an immutable reference:
val dataStorageObj = mapOf<String, Any>()
.run { this + (INIT to { extractWords(filename) }) }
.run { (this[INIT] as Callable<String, Any>)() } (1)
.run { this + (WORDS to { this[DATA] })} (1)
1 | The call order needs to be reversed |
Note that I kept the word frequencies map mutable to benefit from the merge()
method in my solution
Conclusion
As last week, no shared mutable state was involved.
Also, not all languages provide the same OOP implementation: some languages offer specific alternatives of it. In particular, JavaScript uses maps to implement objects. In this exercices, we mimicked this approach in Kotlin, with immutability as icing on the cake.