Just published an article explaining a non-obvious...
# feed
v
Just published an article explaining a non-obvious way to create compile-time checks for any kind of restricted code using Kotlin context parameters. Think of this analogy: you know how
suspend
functions can only be called from other
suspend
functions or from a pre-defined entry points (ex:
runBlocking
)? I found a way to apply this pattern to ANY restriction using context parameters! The use-case I described in the article: Preventing cloud-only code from being accedently called in on-premises (self-hosted) deployments, and vice versa! The magic: If someone tries to call a method that touches restricted code (even indirectly, deep down the call chain) — they get an instant compilation error! 🛑 These bugs literally can’t make it to production. You can apply this approach to many scenarios, for example: • restricting test-only code • guarding database operations from side effects • preventing GDPR-sensitive data from being sent to analytics services • And whatever you need! Takes just a few lines of code, and zero compiler knowledge required. Check it out: https://blog.kotlin-academy.com/stop-bugs-before-they-happen-compile-time-guardrails-with-kotlin-context-parameters-6696fb54c1e8
👍 1
🔥 7
K 13
y
I'm sure you're already aware of this, but it's possible to accidentally capture the context in a lambda, and thus end up performing a cloud-only action outside. For the use case presented, I don't think that's possible since you don't "switch contexts" between cloud and on-prem. In other situations where you might switch contexts though, it can happen. Something like Scala's Capture Checking would eliminate this problem Extending the suspend analogy further, you can actually use
@RestrictsSuspension
for this. It prevents captures (well, it doesn't, but it prevents calling the restricted methods without you having the context legitimately, as in thru a builder function from your "safe kernel"). This is really one step before monadic regions. If you want a cool Kotlin example of that, see this thread. I've also been working on a compiler plugin that makes this simpler, with the file and state examples linked above now looking like this and this. I want to eventually have a plugin that does capture checking so that your article can optionally be checked, with only a few annotations needing to be added. It's a large undertaking though.
🔥 1
j
We're currently using this in our server framework (that's being prepared for release) to ensure you can only read setting values after settings have been loaded. Context parameters are really nice stuff.
K 1
d
Another use case I've been pondering on:
Copy code
data class Stream(..., context(s3UrlRefresher: S3UrlRefreshService) val streamSignedS3Url: String, ...)
to prevent uses of that url without first seeing if expired (in which case that service should refresh it... what do you guys think of such a use?
y
@dave08 I'd instead do:
Copy code
class Stream(private val streamSignedS3Url: String) {
  context(expiredChecker: S3ExpiryChecker)
  val streamSignedS3Url get() = this.streamSignedS3Url.takeUnless { expiredChecker.isExpired(it) }
}
d
Nice idea, I'm just wondering if it wouldn't just be enough to use the context as a hint to do the refreshing... anyways i usually need to batch the resigning requests to send them to the server (i usually need to process a list of streams and their imageurls also need refreshing... but then again your code could be done locally, i'd just have to reprocess each one anyways...
Having to provide a refresh service everywhere i need to access that property could be a great reminder not to forget to refresh it.
y
Sounds like an OptIn might be what you want 🤷🏼
d
The opt in won't direct the user of the property to have the refresh service around... i know it's a bit of a funny use case... i was about to make a value class for that to mark that it might need refreshing, but i thought that context receivers don't just mark something, but actually require a dependency needed to accomplish the task. So even though i can't directly use it in this case, at least i can be one step closer by having the service around there.. that's my doubt, maybe it's not really such a good use of this? The article seems to be doing something similar.
v
Opt-in doesn’t poison the call chain upwards. If A requires OptIn, B is using A — in B the IDE would remind me to add an opt-in. But then I’m not forced to annotate B same as A and then if C would call B — I won’t see any hint that A was used under the hood (unless I have a discipline to annotate every single method — but that’s not really a compile-time check)
d
So do you think my usage of context parameters for this case is good @Vadim Briliantov?