Ayfri
08/05/2021, 11:08 AMRob Elliot
08/05/2021, 11:36 AMDominaezzz
08/05/2021, 12:26 PMDominaezzz
08/05/2021, 12:26 PMAyfri
08/05/2021, 12:39 PMRuckus
08/05/2021, 2:47 PMShalu TD
08/05/2021, 2:49 PMAyfri
08/05/2021, 2:51 PMAyfri
08/05/2021, 2:51 PMRuckus
08/05/2021, 3:00 PMRob Elliot
08/05/2021, 3:29 PMval rex: Dog = TODO()
val bmw: Car = TODO()
rex.getInto(bmw)
Obviously the domain of Dog
shouldn’t know about the domain of Car
, and vice versa. But it’s quite nice to call it that way, rather than putDogIntoCar(rex, bmw)
or something.
4) Adding methods which would otherwise introduce circular dependencies.
For instance Any
having a method toString()
while String : Any
introduces a circular dependency between Any
and String
. It’s there because Java defined it that way on java.lang.Object
, but it would be much better if there were an extension method Any.toString()
- then there’s no circular dependency.
Of course you could just have plain old functions, but extension functions:
1) Allow you to maintain “subject verb object” ordering
2) Allow using null dereferencing - fun Foo.bar()
can be called foo?.bar()
if foo
is nullable, which is much nicer than foo?.let { bar(it) }
3) Allow discovery in an IDE - it’s nice to be able to go foo.
, hit control space and see the extension methods as possible things you want to do.Ruckus
08/05/2021, 3:39 PMtoString
was an extension instead of a member, it couldn't be overridden in sub-classes. Also, I wouldn't consider a parent method returning a sub-type a circular dependency. That's just narrowing and is very common and useful.Rob Elliot
08/05/2021, 3:40 PMRuckus
08/05/2021, 3:41 PMRob Elliot
08/05/2021, 3:42 PMRuckus
08/05/2021, 3:43 PMRuckus
08/05/2021, 3:44 PMAyfri
08/05/2021, 3:47 PMRob Elliot
08/05/2021, 4:08 PMtoString()
on Any - the static compilation means the polymorphic version would be ignored.Rob Elliot
08/05/2021, 4:10 PMinterface Show { fun toString(): String }
. String interpolation should require an explicit call to toString
, too - so often you see untested code, particularly logging, where an object which hasn’t got a useful toString is interpolated. I’m with Haskell on this.
I also like the way that implementing an interface happens after the declaration of the type in Haskell, so you can effectively make Any
implement Show
after the declarations of Any
and Show
, avoiding the circularity.)Rob Elliot
08/05/2021, 4:12 PMRuckus
08/05/2021, 4:17 PMtoString
should be on Any
vs in an interface is an entirely separate question though. And Haskell (as well as Rust since that's another common alternative brought up in such discussions) isn't a great example since it's not object oriented, and design decisions don't often carry across paradigms well (in my experience at least). A lot of Haskell, Rust, C, etc. implementations work well within their context, but don't really fit as well once you have to take in to account how it will interact with OOP.Ruckus
08/05/2021, 4:24 PMAyfri
08/05/2021, 4:26 PMjavaru
08/06/2021, 2:27 PMYes but for personal classes, what is the case ?As Rob mentioned, sometimes we want to extend functionality that is only relevant to part of the code, and not the class itself. And using extension functions has the advantage over utility functions in that calling a function on the class as a receiver can feel more “natural” in its use than passing it into a function. Let’s say in one part of my code I need to validate a person. Validation is specific to the place the Person object is being used in. For example here validation is they are 21 or older. In another part of the code a “valid” person might be 16 and older and have a driver’s license. So I would not add the function to the Person class, but to where I need it. I can write either:
private fun validate(person: Person): Boolean = person.age >= 21
private fun Person.isValid(): Boolean = this.age >= 21
When I use those, the extension function just presents itself better in the code. It reads more naturally.
if (validate(person)) { ... }
if (person.isValid()) { ... }
Also, with extension functions, I can create a fluent API to use rather than calling a series of utility functions:
internal fun Document.normalize(): Document
{
// normalization code
return this
}
internal fun Document.convert(): Document
{
// conversion code
return this
}
internal fun Document.process(): Result
{
// process code
return Result()
}
And when I use it, it results in very readable succinct code:
val document = ...
val result = document.normalize().convert().process()
If I had to do that with utility functions, I’d have something like:
var document = ...
document = normalize(document)
document = convert(document)
val result = process(document)
Not only is that more lines of code, it doesn’t read nearly as well. As mentioned in the book Kotlin in Action, a leading design goal of Kotlin is to make it as readable as possible. That’s because as developers, we actually spend more time reading code, then writing it. Moreover, in the first use with the extension functions, my document
is a val
. But when I use the utility functions, I had to make it a var
We want to favor immutables as they result in less bugs, and are better for functional programming.Ayfri
08/06/2021, 2:34 PM