Sort of; it's Maybes being composed with <|> rather than with >>=. The code in Haskell is still extremely ugly, even though there's no indentation moving the code over to the right:
get_user :: Cache -> Database -> Maybe Id -> Maybe Username -> Maybe User
get_user cache db id name =
(id >>= get_user_by_id cache) <|>
(name >>= get_user_by_username cache) <|>
((id >>= get_user_by_id db) <|>
(name >>= get_user_by_username db)) >>=
\user-> cache_user cache user >> return user) <|>
Nothing
So while I like the look of this code more than I like the look of the Python, it's still shit. The problem here is not that there is nesting, it's that the code isn't well-factored.
The first problem is that the function does too much. There should be two methods, one that accepts a username argument and another that accepts an id argument. The dispatching between the two types of invocation is one level of nesting that is completely unnecessary; get_user_by_id(42) and get_user(id=42) are both equally easy to read, but the latter is much harder to implement cleanly.
The next problem is that the caching logic is handled in this random data lookup function, instead of some place where we can reuse the common pattern of "check cache and return, otherwise fetch, cache, and return". So let's write that:
def cached_lookup(cache, object, method, key):
# already cached
if cache.contains(key):
return cache.get(key)
# not cached; lookup in database and add result to cache
value = object.method(key)
if value is not None:
cache.insert(key, value) # we are punting on calculating the key
# from the value here, but that is something
# the cache could be taught to do with a
# decent metaobject protocol.
return value
Now the functions are separated by concern. The get_user_by_* functions do one thing: compute a cache key and ask the cache for the object. The cache is then responsible for the fetching logic. That means if we want to change how caching works (perhaps to add eviction), we only need to change one thing in one place. If we made every get_foo_by_bar function handle caching, the code would quickly become unmaintainable.
So to summarize, nesting draws our attention to problems, but removing the nesting is not the solution to the problem. Neither are monads. The solution to the problem is to refactor.
Yeah, I'll agree that the functions weren't very good. I actually had a hard time coming up with examples that didn't require a bunch of extra explanation.