Pass a list of functions with different parameters in Kotlin - kotlin

I have a class that calls functions depending on events. Events are emitted from sockets. I should catch these events, parse JSON and respond (call a corresponding function). For instance, {"event_name": "message", "data": {"text": "dfgfdgfdg", "sender": "dsfdsfs"}}
fun listener(jsonString: String, methodsMap: Map<String, () -> Unit>) {
val json = JSONObject(jsonString)
val data = json.getJSONObject("data")
when (json.get("event_name")) {
"update" -> {
val count = data.getInt("count")
methodsMap["update"]?.invoke(count) // 1 parameter.
}
"message" -> {
val message = data.getString("text")
val sender = data.getString("sender")
methodsMap["message"]?.invoke(message, sender) // 2 parameters.
}
}
}
So, I cannot create one method that calls functions with different parameters. How to do this?

Since you are already have if-then logic in listener, having the functions in a Map is of questionable value and it forces you to to deal with the fact that your functions are of different types. If it is parametrisation of listener you are after, perhaps this (simplified example code that skips JSON) is sufficient:
class UpdateHandler {
fun update(n: Int) = println("update ( $n )")
}
class MessageHandler {
fun message(s1: String, s2: String) = println("message ( $s1 $s2 )")
}
fun listener(jsonString: String, updateF: (Int) -> Unit, messageF: (String, String) -> Unit) {
when (jsonString) {
"update" -> updateF(73)
"message" -> messageF("message", "sender")
}
}
fun main() {
val updateHandler = UpdateHandler()
val messageHandler = MessageHandler()
val listener = { json: String -> listener(json, updateHandler::update, messageHandler::message) }
listener("update") // prints: update ( 73 )
listener("message")// prints: message ( message sender )
}

First, I wanted to use a list of parameters in each function, but it leads to poor type verification during compilation. Also I wanted to assign vararg instead of List, but couldn't.
fun listener(jsonString: String, methodsMap: Map<String, (List<Any>) -> Unit>) {
...
methodsMap["update"]?.invoke(listOf(count)) // 1 parameter.
...
methodsMap["message"]?.invoke(listOf(message, sender)) // 2 parameters.
}
This is a poor solution. Bugs may occur, we should remember to change methodsMap in every class that uses listener when we change any event.
Second, I tried to use sealed classes. This is not so simple.
Third, I tried to use interface. We know that callbacks are usually made with interfaces. We can even merge interfaces in Kotlin. So, this can be a solution to a problem (but not to a question).
fun listener(jsonString: String, callback: EventListener) {
val json = JSONObject(jsonString)
val data = json.getJSONObject("data")
when (json.get("event_name")) {
"update" -> {
val count = data.getInt("count")
callback.onUpdate(count)
}
"message" -> {
val text = data.getString("text")
val sender = data.getString("sender")
callback.onNewMessage(text, sender)
}
}
}
interface EventListener {
fun onUpdate(count: Int)
fun onNewMessage(text: String, sender: String)
}
Then we can call listener outside of the class and pass any callbacks we like.

Related

Issue IDE warning if annotated member is not surrounded with a particular block

I have a data structure which has members that are not thread safe and the caller needs to lock the resource for reading and writing as appropriate. Here's a minimal code sample:
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
#RequiresReadLock
val nonThreadSafeMember: String = ""
}
interface LockableProjectItem {
val readWriteLock: ReadWriteLock
}
fun <T : LockableProjectItem, Out> T.readLock(block: T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(this)
} finally {
readWriteLock.readLock().unlock()
}
}
fun <T : LockableProjectItem, Out> T.writeLock(block: T.() -> Out): Out {
try {
readWriteLock.writeLock().lock()
return block(this)
} finally {
readWriteLock.writeLock().unlock()
}
}
annotation class RequiresReadLock
A call ExampleResource.nonThreadSafeMember might then look like this:
val resource = ExampleResource()
val readResult = resource.readLock { nonThreadSafeMember }
To make sure that the caller is aware that the resource needs to be locked, I would like the IDE to issue a warning for any members that are annotated with #RequiresReadLock and are not surrounded with a readLock block. Is there any way to do this in IntelliJ without writing a custom plugin for the IDE?
I think this is sort of a hack, but using context receivers might work. I don't think they are intended to be used in this way though.
You can declare a dummy object to act as the context receiver, and add that as a context receiver to the property:
object ReadLock
class ExampleResource : LockableProjectItem {
override val readWriteLock: ReadWriteLock = ReentrantReadWriteLock()
// properties with context receivers cannot have a backing field, so we need to explicitly declare this
private val nonThreadSafeMemberField: String = ""
context(ReadLock)
val nonThreadSafeMember: String
get() = nonThreadSafeMemberField
}
Then in readLock, you pass the object:
fun <T : LockableProjectItem, Out> T.readLock(block: context(ReadLock) T.() -> Out): Out {
try {
readWriteLock.readLock().lock()
return block(ReadLock, this)
} finally {
readWriteLock.readLock().unlock()
}
}
Notes:
This will give you an error if you try to access nonThreadSafeMember without the context receiver:
val resource = ExampleResource()
val readResult = resource.nonThreadSafeMember //error
You can still access nonThreadSafeMember without acquiring a read lock by doing e.g.
with(ReadLock) { // with(ReadLock) doesn't acquire the lock, just gets the context receiver
resource.nonThreadSafeMember // no error
}
But it's way harder to accidentally write something like this, which I think is what you are trying to prevent.
If you call another function inside readLock, and you want to access nonThreadSafeMember inside that function, you should mark that function with context(ReadLock) too. e.g.
fun main() {
val resource = ExampleResource()
val readResult = resource.readLock {
foo(this)
}
}
context(ReadLock)
fun foo(x: ExampleResource) {
x.nonThreadSafeMember
}
The context receiver is propagated through.

Kotlin Lambda with generic input parameter gets inferred to Nothing when stored in a collection

I have the following classes
class EventHandler<A : Aggregate, E : Event<A>>(
val eventClazz: KClass<out Event<A>>,
val handle: suspend (E) -> Unit
)
class Projection(vararg handlers: EventHandler<out Aggregate, out Event<out Aggregate>>) {
val handlers = handlers.asList()
}
Now I want to invoke the handlers handle function. The Handler is stored in a Projections handlers list.
val subscriptionFilterBuilder = SubscriptionFilterBuilder()
projections.forEach { projection ->
projection.handlers.forEach { handler: EventHandler<out Aggregate, out Event<out Aggregate>> ->
val eventType = getEventType(handler.eventClazz)
val filter = subscriptionFilterBuilder.withEventTypePrefix(eventType).build()
val options = SubscribeToAllOptions.get().filter(filter)
val handlerTarget: KClass<out Event<out Aggregate>> = handler.eventClazz
coroutineScope.launch {
eskWrapper.subscribeToAll(
options = options,
) { subscription, event ->
val deserialized: Event<out Aggregate> =
serDes.deserialize2(event.originalEvent).eventPayload
coroutineScope.launch {
handler.handle(deserialized)
}
}
}
}
}
The compiler gives me the error Type mismatch: inferred type is Event<out Aggregate> but Nothing was expected for the line handler.handle(deserialized).
I guess this is because of the way I store the handlers in the Projection but I don't know how to solve this.
I found a way to ensure type safety in my case.
I can add a function to the EventHandler which takes care of deserialization and invoking the actual handling function. Since this handleWithSer function (don't use this name) is aware of the generic types this approach works.
class EventHandler<A : Aggregate, E : Event<A>>(
val eventClazz: KClass<out Event<A>>,
val handle: suspend (E) -> Unit,
) {
val handleWithSer: suspend (RecordedEvent, EventPayloadSerDes) -> Unit =
{ event: RecordedEvent, serder: EventPayloadSerDes ->
val event = serder.deserialize<A, E>(event)
handle(event.eventPayload)
}
}
If there are other ways to get the correct type from the collection let me know.

rxjava, how to inspect the result of a Single

using kotlin, having code
fun fetchRemoteDataApi(): Single<RemoteDataResponse> = networkApi.getData()
// it is just a retrofit
#GET(".../api/getData")
fun getData() : Single<RemoteDataResponse>
fun mergeApiWithDb(): Completable = fetchRemoteDataApi()
.zipWith(localDao.getAll())
.flatMapCompletable { (remoteData, localData) ->
doMerge(remoteData, localData) //<== return a Completable
}
the code flow:
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache() //<=== would like do some inspection at this level
PublishSubject.create<Unit>().toFlowable(BackpressureStrategy.LATEST)
.compose(Transformers.flowableIO())
.switchMap {
//merge DB with api, or local default value first then listen to DB change
mergeApiDbCall.andThen(listAllTopics())
.concatMapSingle { topics -> remoteTopicUsers.map { topics to it } }
}
.flatMapCompletable { (topics, user) ->
// do something return Completable
}
.subscribe({
...
}, { throwable ->
...
})
and when making the call
val mergeApiDbCall = mergeApiWithDb().onErrorComplete().cache()
the question is if would like to inspect on the Singles<RemoteDataResponse> returned from fetchRemoteDataApi() (i.e. using Log.i(...) to printout the content of RemoteDataResponse, etc.), either in got error or success case, how to do it?
/// the functions
fun listAllTopics(): Flowable<List<String>> = localRepoDao.getAllTopics()
// which a DAO:
#Query("SELECT topic FROM RemoteDataTable WHERE read = 1")
fun getAllTopics(): Flowable<List<String>>
///
private val remoteTopicUsers: Single<List<User>>
get() {
return Single.create {
networkApi.getTopicUsers(object : ICallback.IGetTopicUsersCallback {
override fun onSuccess(result: List<User>) = it.onSuccess(result)
override fun onError(errorCode: Int, errorMsg: String?) = it.onError(Exception(errorCode, errorMsg))
})
}
}
You cannot extract information about elements from the Completable. Though you can use doOnComplete() on Completable, it will not provide you any information about the element.
You can inspect elements if you call doOnSuccess() on your Single, so you need to incorporate this call earlier in your code. To inspect errors you can use doOnError() on both Completable or Single.

How to filter a list of objects based on a dynamically created collection of predicates

I am working on a searching/filtering functionality where a user should be able to filter a list of events to fit a pattern which he will make in runtime.
I made a function filter which is looping over all the constraints the user has set, and then filtering the result.
My problem is that I am copying the list many times, and I was wondering if there is a way where I can do this kind of complex filtering in a more declarative (kotlin-) way without side effects.
fun filter(query: Filter, eventsIn: List<Event>): List<Event> {
var events = eventsIn
query.filters.forEach { filter ->
if (filter.key is EventFiltersListStuff){
events = when (filter.key as EventFiltersListStuff) {
PLACE -> events.filter { event -> (filter.value as List<*>).contains(event.location.place) }
AREA -> events.filter { event -> (filter.value as List<*>).contains(event.location.area) }
CATEGORY -> events.filter { event -> (filter.value as List<*>).any{it in event.category} }
GENRE -> events.filter { event -> (filter.value as List<*>).contains(event.genre) }
}
} else {
events = when (filter.key as EventFilters) {
TITLE -> events.filter { event -> event.title.contains(filter.value as String, true) }
PRICELT -> events.filter { event -> event.price <= filter.value as Int }
PRICEGT -> events.filter { event -> event.price >= filter.value as Int }
TIMELT -> events.filter { event -> event.time <= filter.value as Int }
TIMEGT -> events.filter { event -> event.time >= filter.value as Int }
}
}
}
return events
}
The model looks like this
data class Event(
val title: String,
val genre: String,
val image: String,
val link: String,
val category: List<String>,
val price: Int,
val text: String,
val tickets: String,
var time: Long,
val location: Location
)
I have two enums one is for inclusive filtering, where a user can filter a list based on multiple instances of the attribute in focus.
The other one is non inclusive, and will just remove all entities which is not matching the query.
enum class EventFiltersListStuff(val str: String, ) : FilterType {
PLACE("place"),
AREA("area"),
CATEGORY("category"),
GENRE("genre");
override fun str(): String = str
}
enum class EventFilters(val str: String, ) : FilterType {
PRICELT("priceLT"),
PRICEGT("priceGT"),
TIMELT("timestampLT"),
TIMEGT("timestampGT"),
TITLE("title");
override fun str(): String = str
}
The next code block is less relevant, but I'll include is for transparency, Because it is used in the function which is the core of my question.
interface FilterType {
fun str(): String
}
class Filter private constructor(val filters: Map<FilterType, Any>) {
class Builder {
private var filters: MutableMap<FilterType, Any> = mutableMapOf()
fun filters(key: FilterType, value: Any) = apply {
this.filters[key] = when (this.filters[key]) {
is List<*> -> (this.filters[key] as List<*>) + listOf(value)
is Comparable<*> -> listOf(this.filters[key], value)
else -> value
}
}
fun build(): Filter {
return Filter(filters)
}
}
}
I am also using the Filter to generate a filter in a GraphQL query, this is the reason I am having strings in the enums.
Simple and efficient solution would be just to filter each event with conjunction of all filters.
typealias SingleFilter = Map.Entry<FilterType, Any> // you may want to remodel it as a sealed class, more on that later
fun SingleFilter.isMatching(event: Event): Boolean = ...
fun List<Event>.applyFilters(filters: List<SingleFilter>) = filter { event -> filters.all { it.isMatching(event) }
Your model could be greatly improved by remodeling your filters as a sealed class, replacing instance checks and enums with polymorphism. This will have a huge benefit of being type-safe. You can create a hierarchy with two groups for your two filtering cases. You can define mapping between filter names and classes in different part of your code, decoupling serialization from filter logic.

Single-function listeners using lambda

With all the well-known single-function listeners we can use a simpler lambda notation
view.setOnClickListener { do() }
instead of the original, longer Java way of
view.setOnClickListener(object : View.OnClickListener {
override fun onClick(v: View?) {
do()
}
})
But what exactly makes this work? I tried to do the same with my own listener:
private var listener: OnCopyPasteClickListener? = null
interface OnCopyPasteClickListener {
fun onPasteClick(text: String)
}
fun setOnCopyPasteClickListener(onCopyPasteClickListener: OnCopyPasteClickListener) {
listener = onCopyPasteClickListener
}
and while the long approach works just fine:
copypaste.setOnCopyPasteClickListener(object : CopyPasteMenu.OnCopyPasteClickListener {
override fun onPasteClick(text: String) {
do(text)
}
})
I can't make it accept the short one:
copypaste.setOnCopyPasteClickListener {
do(it)
}
The IDE gives a type mismatch error.
Actually, if you have only one function to be invoked, I recommend you use Kotlin Callback.
typealias OnDoWorkListener = ((String) -> Unit)
class Work {
var doWork: OnDoWorkListener? = null
fun doSomething() {
doWork?.invoke("Message Here")
}
}
And in your function, you just set the callback to it
fun main() {
val work = Work()
work.doWork = {
Log.d("WORK", "This gets called from the `work` object. Message: $it")
}
work.doSomething();
}
We can also use function to set the listener as well.
class Work {
var doWork: OnDoWorkListener? = null
fun doSomething() {
doWork?.invoke("Message Here")
}
fun setOnWorkListener(listener: OnDoWorkListener) {
doWork = listener
}
}
fun main() {
val work = Work()
work.setOnWorkListener {
Log.d("WORK", "This gets called from the `work` object. Message: $it")
}
work.doSomething()
}
Higher order functions make this work:
Kotlin functions are first-class, which means that they can be stored
in variables and data structures, passed as arguments to and returned
from other higher-order functions. You can operate with functions in
any way that is possible for other non-function values.
From the same page:
Passing a lambda to the last parameter
In Kotlin, there is a convention that if the last parameter of a
function accepts a function, a lambda expression that is passed as the
corresponding argument can be placed outside the parentheses:
val product = items.fold(1) { acc, e -> acc * e }
If the lambda is the only argument to that call, the parentheses can
be omitted entirely:
run { println("...") }
Knowing this, a possible update on your class would look like:
class CopyPaste {
private var listener: (String) -> Unit = {}
fun setOnCopyPasteClickListener(onCopyPasteClickListener: (String) -> Unit) {
listener = onCopyPasteClickListener
}
fun doCopyPaste(value: String) {
listener.invoke(value)
}
}
fun main() {
val copyPaste = CopyPaste()
copyPaste.setOnCopyPasteClickListener { println(it) }
copyPaste.doCopyPaste("ClipboardContent!")
}
The class CopyPaste stores the listener, which is a function that takes a String parameter and does not return anything. Its function setOnCopyPasteClickListener accepts a function with the same signature as the listener property and at the end doCopyPaste accepts a String parameter and passes it to the stored function.
Actually, just after I posted, I searched for more thoughts and found this thread: https://youtrack.jetbrains.com/issue/KT-7770 This is indeed a debated limitation as it currently only applies to Java, not Kotlin itself. There is also a suggestion there that gives almost the required simplicity:
interface OnCopyPasteClickListener {
fun onPasteClick(text: String)
companion object {
inline operator fun invoke(crossinline op: (text: String) -> Unit) =
object : OnCopyPasteClickListener {
override fun onPasteClick(text: String) = op(text)
}
}
}
and then, thanks to this overloaded operator, it can be called as:
copypaste.setOnCopyPasteClickListener(CopyPasteMenu.OnCopyPasteClickListener { text ->
do(text)
})
But as the suggested answers offer a more idiomatic solution, I'll accept one of those, I only wanted to include this approach here for reference.