Kotlin operation scopes
Or how I tried to reach the ideal “building bundle” solution
In the last article we’ve spoken about Bundle
and how we can nicely organize the process of build them with Kotlin. But right after the post I’ve started to think “How we can sugar it (this process) even more”.
I want to warn you that this small post is just attempt to play with Kotlin’s features and build interesting pretty syntax, nothing more. So this article is about fun. It is not a guidance :)
I will remind that in the last time we came to the linked snippet of code (if changed last version a little bit, to support optional types), that allowed us to write next code of building Bundles:
val args = bundleOf(
"id" bundleTo 1,
"name" bundleTo "Michael",
"skill" bundleTo null as String?
)
Task
Our aim is to make this code is more concise, brief, short.
- I want to get rid of
bundleTo
word, since it holds 8 symbols. For example, for buildingPair
instance you need to useto
method, which occupies only 2 symbols. We can not simple renamebundleTo
method to, for example,bTo
since it looks careless. I want to make this name tiny, but save readability. null as String?
? Really? Unfortunately yes, since it is trade of for using optional arguments inbundleTo
function. When you want to pass explicitnull
arg, you will receive Overload resolution ambiguity compilation error, since compiler simply doesn’t know which optional you imply when passingnull
definitely.
Getting rid of casting
To get remove verbose null as String?
we need to apply NullObject pattern. The point is that we need to create a uniq immutable singleton object, that will be used as one more overload method bundleTo
and will indicates, that need to put null
in the bundle.
object Null
infix fun String.bundleTo(value: Null) = BundlePair { it[this] = value }
operator fun Bundle.set(key: String, value: Null) = putNull(key)
fun Bundle.putNull(key: String) = putString(key, null)
So the usage of this additional info will be:
val args = bundleOf(
"id" bundleTo 1,
"name" bundleTo "Michael",
"skill" bundleTo Null
)
As alternative you can create bundleToNull
extension:
fun String.bundleToNull() = BundlePair { it.putNull(this) }
Which can give us the next result:
val args = bundleOf(
"id" bundleTo 1,
"name" bundleTo "Michael",
"skill".bundleToNull()
)
That can save you from creating additional NullObject, but suffers from not consistent view (every bundle item uses infix function, but for null
the ordinary extension)
Now our snippet will look like:
Changing bundleTo method
Now I want to find an appropriate alternative for bundleTo
method that will provide more declarative way of building Bundles in Android. I want to have in the end something like:
val args = bundle {
"id": 1,
"name": "Michael",
"skill": Null
}
Unfortunately with Kotlin I didn’t find the way to write exactly as aforementioned example, but still this language contains a lot of mechanism to sugar your life. One of them is operators. There is minus
operator that will give us next result:
val args = bundleOf(
"id" - 1,
"name" - "Michael",
"skill" - Null
)
Definitely the code is more terse than the penultimate snippet in the previous section. But there is one bug problem.
If we reserved
minus
operator String extension only for bundles, we will in fact allocate abstract and general operation for only one specific reason, which is not good. Moreover, if we will want to create, for example, ContentValues building mechanism in the same way, how we will handleminus
operator for them
To resolve it we need to make operator overriding not for all program, but for only specific places. Let’s call the area of operator accessibility a Scope. So, for example, we will have two building mechanisms, that uses storages of String
keys and set of value types: Bundles and Content Values. Then for bundle we will have next interface:
In the same way, let’s say, we build a similar interface ContentValuesScope
with bunch of String extension operators. Now if we want to build our bundle somewhere in the code, we just need implement BundleScope
interface and get access to nice and concise mechanism:
class SomeClass : BundleScope {
fun openArgsFragment() {
val args = bundleOf(
"id" - 1,
"name" - "Michael",
"skill" - Null
)
//.... other code
}
}
But what if we will have both Bundles and ContentValues building in the same class. We can not implement both interfaces, because in this certain case we will have to manually choose realisation for each operator because of collisions or we will have a compilation error:
Class ‘SomeClass’ must override public open operator fun String.minus(value: Boolean): BundlePair defined in BundleScope because it inherits multiple interface methods of it
No panic. For that case we will create additional local scope methods:
object LocalBundleScope : BundleScope
inline fun bundleScope(block: LocalBundleScope.() -> Unit) =
LocalBundleScope.run(block)
inline fun <R> inBundleScope(block: LocalBundleScope.() -> R) =
LocalBundleScope.run(block)
The LocalBundleScope singleton in fact will keep operators, and both function will provide a nice access to it. Now we able to write aforementioned example with help of local scopes:
class SomeClass {
fun openArgsFragment() {
val args = inBundleScope {
bundleOf(
"id" - 1,
"name" - "Michael",
"skill" - Null
)
}
//.... other code
}
}
And with usage both of Content Values and Bundle it will look like:
class SomeClass {
fun openArgsFragment() {
val args = inBundleScope {
bundleOf(
"id" - 1,
"name" - "Michael",
"skill" - Null
)
}
val contentValues = inContentValuesScope {
contentValuesOf(
"id" - 1,
"name" - "Michael",
"skill" - Null
)
}
//.... other code
}
}
With Scopes we will be able to use custom operators
for specific aims without afraid of interleaving with other operator
implementations or default operator
implementation if it will be introduced some day.
Afterwords
Now with those 2 steps we’ve built nice, brief and save mechanism of building Bundles. Hooray!