Wednesday, June 26, 2013
Sunday, June 16, 2013
Chaining Either's in Java 8
The discussion about Either in Scala and trying to implement it in Java got me to thinking. If we make the simplifying assumption that the error portion of any Either is some kind of Exception, It would seem we can make pretty good progress in using the Either pattern of handling error conditions.
I'd be interested to hear of problems and/or improvements to this idea.
I'd be interested to hear of problems and/or improvements to this idea.
This file contains hidden or 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
package net.gilstraps.lambdaplay; | |
public class Either<V> { | |
private final V val; | |
private final Exception ex; | |
public Either(V aNR) { | |
if (aNR == null) throw new NullPointerException("Must have either a hasValue or an error."); | |
val = aNR; | |
ex = null; | |
} | |
public Either(Exception anE) { | |
if (anE == null) throw new NullPointerException("Must have either a hasValue or an error."); | |
val = null; | |
ex = anE; | |
} | |
public boolean hasValue() { | |
return val != null; | |
} | |
public V right() { | |
return val; | |
} | |
public V getOrElse( final V defaultValue ) { | |
return hasValue() ? val : defaultValue; | |
} | |
public Exception left() { | |
return ex; | |
} | |
public interface Map<S, T> { | |
public T map(final S s); | |
} | |
public <O> Either<O> rightMap(Map<V, O> m) { | |
if (hasValue()) { | |
try { | |
return new Either<O>(m.map(this.val)); | |
} | |
catch (Exception e) { | |
return new Either<O>(e); | |
} | |
} | |
return new Either<O>(ex); | |
} | |
} |
This file contains hidden or 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
package net.gilstraps.lambdaplay; | |
import org.junit.Test; | |
import static org.junit.Assert.assertEquals; | |
import static org.junit.Assert.assertTrue; | |
public class EitherTests { | |
static final String USER_NAME = "username"; | |
static final String ROOT = "root"; | |
static class AuthLevel { | |
} | |
static final AuthLevel GUEST = new AuthLevel(); | |
static final AuthLevel USER = new AuthLevel(); | |
static final AuthLevel SUPERUSER = new AuthLevel(); | |
public static class NoSuchUserException extends Exception { | |
} | |
public static class UnknownSettingException extends Exception { | |
} | |
public static class SettingGetter { | |
private String userName; | |
public SettingGetter(final String name) { | |
userName = name; | |
} | |
Either<String> get_setting(final String settingName) { | |
// Simple way to test out both having and not having a username | |
if (settingName.equals(USER_NAME)) { | |
if (userName != null) { return new Either<>(userName); } | |
else { return new Either<>(new NoSuchUserException()); } | |
} | |
else return new Either<>(new UnknownSettingException()); | |
} | |
} | |
public static class Authorizer { | |
public AuthLevel getAuthorizationLevelForUser(final String user) { | |
if ( ROOT.equals(user) ) { | |
return SUPERUSER; | |
} | |
else { | |
return USER; | |
} | |
} | |
} | |
public static class Decider { | |
public Boolean canDoAnything( final AuthLevel l ) { | |
return SUPERUSER.equals(l); | |
} | |
} | |
@Test | |
public void authLevelTests() { | |
{ | |
final SettingGetter sg = new SettingGetter("joe"); | |
final Authorizer authorizer = new Authorizer(); | |
final Either<AuthLevel> authLevel = getAuthorizationLevel(sg,authorizer); | |
assertEquals(USER,authLevel.right()); | |
} | |
{ | |
final SettingGetter sg = new SettingGetter(null); | |
final Authorizer authorizer = new Authorizer(); | |
final Either<AuthLevel> authLevel = getAuthorizationLevel(sg,authorizer); | |
assertEquals(null,authLevel.right()); | |
assertEquals(NoSuchUserException.class,authLevel.left().getClass()); | |
} | |
{ | |
final SettingGetter sg = new SettingGetter(null); | |
final Authorizer authorizer = new Authorizer(); | |
final Either<AuthLevel> authLevel = getAuthorizationLevel(sg,authorizer); | |
final AuthLevel level = authLevel.getOrElse(GUEST); | |
assertEquals(GUEST,level); | |
} | |
{ | |
final SettingGetter sg = new SettingGetter(ROOT); | |
final Authorizer authorizer = new Authorizer(); | |
final Decider decider = new Decider(); | |
final Either<Boolean> isAllPowerful = sg.get_setting(USER_NAME).rightMap(authorizer::getAuthorizationLevelForUser).rightMap(decider::canDoAnything); | |
assertTrue(isAllPowerful.hasValue()); | |
assertEquals(true, isAllPowerful.right() ); | |
} | |
} | |
public Either<AuthLevel> getAuthorizationLevel(final SettingGetter sg, final Authorizer authorizer ) { | |
Either<String> possibleUser = sg.get_setting(USER_NAME); | |
return possibleUser.rightMap(authorizer::getAuthorizationLevelForUser); | |
} | |
} |
Stop writing your code for the happy path
So Jessitron recently posted about avoiding cxceptions in Scala code using 'Either'. Mario Aquino followed up with an example of using Either in Ruby, and Heath responded with a post about the awkwardness of using Either in Java compared to using checked exceptions.
I agree with all of them on many levels. But the most important thing they didn't say (but which is implicit in their discussions), is the need to stop thinking that writing code is about implementing the happy path. It always amazes me when I encounter code base and discover how little thought has been put into the error handling (the "not so happy paths"). The truth is that about 10% of software engineering is implementing the happy path. The other 90% is figuring out how things can go wrong and then eliminating those possibilities (when feasible) or writing code that gracefully handles the errors.
The art of system design is in finding ways to simplify the code that handles all the errors that can occur. The happy path code can never get any shorter than the minimum required. It's all the error handling code that leaves room for good design.
I agree with all of them on many levels. But the most important thing they didn't say (but which is implicit in their discussions), is the need to stop thinking that writing code is about implementing the happy path. It always amazes me when I encounter code base and discover how little thought has been put into the error handling (the "not so happy paths"). The truth is that about 10% of software engineering is implementing the happy path. The other 90% is figuring out how things can go wrong and then eliminating those possibilities (when feasible) or writing code that gracefully handles the errors.
The art of system design is in finding ways to simplify the code that handles all the errors that can occur. The happy path code can never get any shorter than the minimum required. It's all the error handling code that leaves room for good design.
Subscribe to:
Posts (Atom)