Liudvikas Sablauskas
05/17/2023, 11:41 AMfun getConfiguration(softwareInformation: SoftwareInformation?, newVersion: Int): Configuration? {
// 1
return when (softwareInformation?.supportsUpdate) {
true -> null
else -> Configuration(version = newVersion)
}
// 2
return if (softwareInformation?.supportsUpdate != true) {
ControllerSoftwareConfiguration(version = newVersion)
} else {
null
}
// 3
return softwareInformation?.supportsUpdate?.takeIf { !it }
?.let { ControllerSoftwareConfiguration(version = newVersion) }
// 4 Some other approach?
}
Rohde Fischer
05/17/2023, 11:52 AMEither
monad such as from ArrowKT. Or use a sealed class/interface
null
is still (with the improved null-handling) a nasty solution. Even if one is truly able to prevent all the old null issues (I sincerely doubt it) it still has the weakness of having an implicit meaning that will never truly be consistent (no matter how stubbornly people will claim otherwise)
using either Either
or sealed allows you to explicitly and clearly model what is going on, so you will end up returning:
true -> NoSoftwareInformation.left()
else -> Configuration(version = newVersion).right()
or
true -> Configuraion.NoInformtion()
else -> Configuration.WithInformation(version = newVersion)
that said, as a side comment, your function should probably never be called in the way you designed it
rather than do:
val softwareInformation = getInformation()
foo.getConfiguration(softwareInformation, newVersion)
do:
val softwareInformation = getInformation()
softwareInformation?.let { foo.getConfiguration(it, newVersion) }
that way you do not need to design the ugly and bad null
handling. Also this will probably lead to a design raising the question of why softwareInformation
is even nullable in the first place - but of course that depends on your project etc.Liudvikas Sablauskas
05/17/2023, 12:18 PMEither
or Option
in this case is that it seems like you are still handling null
, but you're just making it explicit the form of a class. In the end it's the same, null
just has the benefit of convenient syntax sugar. So using Either
doesn't seem like a big win here.
I guess I don't see a big difference between having a Configuration?
and a Configuration
which is a sealed class with some None
subclass. The second option seems even worse to me, more verbose definitely.
I do agree that restructuring would help which would remove the need for null
in the first place.Rob Elliot
05/17/2023, 12:57 PMval supportsUpdate = softwareInformation?.supportsUpdate ?: false
return if (supportsUpdate) null else ControllerSoftwareConfiguration(version = newVersion)
Makes it explicit that the default value for supportsUpdate
is false and then it's a simple ternary with no double negatives.
I'd be wondering about refactoring getConfiguration
to make softwareInformation
non-nullable though... it feels smelly. Could you have a default instance of SoftwareInformation
where supportsUpdate
is false?
val noInformation = SoftwareInformation(supportsUpdate = false)
val configuration = getConfiguration(softwareInformation ?: noInformation)
Rohde Fischer
05/17/2023, 7:58 PMOption
-class, basically I see it as a slightly prettier version of doing `null`s (which imo is a bad design)
tbh. I’ve never understood why Either
(and a few others) aren’t part of the core libraries, but that’s a different talk
the major difference between the None
and what I propose is being explicit in the naming - which apart from being more useful also allows to communicate various reasons for a missing object (i.e. Configuration
in this case), which allows you to communicate your domain (i.e. rules and understanding) rather than null
or None
both of which doesn’t really say muchJoffrey
05/18/2023, 10:30 AMOptional
is just a disguised null that doesn't have the syntax sugar, so it's usually a bad idea to use it on Kotlin. Either
is not part and will probably not be part of Kotlin stdlib because it's part of a different programming style (railway programming) than the idiomatic Kotlin. The closest thing we're likely to get is union types, or some way to make custom sealed classes more practical to use.Rohde Fischer
05/18/2023, 12:00 PMnull
because of an error, violating the logic of “this is not there”. The second there’s almost any framework utilising any kind of reflection to populate values the risk is there (ORMs and deserialisers as the most common). Where it is true that Kotlin’s design guards against a lot of the null
-issues it does not mean that Kotlin is immune
To me null
is still a bad design (but necessary) decision and should only be used long enough to unwrap from systems that utilise it (legacy code, external frameworks etc.). Just because it got a lot better and less error prone in Kotlin does not to me mean that we should embrace it, I will need more/other arguments than that - especially since I see the other design as better, even if null were perfectly safe
Kotlin is strongly functional, so I see Either
as a perfect valid and valuable addition. It is also the best way to model in any DDD setting, unless you want to specify all domain errors as something sealed. I think Either
(whether it’s called Either
, Result
or something else) is the most sensible way to get away from the malpractice of using exceptions for domain errors. Exceptions should be for exceptional cases and only programming related errors. Cases such as someone trying something they are not allowed is a business/domain rule and should be modelled appropriately 😉 There are probably also other ways to model that, but from what I’ve seen so far, I see sealed classes/interfaces and Either
as the viable solutions (feel free to teach me a better way if there is, I’d be very happy if I’m wrong)Rohde Fischer
05/18/2023, 12:04 PMLiudvikas Sablauskas
05/26/2023, 6:16 PMnull
as early as possible. I feel like I often run into issues where this doesn't seem reasonable for some reason. Two common cases come into mind:
1. A model that has a lot of fields which are all nullable for their own separate reasons. There are a lot of good examples for this. For example, some kind of Form that has a lot optional fields. Or a User
class, which might have some key information and a lot of optional stuff, which is not provided, like:
data class User(
val name: String,
val email: String,
val address: String?,
val phone: String?,
val gender: Gender?,
val petInfo: PetInformation?,
val relationshipInfo: RelationshipInformation?,
// etc.
)
In this case, would you argue that Gender
, PetInformation
and RelationshipInformation
should all be sealed classes
and have their own NoInformation
instead of being nullable? Perhaps so, but I see 2 drawbacks. #1: a field being nullable already has an immediate clarity that screams "no information available" (but I do see your argument that just null
doesn't have the namespace and when passing it around it becomes unclear which class the null
empty state originally came from, which the sealed class
could help with). #2: The NoInformation
needs to be consistent throughout the code. Otherwise, if you call it NoInformation
here, Empty
in another place, NotGiven
in another one, then you cause more confusion than with a null
, since all of those things represent the same situation: the value is not given.
And even if you did that, it still leaves address
and phone
, which are basic Strings. So basically, in this case, would you say that every single nullable field should be a sealed class
with it's own NoInfomation
? Seems a bit.. overkill 😕
Another sad caveat is that if I check somewhere that if(user.petInfo != null) { doSomething(user) }
, inside doSomething
the compiler still doesn't know that petInfo
isn't null
, so I'm forced to check again. In this case, having sealed classes
wouldn't even help. I thought contracts could fix this, but no cigar. But I guess this is a separate issue.
2. Another example of kinda forced nullability is a large model which has nullable fields that get filled up during runtime. Since the model is big, making new data class
with just those fields taken out seems kind of overkill. Once again you are forced into nullability. This was the case in my situation. softwareInformation
is part of a parent data class
and it's filled some time later. (Not via var
but via copy
of course). And multiple fields in the parent data class
do that, they are nullable and later get filled.Joffrey
05/26/2023, 10:08 PMRohde Fischer
05/30/2023, 7:35 AMNoInformation
is still better (although only marginally). Better is NotProvidedByUser
or similar, so it’s entirely clear what the reason is (which is part of the point 😉 ) That said, I’d also be hesitant about how to represent forms with that kind of optional logic, unless it’s made explicit in the form - I’ve seen so many errors on stuff like a form is null
or undefined
until the user touched it, but then it’s “touched” by no data is filled in, what is the truth here? I think especially the form case actually provides a prime example on why null
makes things worse. A text field for instance contains a string (that might be empty) and that’s it. The null
here is an example of some developers trying to be “clever” and putting multiple states into something that should’ve been a separate state - and that actually also invalidates your own statement, because the null
does not convey NoInformation
, it actually conveys more (which it shouldn’t and which would’ve probably been clear if that nasty pattern of using `null`s in forms had never been established
also I will question the logic of NoInformation
having to be consistent throughout the code more than most other cases. To begin with NoInformation
is a bad name, because it conveys exactly the same information as null
and Option.None
, and in the case provided all of them actually masks the actual situation causing misleading situations. Think of it in terms of the domain, what is happening? Did the user touch the form/field? Did the user decide not to fill the form/field etc.? Convey that information, even if it’s unwrapped to a simpler default value later (but if so, keep in mind that you get information loss at that point)
The typing example could be “hacked” using generics and sealed classes
, but that said that solution is very forced, and I would almost certainly consider it wrong 😉 That said, I also think the case is so rare that it’s fine, and in most cases I’d also inclined to say that doSomething
should do it’s own consistency checking. Basically the type checking in this case would - even for simple cases - become hellish, and I wouldn’t want to go out through that path, unless a UserWithPet
-model is relevant (it very well could be, and probably is way more than people model it, IMO it seems people do not model no where enough)
In the large model case, I guess there are two cases. In the one where I’m in control of my models, I’d remodel. If I got a model (and/or model hierarchy) large enough for this to be an issue, I’m almost certainly doing something wrong (there might be exceptions, but I have yet to see them), models large enough for that sounds like a “God model” that does way too much. The second case is where the model is outside my control - my answer to that is probably predictable 😉 basically split the model into smaller units, so it makes sense - and also if possible filter out anything not needed
That also in almost all I’ve done solve the mutability issue, one exception is my work with DNA analysis, where using copy
wasn’t viable - at least not if it were to run on a sane machine. I cannot remember what we actually did back then, but today I’d wrap it in a local mutable model during the calculation that is only accessible in a tightly controlled environment (ofc. be aware of the concurrency implications in that case) do the mutable work and then output the result in an immutable class once outputting it.
One note on the last part is that I usually don’t do UI, but I am aware that the mutability part has it’s own implications there, since the speed of user events etc. also (at least in some cases) makes immutability unviable - however, if I’m not mistaken the same pattern as above (local mutable model) should be applicable - I am also aware that a lot of frameworks aren’t designed that way, so it might be us as application developers that need to shield the mess (just as we should shield against the `null`s from forms/fields)