@@ -90,11 +90,11 @@ user: Optional[User]
9090discount_program: Optional[' DiscountProgram' ] = None
9191
9292if user is not None :
93- balance = user.get_balance()
94- if balance is not None :
95- credit = balance.credit_amount()
96- if credit is not None and credit > 0 :
97- discount_program = choose_discount(credit)
93+ balance = user.get_balance()
94+ if balance is not None :
95+ credit = balance.credit_amount()
96+ if credit is not None and credit > 0 :
97+ discount_program = choose_discount(credit)
9898```
9999
100100Or you can use
@@ -106,9 +106,10 @@ representing existing state and empty (instead of `None`) state respectively.
106106from typing import Optional
107107from returns.maybe import Maybe, maybe
108108
109+
109110@maybe # decorator to convert existing Optional[int] to Maybe[int]
110- def bad_function () -> Optional[int ]:
111- ...
111+ def bad_function () -> Optional[int ]: ...
112+
112113
113114maybe_number: Maybe[float ] = bad_function().bind_optional(
114115 lambda number : number / 2 ,
@@ -129,14 +130,20 @@ And here's how your initial refactored code will look:
129130user: Optional[User]
130131
131132# Type hint here is optional, it only helps the reader here:
132- discount_program: Maybe[' DiscountProgram' ] = Maybe.from_optional(
133- user,
134- ).bind_optional( # This won't be called if `user is None`
135- lambda real_user : real_user.get_balance(),
136- ).bind_optional( # This won't be called if `real_user.get_balance()` is None
137- lambda balance : balance.credit_amount(),
138- ).bind_optional( # And so on!
139- lambda credit : choose_discount(credit) if credit > 0 else None ,
133+ discount_program: Maybe[' DiscountProgram' ] = (
134+ Maybe
135+ .from_optional(
136+ user,
137+ )
138+ .bind_optional( # This won't be called if `user is None`
139+ lambda real_user : real_user.get_balance(),
140+ )
141+ .bind_optional( # This won't be called if `real_user.get_balance()` is None
142+ lambda balance : balance.credit_amount(),
143+ )
144+ .bind_optional( # And so on!
145+ lambda credit : choose_discount(credit) if credit > 0 else None ,
146+ )
140147)
141148```
142149
@@ -157,17 +164,21 @@ Imagine that you have a `django` based game, where you award users with points f
157164from django.http import HttpRequest, HttpResponse
158165from words_app.logic import calculate_points
159166
167+
160168def view (request : HttpRequest) -> HttpResponse:
161169 user_word: str = request.POST [' word' ] # just an example
162170 points = calculate_points(user_word)
163171 ... # later you show the result to user somehow
164172
173+
165174# Somewhere in your `words_app/logic.py`:
166175
176+
167177def calculate_points (word : str ) -> int :
168178 guessed_letters_count = len ([letter for letter in word if letter != ' .' ])
169179 return _award_points_for_letters(guessed_letters_count)
170180
181+
171182def _award_points_for_letters (guessed : int ) -> int :
172183 return 0 if guessed < 5 else guessed # minimum 6 points possible!
173184```
@@ -202,23 +213,28 @@ from django.conf import settings
202213from django.http import HttpRequest, HttpResponse
203214from words_app.logic import calculate_points
204215
216+
205217def view (request : HttpRequest) -> HttpResponse:
206218 user_word: str = request.POST [' word' ] # just an example
207219 points = calculate_points(user_word)(settings) # passing the dependencies
208220 ... # later you show the result to user somehow
209221
222+
210223# Somewhere in your `words_app/logic.py`:
211224
212225from typing import Protocol
213226from returns.context import RequiresContext
214227
228+
215229class _Deps (Protocol ): # we rely on abstractions, not direct values or types
216230 WORD_THRESHOLD : int
217231
232+
218233def calculate_points (word : str ) -> RequiresContext[int , _Deps]:
219234 guessed_letters_count = len ([letter for letter in word if letter != ' .' ])
220235 return _award_points_for_letters(guessed_letters_count)
221236
237+
222238def _award_points_for_letters (guessed : int ) -> RequiresContext[int , _Deps]:
223239 return RequiresContext(
224240 lambda deps : 0 if guessed < deps.WORD_THRESHOLD else guessed,
@@ -245,6 +261,7 @@ Consider this code that you can find in **any** `python` project.
245261``` python
246262import requests
247263
264+
248265def fetch_user_profile (user_id : int ) -> ' UserProfile' :
249266 """ Fetches UserProfile dict from foreign API."""
250267 response = requests.get(' /api/users/{0} ' .format(user_id))
@@ -267,6 +284,7 @@ but with the all hidden problems explained.
267284``` python
268285import requests
269286
287+
270288def fetch_user_profile (user_id : int ) -> ' UserProfile' :
271289 """ Fetches UserProfile dict from foreign API."""
272290 response = requests.get(' /api/users/{0} ' .format(user_id))
@@ -304,6 +322,7 @@ from returns.result import Result, safe
304322from returns.pipeline import flow
305323from returns.pointfree import bind
306324
325+
307326def fetch_user_profile (user_id : int ) -> Result[' UserProfile' , Exception ]:
308327 """ Fetches `UserProfile` TypedDict from foreign API."""
309328 return flow(
@@ -312,13 +331,15 @@ def fetch_user_profile(user_id: int) -> Result['UserProfile', Exception]:
312331 bind(_parse_json),
313332 )
314333
334+
315335@safe
316336def _make_request (user_id : int ) -> requests.Response:
317337 # TODO : we are not yet done with this example, read more about `IO`:
318338 response = requests.get(' /api/users/{0} ' .format(user_id))
319339 response.raise_for_status()
320340 return response
321341
342+
322343@safe
323344def _parse_json (response : requests.Response) -> ' UserProfile' :
324345 return response.json()
@@ -377,11 +398,14 @@ import datetime as dt
377398
378399from returns.io import IO
379400
401+
380402def get_random_number () -> IO [int ]: # or use `@impure` decorator
381403 return IO(random.randint(1 , 10 )) # isn't pure, because random
382404
405+
383406now: Callable[[], IO [dt.datetime]] = impure(dt.datetime.now)
384407
408+
385409@impure
386410def return_and_show_next_number (previous : int ) -> int :
387411 next_number = previous + 1
@@ -427,6 +451,7 @@ from returns.result import safe
427451from returns.pipeline import flow
428452from returns.pointfree import bind_result
429453
454+
430455def fetch_user_profile (user_id : int ) -> IOResult[' UserProfile' , Exception ]:
431456 """ Fetches `UserProfile` TypedDict from foreign API."""
432457 return flow(
@@ -438,12 +463,14 @@ def fetch_user_profile(user_id: int) -> IOResult['UserProfile', Exception]:
438463 bind_result(_parse_json),
439464 )
440465
466+
441467@impure_safe
442468def _make_request (user_id : int ) -> requests.Response:
443469 response = requests.get(' /api/users/{0} ' .format(user_id))
444470 response.raise_for_status()
445471 return response
446472
473+
447474@safe
448475def _parse_json (response : requests.Response) -> ' UserProfile' :
449476 return response.json()
@@ -482,6 +509,7 @@ the `first` one returns a number and the `second` one increments it:
482509async def first () -> int :
483510 return 1
484511
512+
485513def second (): # How can we call `first()` from here?
486514 return first() + 1 # Boom! Don't do this. We illustrate a problem here.
487515```
@@ -498,6 +526,7 @@ However, with `Future` we can "pretend" to call async code from sync code:
498526``` python
499527from returns.future import Future
500528
529+
501530def second () -> Future[int ]:
502531 return Future(first()).map(lambda num : num + 1 )
503532```
@@ -543,10 +572,12 @@ import anyio
543572from returns.future import future_safe
544573from returns.io import IOFailure
545574
575+
546576@future_safe
547577async def raising ():
548578 raise ValueError (' Not so fast!' )
549579
580+
550581ioresult = anyio.run(raising.awaitable) # all `Future`s return IO containers
551582assert ioresult == IOFailure(ValueError (' Not so fast!' )) # True
552583```
@@ -560,14 +591,14 @@ to get sync `IOResult` instance to work with it in a sync manner.
560591Previously, you had to do quite a lot of ` await ` ing while writing ` async ` code:
561592
562593``` python
563- async def fetch_user (user_id : int ) -> ' User' :
564- ...
594+ async def fetch_user (user_id : int ) -> ' User' : ...
565595
566- async def get_user_permissions (user : ' User' ) -> ' Permissions' :
567- ...
568596
569- async def ensure_allowed (permissions : ' Permissions' ) -> bool :
570- ...
597+ async def get_user_permissions (user : ' User' ) -> ' Permissions' : ...
598+
599+
600+ async def ensure_allowed (permissions : ' Permissions' ) -> bool : ...
601+
571602
572603async def main (user_id : int ) -> bool :
573604 # Also, don't forget to handle all possible errors with `try / except`!
@@ -587,23 +618,25 @@ import anyio
587618from returns.future import FutureResultE, future_safe
588619from returns.io import IOSuccess, IOFailure
589620
621+
590622@future_safe
591- async def fetch_user (user_id : int ) -> ' User' :
592- ...
623+ async def fetch_user (user_id : int ) -> ' User' : ...
624+
593625
594626@future_safe
595- async def get_user_permissions (user : ' User' ) -> ' Permissions' :
596- ...
627+ async def get_user_permissions (user : ' User' ) -> ' Permissions' : ...
628+
597629
598630@future_safe
599- async def ensure_allowed (permissions : ' Permissions' ) -> bool :
600- ...
631+ async def ensure_allowed (permissions : ' Permissions' ) -> bool : ...
632+
601633
602634def main (user_id : int ) -> FutureResultE[bool ]:
603635 # We can now turn `main` into a sync function, it does not `await` at all.
604636 # We also don't care about exceptions anymore, they are already handled.
605637 return fetch_user(user_id).bind(get_user_permissions).bind(ensure_allowed)
606638
639+
607640correct_user_id: int # has required permissions
608641banned_user_id: int # does not have required permissions
609642wrong_user_id: int # does not exist
@@ -624,6 +657,7 @@ Or even something really fancy:
624657from returns.pointfree import bind
625658from returns.pipeline import flow
626659
660+
627661def main (user_id : int ) -> FutureResultE[bool ]:
628662 return flow(
629663 fetch_user(user_id),
0 commit comments