« Guiding Software Development with Design Challenges | Main | Flipping Assumptions with 'Programmer Anarchy' »

June 17, 2013

Comments

Erik

'Tellers', otherwise known as the Maybe/Option monad.

Michael Feathers

Hmm.. you still get something from Maybe. The getter cases the result. Not sure I see the connection. Elaborate?

Pattern-chaser

In your first example, has to check for a null object, before it has the "good sense" not to add it to the database. And the Ruby block seems to require an equivalent check, but it's one you can't see. [I think.] So are you removing null checks, or hiding them so that they won't offend you?

Pattern-chaser

Sorry, I didn't realise anything in angle brackets would get chomped. :( Previous query should begin "In your first example, [updatePerson] has to check...".

Weavejester

Consider a Maybe object with a method "bind" that takes a block. If the Maybe object contains a value, "bind" passes the value to the block, while if the Maybe object is empty, the method does nothing.

Robert

Why would you even allow the program to continue when the person you want to update doesn't exist in the database?

philandstuff

In Scala at least, rather than explicitly checking a Maybe for Some or None, you can just do maybe.map(). That's exactly the form of the "teller" code in this post.

philandstuff

That should read maybe.map( [transform fn] ) above..

Sukant Hajra

Erik, Michael,

I wanted to point out that Michael suggests that one would pattern match ("case") the Option (I'm going to just use the Scala vernacular rather than the Haskell since this post has some Java). The complaint is that matching is too similar to a null-check, even if the match is typesafe with compiler checks for exhaustive matching.

While pattern matching is an option, what we'd really prefer to do is something like fold (also known as the "visitor" pattern) on the Option, providing a transformation for the Some case, and a default value for the None case. I don't know how well code will render in these comments, but it would look something like this:

src.person(id).fold(defaultVal) { p => valFor(p) }

What Michael is further proposing is not to expose the Option to the client at all, but to make the fold internal. But notice that the implementation in the article is rigidly imperative (the "void" return). We get a more compact call,

src.person(id) { p => unsafePerformEffect(p) }

but it's terribly restricted to side-effects.

In this sense, we actually provide the client /more/ functionality by just exposing the Option to them. The Option isn't a liability. It's an extremely nice point of abstract flexibility. Option not only comes with the general fold, but also great functions like map, flatMap, and many others.

This does not prevent an API from providing specialized "loan" pattern functions (which is what this "tellers" idea seems to me) to reduce a modicum of Option boilerplate, but I'd recommend that the Option be exposed raw as well because it's very useful. This "teller" solution is highly specialized for side effects. I'd prefer effect tracking for this, but that's another post.

Josefusbarnabas

+1 for Sukant's comments on option types. I've recently blogged about this exact point. http://proseand.co.nz/2013/06/05/optiont-null/

Arseny Kapoulkine

The way you could do this in a language that doesn't have closures is return a collection of 1 or 0 items and use foreach.

Codemonkeyism

Java isn't the best of all languages for this, but

for (String name: getName("hello")) {
// do something with name
}

if getName() returns an Option that is iterable.

http://codemonkeyism.com/for-hack-with-option-monad-in-java/

Gustavo

A bit of a limited example, what do you want to do in case the person wasn't found. Would you not like to tell the user that the Person was not found? How would you handle that?

Vasily Kirichenko

@Gustavo: In this case the author needs the Either/Choice monad :)

Thomas Hawtin

The thing that worries me about this example is that it seems there should be an `else`. If you looked up a specific id, then if it does not exist you really want to do something. Any option/Elvis operator-style/[single method] teller solution is just going to bury the programming error. Probably what we want here is a checked exception, or similar. If not, than perhaps a second block for the else-clause would be in order. Now we have a gaggle of method arguments. Seems like anonymous inner classes would be the perfect solution, if only Java's syntax didn't suck all over.

Matthew O'Connor

What if you need to get & use multiple values at the same time? Are you forced into nested blocks?

R. Kyle Murphy

Note: This comment system seems to eat less than and greater than symbols, I've replace them with lt and gt, you'll have to mentally do the conversation back to make these examples syntactically correct.

There's also the maybe function to go along with the Maybe monad.
E.G.
x lt- getPersonById id
let person = maybe defaultPerson id x
updatePerson $ person { phoneNumber = number }

or if you wanted to do something closer to what's presented in the article you could take advantage of the Applicative class:

x lt- getPersonById id
updatePersonsNumber number lt$gt x

There's some minor gotchas in that you couldn't do the update DB call inside of the Maybe instance, but if you redefined getPersonById to return something of type:

MaybeT IO

then the whole thing simplifies to:
person lt- getPersonById id
liftIO . updatePerson $ person { phoneNumber = number }

and it automatically results in IO Nothing in the case that getPersonById was Nothing.

Michael Feathers

Matthew: No, you can accumulate in into a collection: data_source.person { |p| collection.add(p) }

Michael Feathers

Re the 'else' case..

Context matters. Sometimes continuing silently is exactly the right thing to do. Granted, it's hard to see in this example. To stretch it, imagine that we're setting alternative phone numbers in a background job. We don't expect all of the IDs to be associated and we're okay if they aren't.
The idea that there is one right way to handle errors is a fallacy. I'm reminded of it every time I see someone yell "fail fast." "Fail fast" only works for one class of systems.

Matthew O'Connor

Michael: But, then, can't values of person still be null potentially causing the same problem?

Mario T. Lanza

I use a general helper method I call `present` which is the exact same thing as the Ruby `tap` method except it calls the block (lambda) only when the subject is present (not some equivalent of nothing). This way you have a general purpose means of doing the same but without having to implement anything (i.e. you wouldn't have had to implement the `person` finder.

dataSource.getPersonById(personId).present(function(person){
person.setPhoneNumber(phoneNumber);
dataSource.updatePerson(person);
});

Zeroflag

In smalltalk you can even do this:

dataSource
personById: 'id'
ifPresent: [:person | ... ]
ifAbsent: [ ... ]

Michael Feathers

Matthew: No, because the block is only called if there is a real person object to pass to it.

Michael Feathers

Mario, Zeroflag: Very nice. Again, it points out how the absence of block in languages has led to null checking.

Matthew O'Connor

Michael: Sorry, not values of person, but the values stored inside person (name, age, etc.)

The comments to this entry are closed.