look at example.py; run "make test" to verify I'm not full of shit.
Last active
December 18, 2015 09:28
-
-
Save ericmoritz/5761152 to your computer and use it in GitHub Desktop.
Explaining monads without using the work monad.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*.pyc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Dummy database for demostration purposes | |
""" | |
sections = { | |
"home": [1,2,3] | |
} | |
stories = { | |
1: "story-1", | |
2: "story-2", | |
3: "story-3" | |
} | |
def get_section(key): | |
return sections.get(key) | |
def get_story(key): | |
return stories.get(key) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""I will attempt to explain Monads without using the word Monad again. | |
This was inspired by a discussion about simplifying unit testing | |
by minimizing logical branches per unittest. | |
Starting with a imaginary web framework we attempted to wrangle a complex | |
view function. | |
Starting with this view: | |
""" | |
import db | |
import template | |
import test | |
def view(section_key): | |
section = db.get_section(section_key) | |
if section: | |
stories = [] | |
for story_id in section: | |
stories.append( | |
db.get_story(story_id) | |
) | |
c = {"stories": stories} | |
return template.render("section", c) | |
else: | |
return template.render("404", {}) | |
# Two outcomes: | |
test.expect( | |
"['story-1', 'story-2', 'story-3']", | |
view("home") | |
) | |
test.expect( | |
"404 Not Found", | |
view("not-here") | |
) | |
""" | |
If we identify which expressions are impure, we can | |
identify where we would have to place mocks | |
""" | |
def view(section_key): | |
# impure | |
section = db.get_section(section_key) | |
# impure | |
if section: | |
# impure | |
stories = [] | |
for story_id in section: | |
stories.append( | |
db.get_story(story_id) | |
) | |
# pure | |
c = {"stories": stories} | |
# impure | |
return template.render("section", c) | |
else: | |
# impure | |
return template.render("404", {}) | |
""" | |
By identifying the impure expressions, we've identified where the | |
mocks need to go: | |
db.get_section | |
db.get_story | |
template.render | |
That is quite a few mocks; this is a complex function to test; lets simplify | |
our units: | |
""" | |
def _fetch_section(section_key): | |
return db.get_section(section_key) | |
db.get_section = test.mock( | |
# mock function to call | |
lambda key: [1,2], | |
# assert that it is called with this: | |
"fake-section-key" | |
) | |
test.expect([1,2], _fetch_section("fake-section-key")) | |
reload(db) # reset mocks | |
def _fetch_stories(section): | |
return [ | |
db.get_story(story_id) | |
for story_id in section | |
] | |
db.get_story = test.mock(lambda key: {1: "s-1", 2: "s-2"}.get(key)) | |
test.expect(["s-1", "s-2"], _fetch_stories([1,2])) | |
reload(db) # reset mocks | |
def _context(stories): | |
return { | |
"stories": stories | |
} | |
test.expect({"stories": "!stories!"}, _context("!stories!")) | |
def _render_section(context): | |
return template.render("section", context) | |
template.render = test.mock(lambda t,c: "!render!", "section", "!context!") | |
test.expect("!render!", _render_section("!context!")) | |
reload(template) # reset mocks | |
def view(request): | |
section = _fetch_section(request.GET['section']) # impure | |
if section: | |
stories = _fetch_stories(section) # impure | |
context = _context(stories) # pure | |
return _render_section(context) #impure | |
else: | |
return template.render(404, {}) | |
"""" | |
We're at the point where we have to test our view again. | |
We've abstracted our pure and impure expressions to make mocking | |
easier, but damn it, we have to mock our abstracted functions to | |
test that the if statement works correctly. | |
We want to get to the point of removing all logical branches from our | |
view function to avoid having to retest the sequence of operations. | |
We know that each step in the view works correctly because we tested them. | |
Mocking the entire view to test an if-statement is ridiculous | |
If we make our operations return something or None, we can chain them together | |
with a simple pattern. | |
The pattern looks like this: | |
def foo(value): | |
if value is not None: | |
return op(value) | |
This pattern takes an input that can be something or None and does | |
some operation on if it is not None. | |
Lets rewrite our abstracted functions using this pattern: | |
""" | |
def _fetch_section(section_key): | |
return db.get_section(section_key) | |
db.get_section = test.mock(lambda key: [1,2], "fake-section-key") | |
test.expect([1,2], _fetch_section("fake-section-key")) | |
# Ensure that if db.get_section returns None, we return None | |
db.get_section = test.mock(lambda key: None, "fake-section-key") | |
test.expect(None, _fetch_section("fake-section-key")) | |
reload(db) # reset mocks | |
def _fetch_stories(section): | |
if section: | |
return [ | |
db.get_story(story_id) | |
for story_id in section | |
] | |
db.get_story = test.mock(lambda key: {1: "s-1", 2: "s-2"}.get(key)) | |
test.expect(["s-1", "s-2"], _fetch_stories([1,2])) | |
# Return None if we get None | |
db.get_story = test.mock(lambda key: None) | |
test.expect(None, _fetch_stories(None)) | |
reload(db) # reset mocks | |
def _context(stories): | |
if stories: | |
return { | |
"stories": stories | |
} | |
test.expect({"stories": "!stories!"}, _context("!stories!")) | |
# Return None if we get None | |
test.expect(None, _context(None)) | |
def _render_section(context): | |
if context: | |
return template.render("section", context) | |
else: | |
return template.render("404", {}) | |
template.render = test.mock(lambda t,c: "!render!", "section", "!context!") | |
test.expect("!render!", _render_section("!context!")) | |
# render 404 if we get None | |
template.render = test.mock(lambda t,c: "!render!", "404", {}) | |
test.expect("!render!", _render_section(None)) | |
reload(template) # reset mocks | |
def view(section_key): | |
section = _fetch_section(section_key) | |
stories = _fetch_stories(section) | |
context = _context(stories) | |
return _render_section(context) | |
"""Using this pattern, we move the decision to render a template or a 404 | |
to the final step. Only if all operations return something, will we render | |
a template, otherwise we render a 404. | |
We've made the original complex view function into a sequence of operations. | |
These operations are tested in isolation using unit testing. | |
Let us prove that the view still functions as it did before: | |
""" | |
def view(section_key): | |
section = _fetch_section(section_key) | |
stories = _fetch_stories(section) | |
context = _context(stories) | |
return _render_section(context) | |
test.expect( | |
"['story-1', 'story-2', 'story-3']", | |
view("home") | |
) | |
test.expect( | |
"404 Not Found", | |
view("not-here") | |
) | |
"""Because we have tested each step in the sequence, testing the | |
sequence is less necessary. | |
If this view functions correctly once, it will continue to work | |
correctly as long as the unit test continue to pass. | |
We have a new problem however. All our steps have logical | |
branches. That means that we have two tests per unit for each branch. | |
Luckily, we have a pattern and patterns can be abstracted: | |
""" | |
def call_me_maybe(val, f): | |
if val: | |
return f(val) | |
def my_fun(val): | |
test.expect("in", val) | |
return "yes!" | |
test.expect( | |
"yes!", | |
call_me_maybe("in", my_fun) | |
) | |
def you_better_not_callme(val): | |
raise Exception("I told you so.") | |
test.expect( | |
None, | |
call_me_maybe(None, you_better_not_callme) | |
) | |
""" | |
Excellent, now we can return our functions back to their original form and remove the tests for None. | |
""" | |
def _fetch_section(section_key): | |
return db.get_section(section_key) | |
db.get_section = test.mock(lambda key: [1,2], "fake-section-key") | |
test.expect([1,2], _fetch_section("fake-section-key")) | |
reload(db) # reset mocks | |
def _fetch_stories(section): | |
return [ | |
db.get_story(story_id) | |
for story_id in section | |
] | |
db.get_story = test.mock(lambda key: {1: "s-1", 2: "s-2"}.get(key)) | |
test.expect(["s-1", "s-2"], _fetch_stories([1,2])) | |
reload(db) # reset mocks | |
def _context(stories): | |
return { | |
"stories": stories | |
} | |
test.expect({"stories": "!stories!"}, _context("!stories!")) | |
def _render_section(context): | |
if context: | |
return template.render("section", context) | |
else: | |
return template.render("404", {}) | |
template.render = test.mock(lambda t,c: "!render!", "section", "!context!") | |
test.expect("!render!", _render_section("!context!")) | |
# render 404 if we get None | |
template.render = test.mock(lambda t,c: "!render!", "404", {}) | |
test.expect("!render!", _render_section(None)) | |
reload(template) # reset mocks | |
def view(section_key): | |
section = call_me_maybe(section_key, _fetch_section) | |
stories = call_me_maybe(section, _fetch_stories) | |
context = call_me_maybe(stories, _context) | |
return _render_section(context) | |
# Does it still work? | |
test.expect( | |
"['story-1', 'story-2', 'story-3']", | |
view("home") | |
) | |
test.expect( | |
"404 Not Found", | |
view("not-here") | |
) | |
"""Beautiful. We have chained everything together and made our | |
unittests simpiler by removing the logical branch. | |
The only function that needs the if statement is _render_section() | |
because that is where we decide to render a 404 or a section. | |
We're approaching a level of abstraction that we'll find the Zen of | |
Python will tell us to stop; The call_me_maybe function is approaching | |
non-obvious. | |
We should stop but we won't because we are calling every damn | |
function, every time. Can we short-circuit the sequence and return | |
None? | |
We'll we could but it isn't going to look pretty. | |
""" | |
def view(section_key): | |
context = call_me_maybe( | |
_fetch_section(section_key), | |
lambda section: call_me_maybe( | |
_fetch_stories(section), | |
lambda stories: _context(stories) | |
) | |
) | |
return _render_section(context) | |
# Does it still work? | |
test.expect( | |
"['story-1', 'story-2', 'story-3']", | |
view("home") | |
) | |
test.expect( | |
"404 Not Found", | |
view("not-here") | |
) | |
"""OMGWTF; Yeah, you're right. OMFWTF it is. | |
I'll let you unravel that nested node.js like pyramid of doom. | |
Get it yet? I'll wait longer... | |
Ok, I've waited long enough. A value goes into call_me_maybe and if | |
it is not None, call_me_maybe calls the callback. If the value is | |
none, call_me_maybe doesn't call the callback and the whole sequence stops. | |
Should we do these nested callbacks in Python? Not if you want the | |
scorn of your peers. In Python, it is likely better to use the call_me_maybe | |
function directly and live without short-circuiting or write a clever way | |
to abstract it away. | |
A clever way to abstract it away is exactly what Haskell does. Haskell's do | |
keyword transforms what looks like this: | |
do | |
section = fetch_section(section_key) | |
stories = fetch_stories(section) | |
context = context(stories) | |
render_section(context) | |
Into the pyramid of doom I wrote in Python. | |
If you have ever read anything about Monads you know they have a bind | |
function; The bind function is the glue between the steps. | |
Our call_me_maybe function is a port of the Haskell's | |
Maybe monad bind function. We used the call_me_maybe function to glue | |
our steps together. | |
That glue could be anything, and there are a number of Monads in | |
Haskell to glue things together. | |
The Maybe monad, and my implementation of its bind function is really helpful | |
in gluing together a sequence of steps together. | |
""" | |
print "all tests passed" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
test: | |
python example.py |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
Dummy template engine for demonstration purposes | |
""" | |
templates = { | |
"section": "{stories!r}", | |
"404": "404 Not Found" | |
} | |
def render(template, context): | |
return templates[template].format(**context) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def expect(expected, value): | |
assert expected == value, "{0!r} != {1!r}".format(expected, value) | |
def mock(return_cb, *expected_args, **expected_kwargs): | |
def mocked(*call_args, **call_kwargs): | |
if expected_args: | |
expect(expected_args, call_args) | |
if expected_kwargs: | |
expect(expected_kwargs, call_kwargs) | |
return return_cb(*call_args, **call_kwargs) | |
return mocked |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Best f'ing explanation ev4r!!!1!!cos(0)!!!!!!!11!!!!!!
Seriously, example.py was a joy to read.