Cool library, though.
A sad day for this Erlang enthusiast.
y = (Maybe(just=x) if x > 0 else Maybe()).bind(lambda a:
Maybe(just=x*a) .bind(lambda b:
Maybe.mreturn(a+b)))
It's functionally sound and standard, but ergonomically painful. I built a really fun horrible hack to allow you to write that instead as this: with do(Maybe) as y:
a = Maybe(just=x) if x > 0 else Maybe()
b = Maybe(just=x*a)
mreturn(a+b)
It swaps out the assignment operator `=` in the `with do()` block for the monadic bind operation, which you might be used to seeing as `<-`.You just need to use my @with_do_notation decorator, which just completely rewrites your function using the ast library wherever it finds a block of `with do(SomeClass) as variable:`. I was even able to write ergonomically nice parser combinators [1] that would actually work pretty well if python had tail call optimization.
You shouldn't use it but it was great fun and opened my eyes to the ways you can abuse python if you really wanted to. Using decorators to introspect and completely rewrite functions is a fun exercise.
[0] https://github.com/imh/python_do_notation
[1] https://github.com/imh/python_do_notation/blob/master/parser...
Edit: Oh, nevermind - the "y" is how you access it after the with statement. Weird for python, but it makes sense given the rewritten version. I was thinking your version would have a non-rewritten "result = something" within the with block in a real use, but that's not what it's doing.
Sign me up plz.
(Something about Haskell...)
lambda i:
square = i * i
if i < 0:
return -1 * square
else:
return square
A decent heuristic for whether an algorithm is stateful might be to check if you can map it pretty easily to something like Haskell. In this case, it's not hard to do at all: \i ->
let square = i * i in
if x < 0 then -1 * x else x
Of course, it might be more natural for some programmers to write the Python code in a stateful way like this: lambda i:
square = i * i
if i < 0:
square *= -1
return square lambda i: -i * i if i < 0 else i * i
Also, the functional way of doing multiple lines in a lot of situations would typically be to compose smaller lambdas. with_file("c:/blah/data.csv", lambda fl:
# do stuff with file variable fl
# other statements
# ...
)
The file is opened, the block of code of the lambda does its thing, the file is closed at the end, exception-safe.Python can do this another way (with 'with' objects or something, and it's quite clean and neat, but that's quite recent IIRC), this simpler IMO. Have used this, it's called the loan pattern, in scala, vb.net and C#, and prob others.
The arbitrary restriction on lambdas blocks composability, which disallows useful tricks like this.
I would love to highlight several features of `0.14` release we are working on right now:
1. Typed `partial` and `curry` functions: https://returns.readthedocs.io/en/latest/pages/curry.html
2. `Future` and `FutureResult` containers for working with async Python! https://returns.readthedocs.io/en/latest/pages/future.html
3. Typed functional pipelines with very good type inference: https://returns.readthedocs.io/en/latest/pages/pipeline.html
It is going to be released soon, stay tuned!
The examples in the README and this blog post just give me huge "nope" vibes. Obviously Python could learn a lot more from functional programming, but this is the wrong way to go about a lot of it.
0. https://sobolevn.me/2019/02/python-exceptions-considered-an-...
The answer is simple: let the `DivsionByZero` exception raise! You don't have to return anything!
Better yet, you should have input cleansing/data validation before it gets to that point. The alternative presented in the blog post is absurd over-engineering.
Let's examine the README example for a minute:
user: Optional[User]
if user is not None:
balance = user.get_balance()
if balance is not None:
balance_credit = balance.credit_amount()
if balance_credit is not None and balance_credit > 0:
can_buy_stuff = True
else:
can_buy_stuff = False
I don't know if it's been deliberatly twisted, but that's not what I would called idiomatic or realistic for a Python program:- one should probably never reach this part of the code if there is no user. But I'll indulge the author.
- don't put can_buy_stuff in an else close, what's the point?
- using type hints for no reason, but not other modern facilities like the walrus operator?
- do we really want users without a balance? Let's indulge this, but it seems a bad design.
- what's with all those unecessary conditional blocks?
- credit_amount should never be None. It's a Balance object, put a sane default value. But ok, indulging again.
So, you get down to:
user: Optional[User]
can_buy_stuff = False
if user and (balance := user.get_balance()):
can_buy_stuff = (balance.credit_amount() or 0) > 0
I don't think the solution the lib offers is superior: can_buy_stuff: Maybe[bool] = Maybe.from_value(user).map(
lambda real_user: real_user.get_balance(),
).map(
lambda balance: balance.credit_amount(),
).map(
lambda balance_credit: balance_credit > 0,
)
And that's if we are using the rules of the README, which are not fair.If we have a well designed API, and we use a function (more testable, and hey, are we doing FP or not ?), then:
def can_buy_stuff(user: User):
if (balance := user.get_balance()):
return balance.credit_amount() > 0
return False
Checking the user should not be part of this algo, credit_amount should be 0 if never set. We could even remove return False, I keep it because I like explicitness.You could even that as a method or raise NoBalance depending of your case.
Bottom line, if you really feel that strongly about None, don't jump on the bazooka to kill this fly, go to https://discuss.python.org and advocate for PEP 505 (None-aware operators): https://www.python.org/dev/peps/pep-0505/
It's been deferred since 2015.
That doesn't mean we should not experiment with other paradigms in Python, and I do think this lib is an interesting experiment, but I don't find it conclusive.
For Maybe example: I think this 'functional' style with fmaps in Python is problematic, because lambdas can't be multiline. If you have sevearal lines of logic, you'd need an auxiliary helper def, and at this point it becomes as unreadable.
For Results: I think returning Union[Exception, Value] (where Value is the 'desired' type) and then using isinstance(result, Exception) is much cleaner.
- it can be statically checked with mypy to ensure the same level of type safety as Rust would have
- minimal performance impact
- no extra wrapping and unwrapping in the code. You can completely ignore mypy and error handling, until you're happy, then you harden your program by making sure it complies to mypy.
- no extra dependencies, third party code dealing with your library doesn't have to deal with your wrappers! If they don't check for error type, when Exception is encountered, the program will most likely terminate with AttributeError, which is a desirable behaviour in such situation.
- it's much easier to compose: propagating error with a decorator is neat, until you have some more sophisticated logic, e.g. for error aggregation
- the only downside is that you end up with occasional `if isinstance(result, Exception)...`.
I reviewed results library specifically here [0] and elaborate on different error handling techniques in Python, including the approach I described.
And that’s how your initial refactored code will look like:
user: Optional[User]
can_buy_stuff: Maybe[bool] = Maybe.from_value(user).map( # type hint is not required
lambda real_user: real_user.get_balance(),
).map(
lambda balance: balance.credit_amount(),
).map(
lambda balance_credit: balance_credit > 0,
)
Much better, isn’t it?If this chain of calls supposedly handles Nones at any level, then .map is not the right method. It should be .flatMap (or .bind). Unless this is a magic .map that handles either X or Maybe[X] and even exceptions (just like JS promises). This flexibility may be convenient and I can appreciate its pythonicness, but it's not really in the spirit of typed FP.
(lambda real_user: real_user.get_balance())
and not User.get_balance
(besides subclasses not getting their balance called...)Step 1: try to integrate more functional methods in your current Python programming due to their usefulness.
Step 2: get increasingly frustrated, and then give up, because Guido and the Python devs seemingly hate functional programming and keep hobbling it.
In my experience, the initial ease and speed of development when using python doesn't nearly outweigh the medium to long-term costs of maintaining it and developing the codebase further - at least for codebases that are more than a simple tool or something like a django app. Writing things like go, rust, scala, java etc. isn't that much more difficult or slower, but it does require more up front planning and understanding of your problem domain.
Mostly just to see what a language that 'feels like' Python would be like with the addition of things like proper Option/Result's and a couple of other features (stronger typing?).
For many FP programmers, the lambdas are not nearly magic enough.
In Swift, you can do something like this :
reversedNames = names.sorted(by: { $0 > $1 } )
It's a closure expression, and is an anonymous block of code that will magically bind parameters passed to it to numbered variables.So you see the position Python is in: it has to balance a bit of magic for the power user, and yet not too much for the casual user.
It's a very delicate exercice, and it receives a lot of critics for it.
An F# or Lisp dev comming to Python will complain that it's not expressive enough.
A geographer comming to Python will struggle reading advanced code.
Yet we have to catters to all of them, give the huge Python popularity and it's goal to be "the second best language for everything".
I think Guido did a very decent job at it, although he gets a lot of heat for it. People don't like to hear "no" when they ask for a poney.
And the lambda expression is one of the most controversial decisions. Beginners have a hard time with it, but professional coders may snap when they hear it's limited to one line.
a version of try and either, with a decent do notation taking advantage of for comprehension... https://github.com/papaver/pyfnz
Annotated[IO[str], unpure]
in 3.9 and above if the authors want to commit to type hints being core to this. I agree that the decorators feel a little wrongOf course, in my experience, this is so foreign to people who haven’t worked with it for a time it is very difficult to sell in small reply.
Introducing a new paradigm while this one just got in would be overkill IMO.
if user is not None:
balance = user.get_balance()
if balance is not None:
balance_credit = balance.credit_amount()
if balance_credit is not None and balance_credit > 0:
can_buy_stuff = True
else:
can_buy_stuff = False
Actually I think that's very readable user: Optional[User]
can_buy_stuff: Maybe[bool] = Maybe.from_value(user).map( # type hint is not required
lambda real_user: real_user.get_balance(),
).map(
lambda balance: balance.credit_amount(),
).map(
lambda balance_credit: balance_credit > 0,
)
Much better, isn't it?
No?!? That's much worse. def can_buy_stuff(user):
if user is None:
return False
balance = user.get_balance()
if balance is None:
return False
balance_credit = balance.credit_amount()
if balance_credit is None:
return False
return balance_credit > 0
Edit: something like Swift's optionals and nil-coalescing syntax might make this easier to read: def can_buy_stuff(user):
return (user?.get_balance()?.credit_amount() ?? 0) > 0It overloads map() with a lambda function, to compose function pipelining.
Then, it introduces flow() as the new pipelining tool. But you have to use bind() on the last function call to return the value.
Interesting concepts, but unless Python incorporates this as a standard feature, then this will remain a fringe idea. And it will add significant load to the development and maintenance process of Python programs.
Admittedly, I like Elixir’s pipe forward concept |>
That makes it super simple to do functional composition, and it will automatically bind and return the last value.
Then getting the user profile, can be like so:
user_profile =
userid |> get_request |> parse_json