Re "command-like" objects: I'm not following, can you give an example of something you can do in Scala here that you can't in Kotlin?
Re "command-like" objects: I'm not following, can you give an example of something you can do in Scala here that you can't in Kotlin?
For the same reason that being able to write val rather than var matters. With perfect discipline you can write pure functional code in any language, but in practice (and in codebases that likely have some non-functional sections, particularly if you're interoperating with Java) being able to be 100% sure that a given value is really immutable rather than 99.9% sure makes a huge difference.
> Re "command-like" objects: I'm not following, can you give an example of something you can do in Scala here that you can't in Kotlin?
There are lots of different use cases for this stuff, but one I was working with today was database transactions. I have operations that should only happen inside a database transaction, and want to enforce that at the type level, but I want to be able to compose several of those functions in a single database transaction, so I don't want to just have each one run its own transaction. So what I do is have a custom type, something like MustHappenInTransaction[A], which is actually just an alias for an existing library type (Free), and I automatically get the ability to use the for/yield syntax to compose these, and can use existing library functions to operate on it almost as if it were a plain value, but still safely (e.g. traverse rather than map on a sequence). I have no way to "get the A out" except for my doTransaction() function (what's why I say it's a command-like object), so the function will definitely happen in a transaction, but I can still do normal function composition, extract out common subfunctions, and all that.
You can't do that in Kotlin because there's no equivalent to "do notation" or "for comprehension" that you can use for user-defined types, and there are no higher-kinded types so you can't implement generic helper functions like traverse (there's no way to write the type signature it should have).
class MyTransaction {
fun begin() {}
fun commit() {}
}
fun MyTransaction.query() {}
fun MyTransaction.update() {}
inline fun <R> transaction(tblock: MyTransaction.() -> R): R {
val t = MyTransaction()
t.begin()
try {
return t.tblock()
} finally {
t.commit()
}
}
fun test() {
// update() doesn't compile, no such method
val hello = transaction {
query()
update()
query()
"Hello"
}
println("$hello") // prints "Hello"
}
Does that not work for your usage for some reason? Is there a problem with this approach I'm not seeing? typealias MustBeInTransaction = MyTransaction.() -> Unit
fun makeObject(i: Int): MustBeInTransaction {
return { /* do something with i or whatever in a transaction */ }
}
fun test() {
var genericArray = arrayOf(1, 2, 3).map { makeObject(it) }
//genericArray.forEach { it() } // fails to compile
transaction {
genericArray.forEach { it() }
}
}
Like that? type MustBeInTransaction = implicit MyTransaction => Unit
def makeObject(i: Int): MustBeInTransaction = {
return { /* do something with i or whatever in a transaction */ }
}
def test(): Unit = {
var genericArray = Array(1, 2, 3).map(makeObject)
//genericArray.forEach { _() } // fails to compile
transaction {
genericArray.forEach { _() }
}
}
EDIT - I just noticed that this is almost exactly the same as the example used in the function types link below[1] https://stackoverflow.com/questions/45875491/what-is-a-recei...
[2] https://kotlinlang.org/docs/reference/extensions.html
[3] https://www.scala-lang.org/blog/2016/12/07/implicit-function...
I think this issue with this is what happens when you combine DB transactions with async queries with errors and null handling? It seems like you have to combine 4 different mechanisms in order to do this (receivers, async/await, try/catch, if(null)). Is there some way to do this all with receivers? It seems like they don't really compose.
The 'command' version in Scala could be written roughly the same way for any combination of the 4 features (the types would just change). For example you could write your original example as:
def test() {
// update() compiles but doesn't do anything to the DB
val helloDBOperation = for {
result1 <- query()
_ <- update()
result2 <- query2()
} yield "Hello"
println("$hello") // prints "Hello"
}I think this issue with this is what happens when you combine DB transactions with async queries with errors and null handling? It seems like you have to combine 4 different mechanisms in order to do this (receivers, async/await, try/catch, if(null)). Is there some way to do this all with receivers? It seems like they don't really compose.
The 'command' version in Scala could be written roughly the same way for any combination of the 4 features (the types would just change). For example you could write a Scala version of your original example as:
def test(): Unit = {
// update() compiles but doesn't do anything to the DB
val helloDBOperation = for {
_ <- query()
_ <- update()
_ <- query2()
} yield "Hello"
val hello = db.run(helloDBOperation)
println("$hello") // prints "Hello"
}
This would look almost exactly the same no matter which combination of the 4 features you use.EDIT - it looks like I somehow also accidentally replied to you with an earlier version of this comment. Unfortunately , I didn't notice until it was too old to delete or edit. Please ignore the other comment that is very similar to this one. Sorry about the confusion.
https://kotlinlang.org/docs/reference/type-safe-builders.htm...
It's more designed to build DSLs or similar, so you'll find more documentaion/examples in that area of the docs.
> I think this issue with this is what happens when you combine DB transactions with async queries with errors and null handling? It seems like you have to combine 4 different mechanisms in order to do this (receivers, async/await, try/catch, if(null)). Is there some way to do this all with receivers? It seems like they don't really compose.
It's just a closure with a compiler-added paremeter. It can be nullable if you want and exceptions work the same as any other closure. If you want async/await you'd use coroutines. I think coroutines and receivers mix just fine, you'd just make the type a suspending lambda with receiver, but I've never tried.
Thanks for the link. However, I feel like that still doesn't give a lot of detail. Can you have multiple receivers? Can receivers be generic? Can receiver resolution be controlled by other arguments?
> It's just a closure with a compiler-added paremeter. It can be nullable if you want and exceptions work the same as any other closure. If you want async/await you'd use coroutines. I think coroutines and receivers mix just fine, you'd just make the type a suspending lambda with receiver, but I've never tried
Sorry I didn't mean to imply that Kotlin was incapable expressing all of these things together. I meant it would be desirable to express all of these things with the same tools. It seems like the you would have to express these with several different paradigms and be aware of how they interact. That also means you would need more boilerplate because you have to abstract each of these things separately.
For the same reason that being able to write val rather than var matters. With perfect discipline you can write pure functional code in any language, but [...]
Strongly agree with you there. This is one of the main gripes I've got with many languages. People just have a "well don't do it" attitude and refuse to see the benefit in the possibility of forbidding mutation. A very important point here is the realization that while the part about discipline might be true, it isn't just your discipline the whole codebase might hinge on, either.This is the exact reason why making it easy to do the right thing is a very important part of API design; simply by making hacks more laborious than the sane thing to do, you effectively prevent them most of the time.
So far I've never needed guaranteed immutability, I've just used kotlin's List (which is an immutable view). If that's the only thing anyone gets then it's functionally guaranteed immutable even though it's not sitting on an immutable implementation.
(Kerrigan: Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.)