-
Notifications
You must be signed in to change notification settings - Fork 81
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Issue #149] add generator-based do notation #150
Conversation
52c6743
to
65894a3
Compare
Codecov Report
❗ Your organization needs to install the Codecov GitHub app to enable full functionality. Additional details and impacted files@@ Coverage Diff @@
## master #150 +/- ##
==========================================
+ Coverage 97.37% 97.41% +0.04%
==========================================
Files 3 4 +1
Lines 419 464 +45
==========================================
+ Hits 408 452 +44
- Misses 11 12 +1
|
Hi @francium (+ @wbolster), I got this to pass linting, type checking and tests in 3.8-3.11.
It is well-tested in 3.10-3.12. Further, it also passes strict type checking with pyright, so I'm pretty confident in the correctness. This is a strange but convenient and well-behaved syntax for simulating Added a dev section to README and updated My organization is starting to use the library and this feature would make my life much more convenient. It's a great selling point for the library for FP Python users. Please take a look, thanks. |
Thanks, I'll try to allocate some time later today to take a look through this |
We should add an easy to understand example in the README. So something like this would be a canonical example of how to use this? @do
def _result_sum(is_success1: bool, is_success2: bool) -> DoResult[int, str]:
x1 = yield _get_result(is_success1, 1)
x2 = yield _get_result(is_success2, 2)
return Ok(x1 + x2) Obviously, this is quite nice. One thing to think about is this forces the user to create a function for this. So it's a bit heavy weight vs an expression which you can inline anywhere -- the idea behind #112 Although maybe it would be good to have both? |
@francium , sure I can add an example like that. It follows the look of the Rust or Haskell equivalent fairly closely, with <- notated here as = yield, so we can borrow any of those examples. I didn’t think about inlined expressions, and it might be possibly with anonymous generators - I don’t know. I do think this syntax is more intuitive than ‘for…’ expressions following the final value. I also think it’s better to pick one syntax even if it means not supporting inline do. For me, one premise of do notation is that it is convenient exactly for more complex expressions, which would usually justify a named function. Otherwise you can just manually use ‘and_then()’ if it’s just one or two calls. I’ll look into anonymous generators a bit. |
@JLessinger anonymous generators is the basis of #112's implementation I believe. Currently, I'm inclined towards providing both ways of doing it, a decorator for functions so you use do Since the two implementations, decorator and expression, wouldn't really interfere with each other, right now I can't think of a good reason for us to omit one of them and just provide the other. Naming would be interesting of course, we can't name both |
@francium , ok, why don’t I rename -> named_do and we ship this. It won’t interfere with 112, so someone can push that through on top of this. |
hmm, yeah, I'm fine with that. But let's think of a better name, |
Can you please also look into how async functions will fit into this decorator based approach. And add test cases/readme examples for those. Thanks |
Maybe rename it to |
Re: naming, how about |
Let's just go with |
I just realized this has a big limitation: you can only contain one type. This will have a type check error:
because of the generator type, T has to evaluate to both str and int. I can basically suppress this by allowing the generator to accept and yield Any, but then you lose a lot of the type safety. I don't know how 112 addresses this kind of problem, but we should probably hold off. |
#112 is based off of what |
5e602e0
to
60104c9
Compare
@francium , I seem to have been able to get it to work with the Mypy is happy and it seems to play nicely with pyright in strict mode, which is much stricter than default mypy. |
Thanks @JLessinger, I'm gonna be a bit busy this weekend, so I may be delayed in reviewing this. I'll try to get to it as soon as I can. |
3530359
to
78aa883
Compare
Hey @francium , just to clarify, this is a fairly straight-forward specification and implementation pretty similar to the returns library's. I agree with you and them that it's convenient to inline this kind of expression, and actually it's kind of Pythonic: it's basically a Result comprehension. Mypy and strict pyright pass. Tests for usage with regular and async functions pass. Any changes needed to ship? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good, just a few minor changes are needed
Updated, let me know if this is good to merge and ship (I don't have write permission) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes look good.
However, I've just noticed one potential issue.
When you have,
out: Result[float, int] = do(
Ok(len(x) + int(y) + 0.5)
for x in resx
for y in resy
)
This looks fine, but if you remove the type annotation,
out = do(
Ok(len(x) + int(y) + 0.5)
for x in resx
for y in resy
)
The out
's type is inferred as Ok[float] | Err[Unknown]
(aka Result[float, Unknown]
.
Small thing, but can you look at returns
to see if their generator also suffers from this issue or if they're doing something to get around this?
From what I can tell, the generator expression is the root cause, because it's type is inferred as Generator[Ok[float], None, None]
, but it should really be Generator[Ok[float] | Err[int], None, None]
to get proper typing here.
As in,
out = do(
cast(
Generator[Result[float, int], None, None],
(
Ok(len(x) + int(y) + 0.5)
for x in resx
for y in resy
)
)
)
But expecting the user to do a cast
is pretty terrible from a usability perspective.
The docs also currently don't highlight the need to have an explicit type annotation to get proper typing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears as though returns
also suffers from the same issue,
import random
from returns import result as RS
def get() -> RS.Result[int, str]:
if random.random() > 0.5:
return RS.Success(random.randint(1, 10))
else:
return RS.Failure("something went wrong")
A = get() # Result[int, str]
B = get() # Result[int, str]
x = RS.Result.do( # Result[int, Unknown]
a + b
for a in A
for b in B
)
I'm fine with leaving this in if you'd want to investigate this or think about solutions separately from this (please open an issue if you'd want to do that). Or if you want to investigate prior to merging, up to you.
But let's update the docs first to highlight this limitation and the workaround of adding an explicit type annotation on the returned value of the do
call.
Good find. It might be possible to fix this, but I don't think it's at all straight-forward. We might have to do something like parametrize both Ok and Err by both T and E. I think the convenience of |
Updated docs |
713035b
to
d1424b2
Compare
Addressed comments |
fix |
I'll try to make a release as soon as I get the chance, either today or tomorrow |
Apologies for the delay. This is now released in 0.15.0. |
Proposal for implementaiton using generators to simulate <- notation as in Rust (or Haskell).