https://kotlinlang.org logo
#getting-started
Title
# getting-started
d

Dron Bhattacharya

02/23/2023, 10:19 AM
Can I do
Copy code
class TList(): MutableList<Some> {

}

TList(Some(), Some()).add(Some())
I have tried things like that shown in this code block but it is not working I have a list of something. I want it to have a few methods like add, addAll, remove, etc that are already in MutableList..... If you cannot understand what I am trying to do, please ask. I am open to suggestions on how to approach this
j

Joffrey

02/23/2023, 10:22 AM
You can definitely declare
class TList(): MutableList<Some>
, but since
MutableList
is just an interface, you would have to implement all the methods yourself. Instead, you could choose to extend an actual implementation of list such as `ArrayList`:
Copy code
class TList : ArrayList<Some>()
https://pl.kotl.in/cQoA8nFOE
Also, regarding your last line, you cannot call
TList(Some(), Some())
if you don't declare such constructor yourself. With your current declaration (or the one I suggested), you would need to first create an empty list with
val list = TList()
and then add stuff to that list. If you want a constructor that behaves like
listOf(...)
, you can add a secondary constructor:
Copy code
class TList(): ArrayList<Some>() {
    constructor(vararg initialElements: Some) : this() {
        addAll(initialElements)
    }
}
https://pl.kotl.in/Vf2mpjKNX
r

Ricardo Rodríguez

02/23/2023, 10:26 AM
I guess that your
TList
is going to have some methods that only make sense for a
MutableList<Some>
. Maybe you can avoid creating the
TList
class and directly extension functions to
MutableList<Some>
.
t

Troels Lund

02/23/2023, 10:28 AM
You can even have the
vararg
in the primary constructor, if you like:
Copy code
class TList(vararg e: Some) : ArrayList<Some>(e.asList())
d

Dron Bhattacharya

02/23/2023, 10:28 AM
@Joffrey why do we need the secondary constructor? Can we do it with the Primary constructor
j

Joffrey

02/23/2023, 10:29 AM
It's possible to add the
vararg
parameter to the primary constructor too, but if we do that I am suspecting that the
vararg
would unnecessarily create an empty array every time the constructor is called, even without any arguments
And using the
.asList
call it would generate even more object creations
d

Dron Bhattacharya

02/23/2023, 10:32 AM
If I use 2nd-dary constructs then
TList()
does not create anything? But when I do
TList(Some())
an array is created.
j

Joffrey

02/23/2023, 10:33 AM
Yes. I don't think the compiler could optimize that array away if a true no-arg constructor doesn't exist, because it would have to still call
addAll
or
asList
.
But the real question is, why do you need to create this subclass in the first place? If you want to add extra operations on specific lists like
MutableList<Some>
, maybe extensions are enough. If you want to limit the number of operations, then you shouldn't extend at all but create an entirely new type instead and list the methods by hand. What's your use case?
d

Dron Bhattacharya

02/23/2023, 10:36 AM
This is what I roughly did previously:
Copy code
class TaskList(private val tasks: MutableList<Task>) {
    val count: Int
        get() = tasks.count().also { println("TaskList.count($it)") }

    fun addTask(task: Task) = tasks.add(task).also { println("New task added!") }
    fun rmTask(task: Task) = tasks.remove(task).also { println("1 task removed!") }
    fun findTask(id: Int): Task =
        tasks.first { it -> it.id == id }.also { println("Task.id(${it.id}) was searched and found") }

}
now I have to write all these functions like addTask, rmTask, etc. But these functions are already present in collections like Mutable-list. Moreover, I am using a MutuableList underneath. Why not extend it then? this was my thought. Am I correct?
Can you please tell me if I am doing anything wrong here?
What is the difference between :
Copy code
val tasks = mutableListOf<Task>()
and
Copy code
class TList(vararg task: Task) : ArrayList<Task>() {
    init {
        addAll(task)
    }
}
Which I should I do? And why?
🧵 3
j

Joffrey

02/23/2023, 10:59 AM
The question is, do you need a separate type? It seems you really just want a
MutableList<Task>
, so why not use
mutableListOf<Task>()
directly?
There is a very good reason to do so: an overwhelming number of extension functions in the sdlib for
List
and
MutableList
(
filter
,
map
,
fold
,
chunked
,
windowed
, and many more...)
c

CLOVIS

02/23/2023, 10:59 AM
The type is different. The first one is just a regular Kotlin list. The second one is an entirely new type that you created. I don't see why you would do the second one. It's longer, more complicated to understand, and it doesn't really have any benefits
d

Dron Bhattacharya

02/23/2023, 11:01 AM
Later I will have another list that contains multiple
mutableListOf<Task>()
or
TList()
whichever I choose to go with
t

thanksforallthefish

02/23/2023, 11:03 AM
have you thought of delegation?
Copy code
class TaskList(private val tasks: MutableList<Task> = mutableListOf()) : MutableList<Task> by tasks
?
1
your example becomes
TaskList(mutableListOf(Some(), Some())).add(Some())
a

Adam S

02/23/2023, 11:09 AM
if you wanted to look at an example of ‘wrapping’ a list inside of a class, and then defining helper extension functions to add/remove elements of a list, you could look at
JsonArrayBuilder
in Kotlinx Serialization
JsonArrayBuilder
is used to build a
JsonArray
, which is similar, except the list of
JsonElements
is read-only. The benefit is that the mutable list is private, and can only be modified via one function -
fun add(element: JsonElement)
. Having only one function is nice when you’re writing a public library, because it means you can limit how much code you have to maintain! Having only one
add()
function can be annoying to users though, so that’s why there are multiple helper extension functions to make it easier to add functions. Extension functions can’t access private members though, so they don’t risk exposing functionality that you would be locked into maintaining! But, if you’re writing a small private library, it’s probably nicer just to keep things simple. Maybe you want to migrate to a nice class later - but that’s always possible. My biggest advice would be: don’t get bogged down in paralysis analysis!
d

Dron Bhattacharya

02/23/2023, 11:11 AM
OK
t

thanksforallthefish

02/23/2023, 11:11 AM
I also like “fake constructor” like
Copy code
class TaskList(private val tasks: MutableList<Task> = mutableListOf()) : MutableList<Task> by tasks {
  companion object {
    operator fun invoke(varargs tasks: Task) = TaskList(mutableListOf(tasks))
  }
}
since
invoke
is declared as operator, it can be use as
Copy code
TaskList(Some(), Some())
d

Dron Bhattacharya

02/23/2023, 11:14 AM
@thanksforallthefish I was thinking, are your examples more of trying to make it look like how I want it to look?? I just want to do the most appropriate thing.
a

Adam S

02/23/2023, 11:15 AM
‘fake constructors’ are also defined in the style guide as ‘factory functions’ blob smile https://kotlinlang.org/docs/coding-conventions.html#function-names
factory functions used to create instances of classes can have the same name as the abstract return type
Copy code
// public interface
interface Foo { /*...*/ }

// private Foo implementation 
private class FooImpl : Foo { /*...*/ }

// factory function 
fun Foo(): Foo { return FooImpl() }
😵 1
d

Dron Bhattacharya

02/23/2023, 11:16 AM
I don't know what factory function is
I think I found some similarities. Is
mutableListOf()
a factory function?
t

thanksforallthefish

02/23/2023, 11:20 AM
@Dron Bhattacharya we do actually use that pattern in our code,
Copy code
class TaskList(private val tasks: MutableList<Task> = mutableListOf()) : MutableList<Task> by tasks
it has its uses, you have a custom list with all method on lists (which I like, for you have well understood method names instead of custom names like
find
) and you can easily add business methods when needed. I don’t know if there really is an appropriate way of doing things, it is always a trade-off between functionality, readability and performance.
d

Dron Bhattacharya

02/23/2023, 11:24 AM
@thanksforallthefish Can you explain the
by tasks
part? Your suggestion seems to be most useful for my future use-case.
t

thanksforallthefish

02/23/2023, 11:25 AM
it’s called delegation, you can read more https://kotlinlang.org/docs/delegation.html
👆 2
d

Dron Bhattacharya

02/23/2023, 11:26 AM
OK
@thanksforallthefish What is this
*task
syntax? With IDE suggestion 💡 I have modified the code like this:
Copy code
class TList(private vararg val task: Task): MutableList<Task> by mutableListOf<Task>(*task) {

}
Is it a unary operator like in python?
j

Joffrey

02/23/2023, 1:03 PM
*
is called the spread operator. It allows to use an array in a place where multiple arguments (
vararg
) would be expected (it "spreads" the contents of the array as if they were multiple arguments).
mutableListOf
expects multiple arguments for the elements of the newly created list, but
task
is an array of tasks, received through the vararg param itself (btw, I would really name this
tasks
-plural- for this reason)
👍 1
Again, I would really refrain from creating your own list type unless you want something specific.
d

Dron Bhattacharya

02/23/2023, 1:07 PM
I will in the future have some custom functions too. From the conversation in this thread I understood that in that case I have create my own list. But if I just want to use the functions that are available with MutableList, I should not create my own list. This is my understanding till now
j

Joffrey

02/23/2023, 1:08 PM
you can easily add business methods when needed
@thanksforallthefish you can also do that with extensions. This is not part of the benefits of using your own type, and not really a good reason to do so
@Dron Bhattacharya ☝️
I will in the future have some custom functions too. From the conversation in this thread I understood that in that case I have create my own list.
Not really. If you want to add operations to mutable lists of `Task`s, you can use extension functions without creating your own type:
Copy code
fun MutableList<Task>.doSomethingSpecial() {
    // ..
}
d

Dron Bhattacharya

02/23/2023, 1:10 PM
yes, extensions can meet my needs... Can you tell me when I should create my own type in a similar situation?
j

Joffrey

02/23/2023, 1:11 PM
Here are some examples of reasons why you would want your own type: • reduce the number of available operations (if you don't want your type to support all list operations). In that case you would not implement a list interface or extend a list class, so most of the suggestions in this thread would not be what you want. • store some extra custom state • define operations on your custom container class but not on all possible lists of `Task`s (this would usually be because you have custom state, but maybe not) • your custom class doesn't really represent a list semantically, it just happens to use a list for its implementation. This usually implies that the backing implementation could change, or some custom state might be needed at some point If your goal is to have a nicer name for
MutableList<Task>
, you could use a
typealias
instead. If your goal is only to add nice extensions on any (mutable) list of
Task
, then use extension functions.
d

Dron Bhattacharya

02/23/2023, 1:16 PM
reduce the number of available operations
This is a necessary requirement. But, in the last code that I shared, creating my own Type is not limiting the number of unnecessary operations. Am I missing something?
j

Joffrey

02/23/2023, 1:17 PM
in the last code that I shared, creating my own Type is not limiting the number of unnecessary operations.
Exactly, that's why I added "In that case you would not implement a list interface or extend a list class, so most of the suggestions in this thread would not be what you want" If you want to limit the number of operations, you have to create your own type, but you cannot make it implement the
List
or
MutableList
interfaces because by definition that would mean supporting all their operations.
But, you used the word "necessary" here. Do you need to forbid some operations, or do you just find them unnecessary - those are 2 different things. If you want to forbid them, you need a new type that is not a subtype of
List
. If you just find those operations unnecessary, you just get them for free by using lists, so it's not a problem per se and you don't need your own type for that.
d

Dron Bhattacharya

02/23/2023, 1:21 PM
All those operations will never be used. But if they are available, there is a chance they will be used by mistake.
j

Joffrey

02/23/2023, 1:23 PM
So we're coming back to the current last reason I listed: "your custom class doesn't really represent a list semantically". Does it? Or does it not represent a list? What would be the harm in someone filtering the task list using
.filter
? Do you have any example of list operation that you want to forbid? (or that would be a mistake to use with your type?)
d

Dron Bhattacharya

02/23/2023, 1:27 PM
Operations like
sort
,
addAll
,
removeAll
, etc that modify the List in a way the List should not be modified (can cause issues)
To put it in more "common grammatical" form: • The Liskov Substitution Principle - a lot of inherited methods that I don’t want, because they probably don’t make sense in the context, and using them can cause unexpected problems
j

Joffrey

02/23/2023, 3:38 PM
You technically already allow
addAll
if you allow
add
, you just make it less convenient and likely less efficient. This may also be true for
removeAll
if you provide a way to iterate your list of tasks. If you don't provide a way to iterate, and just want to provide access by id, maybe you want a
Map
instead of a
List
(but that means you also give up on order, which would be strange because tasks are usually ordered). It would be interesting to share how you want to use the instances of this type, because there may be an existing data structure meant for your use case. But yes, overall I understand your point. If you really want to limit the number of operations, or control which exact operations you want to support, you will have to create a new type and define the allowed operations by hand. Then you're free to choose (and even change) the backing data structure(s) that you'll use inside.
💯 1
4 Views