I you consider an extension function just syntactic sugar for a function that takes an object as a parameter, you would do this:
fun C.foo() {
if (s == null)
throw IllegalArgumentException("C is illegal because it does not have s.")
}
// Or:
fun C.foo() {
requireNotNull(s) { "C is illegal because it does not have s." }
}
But when you view it as a method on an object, you would do this:
fun C.foo() {
if (s == null)
throw IllegalStateException("You cannot call foo() when s is null.")
}
// Or:
fun C.foo() {
checkNotNull(s) { "You cannot call foo() when s is null." }
}
I donât think there is a right and wrong here as long as you stay consistent. I would probably use an IllegalStateException because I think of extension functions as a kind of âmemberâ function. That said your argument to treat the receiver as an argument is also valid.
I think the way that the receiver is called this within the function, as well as the dot notation for calling it, are strong invitations to think of extension functions in a similar way to member functions (i.e. methods).
Yes, theyâre implemented as static methods in the bytecode, but I think thatâs more a reflection of JVM limitations, and for interoperability, than any statement of intent. Itâs similar in Java: in my experience, most Java static methods would be written as extensions if that was an option.
So I think the arguments for treating them like methods are much stronger, and for me an IllegalStateException would feel more natural.
This is an interesting point that could be taken for an argument for IllegalArgumentException as well. If you write a library that is used from java (and letâs face it, java is still much more used than kotlin sadly) then throwing an IllegalArgumentException might be the way to go, to make the interop with java even smoother.
And even with me bringing up this argument I personally think IllegalStateException is the way to go.
I think it varies depending on the function is and use-case. For me, this is all about communicating to the user as effectively as possible.
I usually land on illegal state exceptions for extension functions.
Although extension functions are statically resolved, they still feel like a property of instances of their recieverâs typeâsimilar to how member functions are exactly that, but in addition to that member functions are dynamically dispatched.
Just to round out the idea: you could just as easily imagine member functions to be functions with their reciever as a parameter (with dynamical dispatch)âthen youâd be asking this question about member functions.
When I look at both exceptions I would see IllegalArgumentException as a usage error, but IllegalStateException as also a design error (sometimes not even a usage error). In principle, you would aim to design classes such that they only have a single (behaviour relevant) state. Even with such a design it can be possible that the state is inconsistent/corrupt, but that would be an indication of an implementation bug (somewhere). Of course there are cases where there must be state, but often subclasses/generics/interfaces can be used to present a typesafe interface that encapsulates this state in a way that the compiler can check.
An IllegalArgumentException indicates to me that the caller is violating the function contract. So in the case that you have an extension function on a list and it doesnât work on an empty list (by design) that is not a state issue, it is a value issue. The argument doesnât fit the expected domain. At the same time I can see where you want parity between regular and extension functions and be consistent.
Well said @pdvrieze I hadnât looked at it quite that way.
So if one chooses via those guidelines, what is chosen for a class with initialization (no extension functions)? Failing to call MyClass.initialize() before MyClass.doSomething() is a usage error. But I would consider this example to be one for an IllegalStateException.
Do you tend to stick with your guidelines for choosing or do you flex on this case?
A part of me wishes we had an IllegalUsageException instead of IllegalArgumentException.
@arocnies Beyond that you would attempt to avoid classes with initializer functions (one way is to have two types (possibly sharing private state), one is the creator, the other the initialized object - they could even share an interface where appropriate). However, in the case that it is unavoidable (like a fixed ABI) I would go with IllegalStateException as the expectation is that you only use initialized objects.
Btw. the builder pattern in general is a good way to avoid the issue with initialization, and in Kotlin you can even have an inline factory function that does the builder transparently (with lots of bits left out):
class TypeWithComplexInit private constructor(builder:Builder) {
// code that uses the builder to initialize properties etc.
class Builder(var propA:Int=0, var propB:Boolean=false)
companion object {
inline operator fun invoke(configurator: Builder.() -> Unit):TypeWithComplexInit {
val b = Builder()
b.configurator()
return TypeWithComplexInit(b)
}
}
}
I think it depends on the kind of contract, is it a verification of required arguments before computing on them, then you got an IllegalArgumentException in case there was a validation error.
If, however, the kind of computation was wrong, i.e. the states after it are not satisfying, then an IllegalStateException would be more appropriate.
It is somewhat true that illegal state exceptions shouldnât exist as those can be, in theory, replaced by IllegalArgumentExceptions which check all possibilities for emerging illegal states.
But as it already sounds, this is practically unfortunate to impossible to hold on in productive software development even with fancy type systems.
IMO, thereâs not much difference between the two. When I think about exceptions, I first think about how they should be handled. For example, if an exception signals an unexpected external error, such as running out of disk space, pretty much the only thing you can do is to show an error message (perhaps not even log it, since thereâs no free space). There are also expected exceptions that shouldnât even exist in a well-designed programs, but sometimes you just have to do it. For example, a 3rd party library may throw an exception in a situation that is well expected in your software. In such cases, it makes sense to catch that exception as soon as possible and convert it into something more useful for flow control, such as a result-like object.
But when it comes down to things like IllegalStateException and IllegalArgumentException, they usually indicate a bug in software. It doesnât really matter what kind of bug it is, as long as there are enough details in the error message. There are few exceptions to this rule, such as NumberFormatException, but thatâs just bad library design.
Hereâs an example that blurs the line between them even further. I designed a result-like class Outcome to avoid abusing exceptions and to have a neat way to organize code around expected errors. Sometimes, however, you know in advance that a certain call canât possibly produce an exception (for example, Integer.parse(s) would never throw if you pass in a string that was previously matched against \d{3}). So I eventually came up with a test like this:
val result: Outcome<MyClass> = MyClass.parse(s)
val value: MyClass = resut.get()
Now, parse() can return either a failure or a success. And get() must throw an exception if called on a failure. What exception? Well, itâs an indication that the state of the result is wrong, so it must be an IllegalStateException, right? Seems so. But hereâs another example:
fun tryParse(s: String): Outcome<MyClass> = // ...
fun parse(s: String): MyClass = tryParse(s).get()
Now I have two functions: one should be used when I donât know in advance whether the argument is valid, the other one is to be called I do know that. But with an implementation like this, parse(s) will still throw an IllegalStateException on an invalid argument! This doesnât make any sense because the Outcome instance is now purely an implementation detail of parse(). So, should I catch the exception in parse() and convert (or wrap) it into an IllegalArgumentException? One would think so, but it would just lead to unnecessary boilerplate while providing little to no value to the API user. Even if I were to encapsulate that boilerplate into something like getOrThrowIllegalArgumentException() and getOrThrowIllegalStateException() that would still be rather ugly.
Eventually I just decided to let IllegalStateException to propagate, and just never write catch (IllegalStateException) or catch (IllegalArgumentException).
Thanks for all the thoughtful answers! It seems that most of you favor the IllegalStateException. And it seems that it may have been a better design decision to not make a distinction between these two exception types in the first place.
Now Iâll be a bit contrary to all of the other answers (while noting that this extends on a lot of the previous suggestions), but I do notice multiple points here. Letâs start by the exceptions, because IMO exceptions are almost always a bad design. Same goes with null. From your example I donât know why s is nullable, if C is from something out of your control or if itâs a class youâre designing. So letâs dive into the first case.
You are in control of s and thus also C. Then my first question would be is why is c even nullable?
Nulls are still error-prone even in languages like Kotlin, although admitted a lot less so than in e.g. Java
The APIs for nullable types are inconsistent - most APIs use .map for monad/monad-like types, whereas nullable uses ?.let
Worst null still does not communicate, what does it mean? Not found? Optional? Etc.
In Kotlin nulls are usually extremely to avoid, while improving the communication. Even better the same design techniques should solve your Exception-issue. Depending on the use case the two most solutions in my experience are:
sealed class ProfilePicture {
class NoImageSet: ProfilePicture()
data class UrlProfilePicture(val url: String): ProfilePicture()
}
and when using it:
when (user.profilePicture) {
is NoImageSet -> autogenerateAvatar()
is UrlProfilePicture -> urser.profilePicture.url
}
That way you communicate the optional nature of things, and the reason behind it.
The second is where itâs some kind of expected error case, such as a domain error. What lacks here is a proper Either type that communicates what goes in inside the domain. Either as the name implies is similar to a pair, except only one value will be present at a time. So it is either Either.Right or Either.Left, but never both.
That way you can communicate your intent much clearer (by convention the error is left):
interface LoginService {
fun login(credentials: Credentials): Either<LoginError, User>
}
And usage
val user = loginService.login(credentials)
when (user) {
is Either.Right -> showPage(user.b)
is Either.Left -> when (user.a) {
is LoginError.UserNotFound -> showUserNotFound(credentials.userName)
is LoginError.WrongPassword -> showWrongPassword()
is LoginError.Banned -> showUserIsBanned()
}
}
In short designing new code with nullables is almost always (if not always, still havenât seen a good use case) a bad design choice in new code. Nullable types are a necessary evil to deal with the mess we have from the legacy Java world and formats (e.g. json) that lacks the expression power to do better.
Also another note I make is that youâre using extension methods. Long story short, extension methods have a tendency to result in a poor design. Just like nullable types in my opinion a necessary evil for dealing with poor code/legacy code outside your control, so far the only good use case Iâve seen is dealing with legacy classes without a proper interface.
Otherwise things like decorator pattern, delegates, and similar designs (better structure too) provide much better alternatives.
Some of the issues I noted about extension methods:
They are harder to reason about
They are not located with the class they are extending making the code messier
If the code uses dependencies, your DI/IOC designs is likely to go out the window, or will need you to leak knowledge of the DI framework into your domain (yuck, your domain should only know about⊠yeah the domain)
It doesnât take much complexity to make them hard to work with, and for instance refactoring the complexity out puts all sorts of issues of how to do so in a nice way (e.g. with encapsulation etc)
Even if decided to use with good reasons, Iâd hesitate for pure human factors, a new developer does not know the considerations and may blindly imitate the design
Iâll probably make a more thorough post about these things later on, but basically my advice is, avoid nulls, Exceptions for expected errors - especially domain errors, and avoid extension methods. Instead there are usually better design-options out there
You are referring to a use case requiring to consider two branches while nullable operations referring to use cases where one branch of code is chosen when certain non-nullability conditions are met.
The functional equivalent of safe call operations is mapping functions over Option values yielding Option.None in case something was None/Null in the chain.
Meh, this isnât even readable, why not Either.LoginSuccess->... and Either.LoginFailure?
Why?
They represent some neat sugar for functions expecting the instance of the class they extend as first argument allowing them to chain like normal class methods which is far more practical than simply composing static utility methods in Java.
Really?
Just to state my assumption, you are the functional Guy here?
I think strongly following the functional paradigm in a multi paradigm JVM language exhibiting many commonalities with Java isnât the way to go for, but you can try it.
I think Scala suits better for the functional paradigm, they also donât appreciate nullability that much and prefer Option<T> more than T?.
Yes, and the other case right below it to which you also answer. One valid point that I did not focus that much on in my response is that Either does support nice (and consistent!) mapping, furthermore it does support flatMap which nullable types doesnât trivially support.
If designing the Either yourself and for each case that is indeed a valid point However, in practice I would use something like the Either from Arrow-KT or similar, which has to design for the same issue. So your question is a bit like asking why Pair is using first and second
On the readability I completely disagree, the only reason one would find that unreadable is habit. I taught that structure to my junior developers in my current project and they caught on to it in minutes, and one they had used it a few times it was completely natural - a lot more so than ?.let, ?: and all the other ? notations.
The true power does not come from Either.Right or Either.Left which indeed in a way just becomes another notation for the same deal. The power comes from the Left part containing an explicit value making it clear and communicative on what the missing value means. null means nothing apart from whatever implicit interpretation one puts over it, which almost always turn overloaded (in repositories it means not found, in models itâs optional/not set yet/etc.)
Here is a clear and meaningful communication that is easy to expand with multiple meanings, and due to the niceness of sealed class have a finite interpretations, thus no more exceptions for control flow (now exceptions are for the truly exceptional cases - as should be), no more forgotten exception handlers etc. Basically put your business logic in front.
Nullable types are nice for dealing with legacy frameworks, however, not matter how much you polish it, nulls are still a mistaken design that should not have been there. Now with the expressive power of Kotlin, itâs easy to actually design properly instead of using a structure with ambiguous and implicit meanings. Over and over we learn itâs better to consider programming a language rather than code, this observation have been made a multitude of times. Latest trend is in DDD, but itâs by far not the first time this observation has been made. So basically if you mean ânot foundâ, well then tell me, instead of sending a null and expecting me to guess it (and please donât use the JavaDoc argument, if I need to check that to understand your interface itâs too complicated).
I already provided a few reasons in my post. And I didnât even mention testability, how sloppy of me but be careful, I see a lot of embracing of extension functions lately - seems like every kid on the block wants to play with the new toy. I did try KTor, and my experiences were basically, it maked proper DI/IOC design cumbersome, I basically have to pass my DI around - the point of DI is that the objects do not know what they use. It makes slightly more complex structures harder to write, e.g., if I want a helper function it either has to be in a separate shared object (not sensible if I donât want to share it - encapsulation) or it always have to be before usage, because there is no class etc. There are a lot of small details over and over again, to the point that Iâm wondering if Spring for the endpoints is actually a better design for the endpoints, since itâs proper OOP, albeit with some nasty annotations/reflection logic.
Actually my original background is Java, but yes I do have strong a functional background too. Why are you stating that like itâs a bad thing? Keep in mind that the most major and game changing improvements over the latest years actually come from functional programming.
That is said my point of view on a lot of these things are shared by a lot of more pure Java/OOP people, the major difference is that some of my solutions are in part taken from the functional paradigm. If you read Clean Code, Effective Java, Domain Driven Design, Elegant Objects to mention a few, the same patterns of thinking will emerge. Exceptions should only be for exceptional cases, not domain logic, do communicate your domain, avoid nulls etc.
Both are bad IMO, Option<T> is marginally better, but so marginally that itâs close to pointless. Both designs suffer from the communication weaknesses, and the only benefits Option gives is a more consistent design, and that you are forced to consider if a null is an error or expected
Nulls get used in a variety of subtly-different cases: when a value is not initialised, not available, not applicable, not known, secret, when a default should be assumed, as the result of a bugâŠâ How does Left distinguish between those, and what sort of value would it hold to do so?
Iâm guessing it depends on the context.â Which would not make it significantly better than null.â (And arguably worse, because at least most of those meanings of ânullâ are closely related to its everyday meaning, which âleftâ certainly isnât.)
I appreciate that an Either is simpler theoretically, needing no language support.â But Kotlin is not a minimalist language; itâs pragmatic (as illustrated by some of its syntactic sugar).â And Iâd contend that in practice, nullability is used sufficiently often and differently from other constructs as to justify treating it differently.â Yes, it means a couple more operators, and complexity in the type system; but it makes for code thatâs a lot clearer, shorter, and more intuitive to those experienced with it.â And being able to treat nullable types as supertypes of non-nullable ones is quite powerful.
(After all, if elegance and purity were all a programming language needed, then arguably there wouldnât have been any functional languages after LISPâŠ)
Many valuable ideas and techniques have spread from functional programming; but âmostâ?â You seem to be overstating the case a little.
And Iâd argue that many functional ideas are copied a little too slavishly.â If I recall correctly, Martin Odersky, creator of Scala, pointed out that while much of the functional side of that language was new to many developers, heâd actually made very few developments there and most of the innovations were on the object-oriented side.
(For one unimportant but symbolic point of tradition, look at how the keyword for defining a function has begun with def all the way from LISPâs defun and Schemeâs define down to Scalaâs def â where it never made any sense to me that you âdefineâ a function but not a propertyâŠâ Kotlin, though, wasnât afraid to break with tradition, and now fun sits well next to var and val for making clear what youâre defining.)
Eh?
Kotlin has no problem with top-level functions being declared earlier in the file that theyâre used, or later, or in a separate file in the same package or a completely different package.
While very interseting this has nothing to do with the original question. Could we move the discussion about Null vs Either and the pros and cons of functional programming into itâs own topic?