val fiveApples: BigDecimal = tag(BigDecimal(5), "apple")
val fiveOranges: BigDecimal = tag(BigDecimal(5), "orange")
val threeApples: BigDecimal = tag(BigDecimal(3), "apple")
// One can add apples to apples
threeApples + fiveApples // BigDecimal(8)
// But can't add apples and oranges
fiveApples + fiveOranges // IncompatibleTagsException: Tags are incompatible between 'apple' and the arguments ['orange']
- Why?
- Usage
- Opting out
- “Zero-Cost Abstraction”
- Import
- Tag laws
- Why wouldn't you just
X
- Limitations
- TODO
Somewhat recently™ we had a thing to do at work were we were having to calculate some invoices. In order to get all the relevant information, we had to query several services which responded with raw numbers. Some of these values had V.A.T., some didn't. Some were in cents, some in decimal currency amounts, and some other qualities that were not obvious given the data type we were receiving.
We needed to make sure that we were not adding (or any other kind of operation) between numbers of different “kinds”, so we started making comments in the code base, but this didn't prevent us from having this kind of problems:
class SomeComputation {
BigDecimal calculateTotal() {
BigDecimal toll = dataFromSomeService.getTollValue(); // This has tax included
BigDecimal tip = dataFromAnotherService.getTipValue(); // This does not have tax included
return toll + tip; // Oh noes!
}
}
(Refer to Why wouldn't you just X
to see other alternatives that were considered)
You can find a Worksheet to try it out yourselves at src/main/kotlin/Try.ws.kts
.
We basically try to replace comments, for code.
import static ar.com.florius.aao.Tag.tag;
class SomeComputation {
BigDecimal calculateTotal() {
BigDecimal toll = tag(dataFromSomeService.getTollValue(), "This has tax included");
BigDecimal tip = tag(dataFromAnotherService.getTipValue(), "This does not have tax included");
return toll + tip; // This will trow an IncompatibleTagsException
}
}
This way we can safely operate with tagged things, with no extra overhead in the types.
val toll: BigDecimal = tag(dataFromSomeService.getTollValue(), "taxed")
val tip: BigDecimal = dataFromAnotherService.getTipValue()
val tax: BigDecimal = tag(BigDecimal.valueOf(0.21), "taxed")
toll + (tax * tip) // This is now Ok!
The result itself is also tagged with the most specific of the tags. Refer to Tag laws for a more in depth understanding of what "most specific" means.
So in this example, tip is not tagged with anything, but after doing tip * tax
, te result will get tagged
with "taxed"
, and now it can be operated (+
) with toll
that is "taxed"
tagged.
TBD
Let me start this with a simple, yet powerful quote:
There are no zero-cost abstractions.
What I tried to achieve here is that the tagging “meta world” can be disabled, incurring in the cost of a really naive function call if assertion status is turned off.
fun <T : Any> tag(o: T, _: String) = o
TBD. Uploading to maven central is a pain 😅
Joined semilattice is the name of the game when it comes to tag. There are three “layers” of semilattices interacting in tags. In reverse order of application:
TagName
A string representation of the tag itself, where dissimilar values yield ⊤Breadcrumb
A ordered set ofTagName
that are joined by position wise join, where an intermediate ⊤ bubbles toBreadcrumb
's ⊤. Shorter sets have the default missing values ofTagName
's ⊥Namespace
A map of namedBreadcrumb
s that are joined by their key, where an intermediate ⊤ bubbles toNamespace
's ⊤
⊤: the maximum element. This can be interpreted as incompatible. ⊥: the minimum element. This can be interpreted as "no information".
Each layer has a distinct string representation that can be used. This way:
foo
is theTagName(foo)
foo:bar
is theBreadcrumb
ofTagName(foo)
andTagName(bar)
(the order is not important)biz->foo:bar,buz->foo
is theNamespace
of theBreadcrumb
sfoo, bar
namedbiz
, and theNamespace
of theBreadcrumb
foo
with the namebuz
class SomeComputation {
BigDecimal calculateTotal() {
BigDecimal toll = new BigDecimalWithTax(dataFromSomeService.getTollValue());
BigDecimal tip = new BigDecimalWithoutTax(dataFromAnotherService.getTipValue());
return toll + tip; // does not compile 👌
}
}
Now we have an error where we wanted, but the usage is very much impede, as one should either rewrite every method used
from BigDecimal
onto both BigDecimalWithTax
and BigDecimalWithoutTax
, or manually de-encapsulate the value for
usage; and once we cross that threshold of going back to normal BigDecimal
s, we are stuck in the same problem space.
This was deemed too much boilerplate, a performance hindrance and not composable in any way.
To tackle the performance in 👆, it's true that we are using mostly Kotlin >1.3, so we have inline classes; but a major difference that I wanted to avoid is having to manually unbox values to use them. They key point of inline classes is that they are not their wrapping counterparts.
inline class WithTax(private val value: BigDecimal) {
fun valueWithTax() = value
}
inline class WithoutTax(private val value: BigDecimal) {
fun valueWithTax() = value * tax
}
@Suppress("RedundantExplicitType")
class SomeComputation {
fun calculateTotal(): BigDecimal {
val toll: WithTax = WithTax(dataFromSomeService.getTollValue())
val tip: WithoutTax = WithoutTax(dataFromAnotherService.getTipValue())
return toll.valueWithTax + tip.valueWithTax // Crisis adverted!
}
}
Whereas a tagged object, is of the original object's type!
fun <T : Any> tag(o: T, tag: String): T
- Cannot tag primitive, array or final types (because JVM rules)
- Objects that use raw fields (and not methods) will bypass any magic
aao
is able to do - Untagged objects interacting with tagged objects bypass
aao
- [] A way to explicitly ignore tags