https://kotlinlang.org logo
Title
d

dave08

01/22/2018, 8:12 AM
What's the accepted practice for not found situations in repos and apis? Say I have
userRepo.getUser(userId)
and it wasn't found, should it return null, throw a domain exception (NoUserFoundException) or have a sealed class with a null object? (I'm using coroutines, not Rx...)
👍 1
s

spand

01/22/2018, 8:17 AM
Not sure what "best practice" is but we have both getUser and findUser. getUser throws if not found and findUser returns User?. I find that it is often the caller that has be best idea of what to do
1
Also, getUser can be implemented as a default method so it is not much more work to give the caller this flexibility
c

Czar

01/22/2018, 8:20 AM
If the caller is Kotlin, your signature should be
fun getUser(id: YourIdType): User?
and you return
null
when User can't be found; if the caller can be Java, I'd go with
Optional<User>
return type. Throw is bad practice here, because you're forcing try/catch semantics when your caller might be okay with not finding a user.
I've also seen what @spand proposes:
requireUser/findUser
or whatever you call these two methods, but this seems superfluous to me, as it just complicates the API. If you're not working in an extremely performance-constrained environment a simple null/optional check of a return value should not be an issue and simpler approach (one less method) wins in my book.
s

spand

01/22/2018, 8:30 AM
The purpose is to rid every requireUser callsite of a ?: error("Missing user with id $id"). Much like kotlins Map.getValue. But each to his own 🙂
c

Czar

01/22/2018, 8:41 AM
Yup, opinion only, it also really depends on usage context. If
?: error("")
is something you'll find yourself writing more often than (
?.let {}
), then by all means
get/find
(where
get
throws and
find
returns
null
) is indeed a better approach. I'm probably just a bit biased due to the latest project I worked on, which didn't have many
?: error("")
situations, if something wasn't found it didn't mean an error has happened. 🙂
d

dave08

01/22/2018, 8:52 AM
Thanks alot for the insights @Czar and @spand! When would you use null objects/sealed classes? But then, maybe I'm asking from a context of translating code from Rx, I guess in coroutines it would be simpler, unless using a channel like in Rx where you have to keep the channel open and constantly representing a current valid state... here this might not apply.
c

Czar

01/22/2018, 8:56 AM
I wouldn't because I'm not very familiar with the concept, I think it comes from more functional approach, with which I'm not nearly fluent.
a

Andreas Sinz

01/22/2018, 9:25 AM
@dave08 I usually follow this rule: https://stackoverflow.com/a/19061110/4706743
Unchecked exception = exception; checked exception = Null/Sealed class
d

dave08

01/22/2018, 10:18 AM
@Andreas Sinz Very interesting addition, thanks! We all think about this more or less, but its easy to overlook when under pressure...
c

cedric

01/22/2018, 2:48 PM
If your function can either throw or return
null
, I've seen
findUser()/findUserNoThrow()
or
findUser()/findUserOrNull()
. This is more useful in libraries where you want to give your users options.
d

dave08

01/22/2018, 3:30 PM
@cedric You're suggesting that the throw version should be the default? Our use case is not in a library, but rather in the model/networking layer of our internal projects.
c

cedric

01/22/2018, 3:31 PM
To be honest, I don't know. I don't think there is a single answer, it depends a lot on the scenario.
Absent values are not always an exceptional situation.
👍🏼 1
r

raulraja

01/22/2018, 8:54 PM
As a rule of thumb I would not use
?
or
Option
to denote User not found if you want to remain exception free. Does it have the same semantics to not actually finding the user to say the network is down? I'd model the exceptional known cases in an ADT and return a disjunction of a known error case or the actual value:
sealed class ApiError
object NetworkFailure: ApiError()
data class UserNotFound(val id: String): ApiError()

fun findUser(): Deferred<Either<ApiError, User>> = TODO()
You loose valuable types and semantics when modeling known cases with just
?
or
Option
and you may need those down the road to display appropriate error messages or branch out biz logic into the unhappy path. If you are interested in an FP approach to error handling more info here: http://arrow-kt.io/docs/patterns/error_handling/
t

tschuchort

01/22/2018, 9:03 PM
imo the function should never throw if the input is legal, i.e. the user not being found is an expected result of the function. That possibility should be encoded in the function signature either through nullable types or something like
Maybe
. I prefer nullable types for non-rx functions because they're nicer to use.
?.
is like
Maybe.flatMap
anyway
👍 1
c

cedric

01/22/2018, 9:07 PM
@tschuchort I wouldn’t go that far. Just because the input is legal doesn’t mean the function can handle it (i.e. be total on that input)
Really the first questions are: 1) Is this situation exceptional? and 2) Do you care about equational reasoning? Based on the answer to these questions, you can engage on various paths
☝️ 1
d

dave08

01/22/2018, 10:46 PM
@raulraja That's part of what I was doing in Rx... but I think that it might be adding an extra layer of complexity... I think that you are right in certain cases though. I'll note the link anyways, we could always learn something 😄 @cedric #1 is understood, but what's equational reasoning?
c

cedric

01/22/2018, 10:48 PM
@dave08 The ability to equate your type signatures with logical proofs, which can give you a certain amount of confidence about the soundness of your code. A “certain” amount. There’s still runtime code that’s not accounted by this static analysis.
If you throw exceptions, you can no longer establish that correspondence
r

raulraja

01/22/2018, 10:54 PM
To add to @cedric's comment, the way I see it is that Equational reasoning is a property of programs declared in terms of total and pure functions. Pure: They produce the same output for the same input and do not change their outter state or perform uncontrolled effects. Total: As implemented these functions contemplate all cases in which their input may be found itselft into, for example when you use
when
over a sealed class hierarchy). An example of pure and total functions are:
fun add(a: Int, b: Int): Int = a + b
And example of an impure one:
var x: Int = 0
fun addToX(a: Int): Int = x = x + a
An impure and partial function because it performs throwing an exception which is an effect and it's input it's not defined for all cases:
var x: Int = 0
fun addToX(a: Int): Int = x = if (a > 0) x + a else throw IllegalArgumentException("value expected to be positive")
d

dave08

01/22/2018, 10:58 PM
Why should throwing be any different from a
return
in the middle of
fun
logic? Also, even exceptions can be unit tested...? @raulraja You still needed to use an external
var x
to make it impure, the exception didn't do it, did it?
r

raulraja

01/22/2018, 10:58 PM
the exception did it too
mutating external state and throwing exceptions are both types of side effects
c

cedric

01/22/2018, 10:59 PM
If you want a quick peek at what it means: When you read code and you see something like
F => T
, you will typically think “It’s a function that takes a parameter of type
F
and returns a parameter of type
T
. Here is the cool part: now read this from a mathematical eye and you’ll read “F implies T”. There is actually a mathematical equivalence between these two interpretations: they are strictly equivalent according to a theorem that was proven decades ago.
🔝 2
r

raulraja

01/22/2018, 10:59 PM
@dave08 yeah you can test anything
but pure functions don't need to be mocked
There are testing benefits to purity and total functions too. In general to anything that is predecible and encoded in types. Less surprises at runtime.
d

dave08

01/22/2018, 11:01 PM
Retreiving data from network or a db is intrinsically not 'pure' too...?
r

raulraja

01/22/2018, 11:02 PM
right unless encapsulated in a data type that can suspend that computation
for example this is pure:
c

cedric

01/22/2018, 11:02 PM
@dave08 To answer this question, ask yourself: “Is it possible to call this function with the same parameters twice and receive different results?“. If the answer is yes -> impure
r

raulraja

01/22/2018, 11:03 PM
fun printLater(): () -> Unit = { println("Hello pure") }
c

cedric

01/22/2018, 11:04 PM
But purity is a necessary but not sufficient condition: your function can be pure but have side effects, something that’s undesirable if you want to stay within the realm of equational reasoning
r

raulraja

01/22/2018, 11:04 PM
That is
pure
because
printLater
performs no effects, it just returns a function that does.
d

dave08

01/22/2018, 11:07 PM
@cedric So, back to the original issue, using sealed classes andd Either to represent the return value of getUser, would make my function closer to being pure?
But not really pure
c

cedric

01/22/2018, 11:08 PM
Usually, the types you choose to return have no impact on whether your function is pure or not. What matters is what’s inside that function (the runtime aspect).
r

raulraja

01/22/2018, 11:13 PM
I think moving the effect of absence or error handling to the type guarantees that callers to your function deal with that case because it is returned as value. If you throw you have no guarantees your users will handle the exception.
c

cedric

01/22/2018, 11:13 PM
Except if you throw a checked exception…
Also the constraint of returning the error instead of throwing it is that the caller needs to deal with it or pass it to its own caller manually, thereby reimplementing manually what exceptions give you for free
Or do like Go and just ignore it completely 😄
d

dave08

01/22/2018, 11:15 PM
Ooh, now I think I got it! Side effects shouldn't be brought in to a pure function, but a pure function might have side effects... that's not desirable. @raulraja So the goal here is not for pure
fun
, but rather for total
fun
?
r

raulraja

01/22/2018, 11:15 PM
but Kotlin code would not have to handle the checked
@Throws
exceptions right?
c

cedric

01/22/2018, 11:16 PM
@raulraja Yeah Kotlin will happily ignore it
r

raulraja

01/22/2018, 11:17 PM
A pure function is not pure if it has side effects not captured by it's types. Try to write one. Pure functions produce no side effects when invoked, that means no change to the world state outside the function.
t

tschuchort

01/22/2018, 11:22 PM
One important aspect of purity is that you can see by the types what the function does. If your function throws, you can not see that in the function signature, but if you use Either then it's clear to everyone that the function might fail and what kids of errors it might return. In fact you will be forced to handle all possible results, which will drastically reduce the amount of bugs in your code
🔝 2
c

cedric

01/22/2018, 11:23 PM
@tschuchort On the JVM, the throws is part of the signature
and on
Either
, the compiler will only keep you honest if quite a few conditions are met (sealed type, using when, etc…). Still plenty of opportunities to ignore the error part of
Either
.
d

dave08

01/22/2018, 11:24 PM
@cedric That was my original thought (actually I remember vaguely seeing you mention it in this slack...). @raulraja i guess it all depends on how important equational reasoning is in each case like @cedric put so well, and you both gave me more valuable insight on this, these concepts are nice additions to a programmers toolbox 😄 One last little thing, why doesn't Kotlin enforce @Throws?
👍 1
c

cedric

01/22/2018, 11:25 PM
Because checked exceptions don’t work well with composition: `foo().bar().baz()`: if any of these has a checked exception, it gets ugly fast
d

dave08

01/22/2018, 11:27 PM
That's not particular to Kotlin... also the exceptions will still through and the developer won't know what hit him if it's not enforced...
r

raulraja

01/22/2018, 11:29 PM
and back to my point, exceptions are generally not desirable unless you are in an environment that requires them
Exceptions are also very costly unless you override not getting the stack trace compared to passing values.
c

cedric

01/22/2018, 11:30 PM
The cost factor is incorrect and not worth bringing up in my opinion
as for your first point, I disagree but it’s ok 🙂
r

raulraja

01/22/2018, 11:32 PM
I'd love to understand why is incorrect, you mean this is not accurate http://normanmaurer.me/blog/2013/11/09/The-hidden-performance-costs-of-instantiating-Throwables/ ?happy to continue in DM. I'd like to understand if they that is wrong, other post I've read seem to agree.
c

cedric

01/22/2018, 11:33 PM
Instantiating is cheap. Throwing is more expensive but it should be rare. Either way, you’ll probably never find anything related to exceptions in the top 10 lines of a profiler dump.
r

raulraja

01/22/2018, 11:37 PM
You may not find those as often as IO in DBs or network but the performance of instantiating the Throwable is directly affected by the size of the current stack and calls into native code so it's platform dependent and people abuse them to signal control in performance sensitive code frequently without understanding this because it's an implementation detail hidden in the super constructor.