Btw the quoted text is not 100% accurate: Try can ...
# arrow
j
Btw the quoted text is not 100% accurate: Try can yield pure functions, just not any useful ones. Try with pure functions is itself pure, it will just always succeed and thus not be useful at all 😉
s
What about
Try { "a".toInt() }
j
"a".toInt()
has the type
fun String.toInt(): Int
which would mean it would have to be defined for all strings possible. But it isn't as not all strings can be converted to ints. Thats a partial function and partial functions throw exceptions on unhandled input and are thus not pure. Throwing an exception is an action not indicated by the return type => definition of a side effect.
fun Sring.toInt(): Int
is much better if it has either
Option<Int>
or
Either<SomeDescriptiveError, Int>
(or similar) as a return type. Coming back to
Try
since
String.toString(): Int
is impure and
Try
does not suspend that side effect, the invocation of
Try
is by definition also impure.
But also: This use of
Try
is the main purpose it was added for (as a simple wrapper around try catch), so using it that way is also not too bad when no other side-effects are involved. It's just that kotlin cannot prove that with it's type system, making this a bad abstraction because it's too easy to use it wrong. I usually follow two paths when working with exception throwing functions that I have no control over: Wrap it in suspend because it might be doing side-effects or use try catch directly and convert to option/either for code that I know is otherwise pure. It's quite useful to define an extension function for that. The only reason arrow does not provide this is because it's again, like
Try
, an abstraction that promotes misuse and we try to avoid that in arrow.
s
I disagree 🙂 A pure function is one that returns the same outputs for the same inputs, and doesn’t have side effects. Parsing a string into an int and returning a
Try
value is such a function. I don’t believe there’s any requirement for the function to be total. The reason I asked about this is because when people question why
Try
was deprecated, I think the better answer is not “because it’s not pure” but because “we don’t want to encourage people to catch IO exceptions” or “we don’t want to encourage strict evaluation of effects”.
j
Totality is required for pure functions as they otherwise would not be pure (they will throw on undefined input that makes them impure). And yes
fun a(str: String) = Try { str.toInt() }
is total and deterministic and thus anything returning
Try
is pure (if and only if there are no other side-effects apart from exceptions). That is not the problem with
Try
though. The problem is anything within it has to be impure to be useful, and that is not what arrow wants to promote. As I said in my second message, wrapping impure code that has no other side effects and that you have no control over, is exactly what
Try
is/was useful for and that's still how I use it (or an equivalent ext func to option or either). But as the deprecation message states: Try promotes use of impure functions (or something like that) and that is just bad in all cases. My original message is indeed wrong.
Try
is pure in all cases that only involve non-fatal exceptions (fatal exceptions are rethrown afaik). So a good answer to "why is
Try
deprecated?" is: "Because it promotes using and writing impure functions". The other part about IO or effects is also true, but not the only reason.
Sadly most libraries writting for the jvm world have an impure api, so to wrap it you either need
IO
or
Try
(or equivalent). If you know it has no side-effects other than exceptions (which is quite hard to figure out about code you do not own) then
Try
is perfectly fine, for all other cases use
IO
or equivalently suspend
s
I’d be interested as to why you think it has to be total to be pure, I couldn’t find literature to back that up. In general I agree with you about Try I just think the deprecation reasons could be better communicated by taking about IO
j
Because a non-total function throws on input it is not defined for
and throwing is a side effect => impure
s
Is it
j
Any observable state change outside of what the return type indicates is considered a side effect
s
You could argue that throwing an exception is a return type of sorts
j
and exceptions change state quite a bit by aborting computation
s
I don’t like exceptions anymore than you, I just think
a.toInt()
fulfils the rules of purity - output is always the same for the input and it has no side effects.
j
not strictly:
fun String.toInt(): Int
has no indication that it might throw, similar to
fun Int.div(i: Int): Int
both are lying signatures. You should be able to read it as: "for all strings I will get an Int"
but thats not true!
s
public Int toInt(String input) throws NumberFormatException
is that better ?
j
yes, in terms of return type it is, now it's a union of
Int | NumberFormatException
. It has lots of other problems but it is something I'd consider pure
s
The kotlin one is the same, it’s just not as obvious. My point wasn’t “throwing exceptions is sometimes ok”, it’s just that these examples are pure even if they’re poor examples of code style
The JDK should just return an Optional<Int> or whatever
j
wait where is the kotlin one the same? o.O
s
fun String.toInt(): Int
<-- it’s the same just the exception isn’t included in the return type, but it’s the same as the Java one.
j
Well yeah, might call the same function, but as long as the exception is not included in the type it is worse 🙂
s
It’s exactly the same as the Java one that you said is now ok because it’s a union. The Kotlin one is a union too. It’s awful, but pure.
j
Wait it does not mention the exception, why would that be the same?
s
We know the exception can be thrown.
j
From what?
Where does this knowledge come from? Not from the type
s
20 years of using it.
j
That sadly does not make it equivalent to the signature with
throws NumberFormatException
. It needs to be in the signature/return type to be considered pure
s
If you want to say that having possible errors encoded explicitly in the return type is a good thing. I agree. 100%. That’s why I always use functional error handling.
💯 1
So languages that do not declare the return type cannot be pure ?
const foo = (a) => a + 1
Is that pure
j
Hmm, yes and define is the wrong word to use as that example shows 🙈 Haskell can be untyped yet is pure for example.
s
So it doesn’t need to be in the signature/return type ?
So if you agree it doesn’t need to be in the signature, then the kotlin one can be pure
j
Yes and no, if the infered type matches what the correct return type would be fine! If you define
fun String.toInt() = toInt()
it's just as impure
s
It’s only impure if you’re going to define throwing an exception as impure
j
A throwing function cannot be pure unless the return type indicates it wether you specify it or not. If you let the compiler infer it it still has a return type
s
Not all languages have inferred types.
The only rules for purity that I can find are no side effects and output based only on input.
So that’s my original point. We’re better off saying “don’t use try because exceptions tend to hide bad things”
Let’s not say that every function that throws is necessarily impure even if they usually are
j
and the definition of side effect is "no state-change outside of what is indicated by the return type"
that is where throwing becomes impure
s
It’s just a value that’s passed around in a special way. It’s getting a bit picky otherwise.
j
What is? Exceptions?
s
Yeah
j
They break control flow and are not in the return type. They are quite a big deal imo
s
I agree they are a big deal and generally avoided !
As I’ve been saying
j
If I define
fun a(arg: A): B
I do not want this to throw ever
s
agreed
j
And by definition it should never in a pure setting
s
But if you have an existing awful function like .toInt that does throw, wrapping it in a Try makes it pure, even if it’s still shit
j
If you cannot change it, then yes. and as I said previously sadly there is lots of those out there 🙂
s
Do you agree that the real reason we’re saying avoid
Try
and things like that is because they tend to hide effects
j
Thats part of it
But also that it promotes using it on own code that throws
s
That’s a fair point.
The purity angle is not the one to use though. Let’s use those two points - Don’t use Try because it encourages the use of strict effects and because it promotes using code that throws
In other news, when are you going to merge propcheck into KotlinTest so we can have all the goodies 😂
j
Well
Try
is pure, code that is useful to use inside
Try
and has no other side effects is not. Btw thanks for pointing out that
Try
is pure, I had that wrong first. 🙂
I am currently playing around with integrated shrinking, if I can get a nice api for it I'll rethink it. But there are a few problems
not with the approach itself but with what it implies on writing tests
s
You want to be able to generate shrinkers ?
j
No and yes
s
lol
j
😄
whoops
one message at a time please slack
integrated shrinking is a concept where instead of generating a single value you generate a rose tree of values where the branches contain shrunk values
s
Well I’d love to get you contributing to KotlinTest on the property test stuff so if you ever fancy it let me know 🙂
What’s the advantage of that ?
j
The genius part of this approach (which the hedgehog library made popular) is that transformations to the value generated transforms all shrunk values as well
That means it retains invariants
s
That’s a nice feature
And something that isn’t even supported in quickcheck or scalacheck I don’t think ?
j
Also you never have to write custom shrinkers
Scalacheck has a smaller feature set than quickcheck and quickcheck cannot have that
different approaches to shrinking
It has some drawbacks tho, even apart from it having some implications on writing tests and generators
s
I will monitor your project, I saw your ticket for this
j
for example:
Copy code
val genA = Gen.fx {
  val length = int(0..100).bind()
  string().list(length)
}
this is a perfectly fine generator for creating lists strings, however when shrinking it will first shrink the length and then the elements inside the list. That means once it shrunk the content list it cannot go back to shrinking the length
That is very obvious when shrinking tuples, but there are solutions to that, those are just not as nice
s
Yeah I’ve ran into the issues when trying to apply a shrinker to a user’s map function
Gen.ints().map { … }
j
yes I saw the ticket discussing that ^^
But that is also for a different reason, mapping over gens with rose trees is perfectly fine
it's just monad operations like flatMap that fail
s
I’ll have to read up rose trees and how they would solve the mapping problem
j
Because in
flatMap
the result is a function, so to shrink it it has to first evaluate it, but you cannot go back after doing so
rose tree's on their own are just:
Copy code
class Rose<A>(val el: A, val branches: Sequence<Rose<A>>)
Getting them to work nicely with a decent api is quite a bit of work tho.
s
So it’s just a n-tree where each node has a value ?
j
Yes, and in terms of shrinking every branch represents a shrunk value with the first one being the smallest one and n + 1 being larger than n
s
Right
So it’s easy to locate the shrunk value for any given n
j
For any element of a rose the shrunk values are the elements in the branches, and the branches are ordered smallest to largest
don't know if that is what you asked 😄
s
yes
that’s what I was saying, it’s a neat solution
👍🏻
j
Yes, but the drawbacks with
flatMap
require quite a bit of work to overcome. The solution is two fold: Don't ever use it unless you need it, and if you need it either ignore that you have worse shrinking or add you own shrinking back 😕
Have you seen how Hypothesis for python works?
It's yet another completly different attempt at shrinking and generating
s
No
I’ll add that to my reading list
j
Quick summary is: Instead of using a random gen, it flips a coin for every random choice and keeps track of the results. When shrinking it shrinks this list of coin flips based on heuristics. Seems to work good enough, but I have my doubts. It's an interesting solution though
s
clever for sure