1. Quick Introduction¶
1.1. Explanation by Example¶
Effect starts with a very simple idea: instead of having a function which performs side-effects (such as IO):
def get_user_name():
return raw_input("Enter User Name> ") # or 'input' in Python 3
you instead have a function which returns a representation of the side-effect:
def get_user_name():
return Effect(ReadLine("Enter User Name> "))
We call objects like ReadLine
an intent – that is, the intent of this
effect is to read a line of input from the user. Ideally, intents are very
simple objects with public attributes and no behavior, only data.
class ReadLine(object):
def __init__(self, prompt):
self.prompt = prompt
To perform the ReadLine intent, we must implement a performer function:
@sync_performer
def perform_read_line(dispatcher, readline):
return raw_input(readline.prompt)
To do something with the result of the effect, we must attach callbacks with
the on
method:
def greet():
return get_user_name().on(
success=lambda r: Effect(Print("Hello,", r)),
error=lambda exc: Effect(Print("There was an error!", exc)))
(Here we assume another intent, Print
, which shows some text to the user.)
A (sometimes) nicer syntax is provided for adding callbacks, with the
effect.do.do()
decorator.
from effect.do import do
@do
def greet():
try:
name = yield get_user_name()
except Exception as e:
yield Effect(Print("There was an error!", e))
else:
yield Effect(Print("Hello,", name))
Finally, to actually perform these effects, they can be passed to
effect.sync_perform()
, along with a dispatcher which looks up the
performer based on the intent.
from effect import sync_perform
def main():
eff = greet()
dispatcher = ComposedDispatcher([
TypeDispatcher({ReadLine: perform_read_line}),
base_dispatcher])
sync_perform(dispatcher, eff)
This has a number of advantages. First, your unit tests for get_user_name
become simpler. You don’t need to mock out or parameterize the raw_input
function - you just call get_user_name
and assert that it returns a
ReadLine
object with the correct ‘prompt’ value.
Second, you can implement ReadLine
in a number of different ways - it’s
possible to override the way an intent is performed to do whatever you want. For
example, you could implement an HTTPRequest client either using the popular
requests package, or using the Twisted-based treq package – without
needing to change any of your application code, since it’s all in terms of the
Effect API.
1.2. A quick tour, with definitions¶
- Intent: An object which describes a desired action, ideally with simple
inert data in public attributes. For example,
ReadLine(prompt='> ')
could be an intent that describes the desire to read a line from the user after showing a prompt. effect.Effect
: An object which binds callbacks to receive the result of performing an intent.- Performer: A callable that takes the Dispatcher, an Intent, and a Box. It
executes the Intent and puts the result in the Box. For example, the
performer for
ReadLine()
could callraw_input(intent.prompt)
. - Dispatcher: A callable that takes an Intent and finds the Performer that can
execute it (or None). See
TypeDispatcher
andComposedDispatcher
for handy pre-built dispatchers. - Box: An object that has
succeed
andfail
methods for providing the result of an effect (potentially asynchronously). Usually you don’t need to care about this, if you define your performers witheffect.sync_performer()
ortxeffect.deferred_performer
from the txeffect package.
There’s a few main things you need to do to use Effect.
- Define some intents to describe your side-effects (or use a library
containing intents that already exist). For example, an
HTTPRequest
intent that hasmethod
,url
, etc attributes. - Write your application code to create effects like
Effect(HTTPRequest(...))
and attach callbacks to them withEffect.on()
. - As close as possible to the top-level of your application, perform your
effect(s) with
effect.sync_perform()
. - You will need to pass a dispatcher to
effect.sync_perform()
. You should create one by creating aeffect.TypeDispatcher
with your own performers (e.g. forHTTPRequest
), and composing it witheffect.base_dispatcher
(which has performers for built-in effects) usingeffect.ComposedDispatcher
.
1.3. Callback chains¶
Effect allows you to build up chains of callbacks that process data in turn.
That is, if you attach a callback a
and then a callback b
to an Effect,
a
will be called with the original result, and b
will be called with
the result of a
. This is exactly how Twisted’s Deferreds work, and similar
to the monadic bind
(>>=
) function from Haskell.
This is a great way to build abstractions, compared to non-chaining callback systems like Python’s Futures. You can easily build abstractions like the following:
def request_url(method, url, str_body):
"""Perform an HTTP request."""
return Effect(HTTPRequest(method, url, str_body))
def request_200_url(method, url, str_body):
"""
Perform an HTTP request, and raise an error if the response is not 200.
"""
def check_status(response):
if response.code != 200:
raise HTTPError(response.code)
return response
return request_url(method, url, str_body).on(success=check_status)
def json_request(method, url, dict_body):
"""
Perform an HTTP request where the body is sent as JSON and the response
is automatically decoded as JSON if the Content-type is
application/json.
"""
str_body = json.dumps(dict_body)
return request_200_url(method, url, str_body).on(success=decode_json)