returns
returns copied to clipboard
Think about typed do-notation
I have removed @pipeline
in 0.14
release.
It had lots of problems:
- It was not working properly with
Failure
types: #90 - It had problems with unwrapping complex values like
IOResult
andFutureResult
- It had problems unwrapping containers of different types (you were not allowed to unwrap both
Maybe
andResult
in the same@pipeline
- It was encouraging imperative code in a functional codebase
I would love to have some solution instead! But, no ideas about how it should work or look like. So, any ideas are welcome.
Related: https://github.com/papaver/pyfnz/blob/master/pyfnz/either.py#L144
FWIW... Parsy (parser combinator library) has a way of mimicking do
notation using generators and yield expressions:
https://parsy.readthedocs.io/en/latest/ref/generating.html#motivation-and-examples
their example:
@generate("form")
def form():
yield lparen
exprs = yield expr.many()
yield rparen
return exprs
if roughly equivalent to:
form = do
lparen
exprs <- expr.many()
rparen
exprs
...where form
, lparen
, rparen
and expr.many()
are all Parser
monads
the correspondence works well enough, in this specific domain, that I was able to translate a bunch of code from Haskell's Megaparsec library into Python in a fairly straightforward way
usefully the generator/do
form is not required, you can still use these objects in ordinary expressions like myparser = form | (lparen > other_token < rparen)
I don't know how similar or different this is to what you tried already, or if it's not useful, but if it's helpful at all then I'm glad 😄
@anentropic I have tried this approach. It works fine. But, it is impossible to type it properly.
Because, generators require to use Generator
or Iterator
return types. And these types require a specific generic parameter, ie: Generator[_SendType, _ReturnType, _YieldType]
When this parameter is specified, it becomes impossible to use other types inside your do-notation.
Currently I am thinking about this API:
assert do(
x + y
for x in wrap(Some(1))
for y in wrap(Success(2))
) == 3
But, this API still does not work properly with IO
values. How can we unwrap
IO in this comprehension safely?
It does mean that for z in wrap(IO(1))
should return raw 1
value, but the result of do(...)
would have IO
type on top of it.
Ideas?! 💯
@papaver since I feel very inspired by your implementation I would love to hear your opinion and ideas about it! 🙂
Related: https://github.com/papaver/pyfnz/blob/master/pyfnz/either.py
I am curious, I didn't fully understand the typing issue with generators+yield expressions.... and this looks like a generator expression as an argument to a function. Does it not have the same problem?
for x in wrap(Some(1))
seems kind of a cumbersome/weird way to extract a single value, compared to x = yield Some(1)
(or maybe it needs to be wrapped... x = yield wrap(Some(1))
)
(none of this is intended as a criticism, I am just curious about the issues involved)
@anentropic maybe I got your idea wrong. Can you please send a simple working demo?
Related: https://github.com/dbrattli/OSlash/pull/12
https://github.com/gcanti/fp-ts/issues/1262
@sobolevn i ended up going the generator route as it resulted in the cleanest code while still looking like python. all the other implementations i ran into were fairly gross looking and convoluted to force the code to do something it wasn't meant for (like the decorator with dozens of yields). the for comprehension was was able to do a form of 'lifting' for free, cleanly.
i actually use that pattern in code all the time now. most people just don't understand the power of the for comprehension.
# run validator on cut data, convert errors into issues as well
def tryValidation(validator, cutData):
idx, cut, take, shot = cutData
def toIssue(e):
return {
'issue' : 'ValidationIssue',
'cut_idx' : idx,
'shot' : shot and shot.code or None,
'take' : take and take.code or None,
'validator' : validator.__name__,
'error' : repr(e) }
return Try(validator, cutData).recover(toIssue)
# keep track of all the issues found
issues = []
def validateCutlist(cutlist, validators):
[issues.append(issue)
for validator in validators
for cutData in cutlist
for issue in tryValidation(validator, cutData)
if is_some(issue)]
personally i think typed python is a step backwards so i haven't messed with it. python is extremely powerful functionally without types.
We can also try to use something similar to Elixir
's notation with with:
Like so:
with (
result as success_value,
needs_raw(success_value) as returns_raw,
):
print(returns_raw)
This is maybe obvious and the reason you chose it, but I like with
and as
because its consistent with how with is used here:
with open(file, "w+") as f:
...
which is also consistent with how Resource is used in scala cats and other monadic behavior. I'm not crazy about the needs_raw notation though and it would make sense to stick with bind
to indicate flatmap like:
with (
fetch_api_keys() as apikeys
bind(get_user(apikeys)) as user
bind(get_settings(apikeys)) as settings
get_local_config() as local_config
do_stuff(user, settings, local_config)
) as final
@sobolevn any other thoughts about this? Is anyone working on this yet?
Nope, this issue is not in the works right now. I like the generator expression approach:
do(
x + y + z
for x in c(Some(1))
for y in c(Some(2))
for z in c(Some(3))
)
With the support of async generators for Future
.
Do you want to work on it?
https://github.com/oleg-py/better-monadic-for
Currently I am thinking about this API:
assert do( x + y for x in wrap(Some(1)) for y in wrap(Success(2)) ) == 3
But, this API still does not work properly with
IO
values. How can weunwrap
IO in this comprehension safely? It does mean thatfor z in wrap(IO(1))
should return raw1
value, but the result ofdo(...)
would haveIO
type on top of it.Ideas?! 💯
A couple of thoughts here:
- I don't believe you can typically mix different types of monads in the same do block (in pure functional languages that enforce strong static types). This is the problem that monad transformers solve. In this particular case, you'd probably just convert your Maybe value into a Result value and use 2 Result values in the do block.
- I think the result of
do(...)
should be wrapped in IO. 3 is a pure value, butx + y
happens in the impure IO context. Once you enter IO, you cannot get back; you just have to keep working in IO. - what is
wrap
? Is it just a way to turn a monadic type into a generator? I wonder if there's a way to bake generator functionality into the monadic types themselves so you could avoid thewrap
function... - I think using generator expressions for do notation is a really cool idea. I don't know if they're equally powerful, but this reminds me of LINQ in C#. Language-Ext is a comprehensive C# library that uses LINQ for do notation, which could be a handy reference for this work.
Illustration in Haskell:
( do
x <- note "maybeX was Nothing!" (Just 1)
y <- Right 2
return (x + y) ) == Right 3
https://replit.com/@bagrounds/do-notation#main.hs
related https://github.com/internetimagery/do-not