What I want to do
- Given a form in an Android activity or dialog, convert form values (
java.lang.Strings) to domain classes and validate if they fulfill business constraints
- Don’t validate if there is a conversion error
- Chain validators
- Perform complex validations (e.g. of two dependent fields)
Solution with Option monad
I encapsulated a form value coming from a visual component (it can be an EditText, Spinner, whatever) in the FormValue monadic type. This is a straight implementation of the Option monad with two subtypes:
Some containing the valid value
None containing the error description
See the source code below for the implementation:
public abstract class FormValue<T> {
public static <T> FormValue<T> some(T value) {
return new Some(value);
}
public static <T> FormValue<T> none(String error) {
return new None(error);
}
private FormValue() {}
public abstract boolean hasValue();
public abstract T value();
public abstract boolean hasError();
public abstract String error();
public abstract FormValue<T> validateWith(Validator<T> validator);
public static class Some<T> extends FormValue<T> {
private T value;
private Some(T value) { this.value = value; }
@Override
public boolean hasValue() { return true; }
@Override
public T value() { return value; }
@Override
public boolean hasError() { return false; }
@Override
public String error() {
throw new NotImplementedException("Not an error");
}
@Override
public FormValue<T> validateWith(Validator<T> validator) {
return validator.validate(value);
}
}
public static class None<T> extends FormValue<T> {
private String error;
private None(String error) { this.error = error; }
@Override
public boolean hasValue() { return false; }
@Override
public T value() {
throw new NotImplementedException("Not a value");
}
@Override
public boolean hasError() { return true; }
@Override
public String error() { return error; }
@Override
public FormValue<T> validateWith(Validator<T> validator) {
return this;
}
}
}
How does it work?
Conversion
First, we have to extract the raw value stored in a widget and convert it to a domain representation wrapped in FormValue. We do this with the ViewValueExtractor interface:
public interface ViewValueExtractor<T> {
ViewValueExtractor<T> withMessage(String message);
FormValue<T> extractValue();
}
Implementations of this interface depend on the widget. In the simplest form, it extracts a String value from a TextView doing no conversion (and thus not raising any errors):
public class StringExtractor implements ViewValueExtractor<String> {
private TextView view;
public StringExtractor(TextView view) { this.view = view; }
public ViewValueExtractor<String> withMessage(String message) {
return this;
}
public FormValue<String> extractValue() {
return FormValue.some(view.getText().toString());
}
}
Another example converts a text field to the SSN class, requiring AAA-GG-SSSS format:
public class SSN {
// getters and setters omitted for brevity
public int areaNumber;
public int groupNumber;
public int serialNumber;
public SSN(int areaNumber, int groupNumber, int serialNumber) {
this.areaNumber = areaNumber;
this.groupNumber = groupNumber;
this.serialNumber = serialNumber;
}
}
public class SSNExtractor implements ViewValueExtractor<SSN> {
private TextView view;
private String message;
public SSNExtractor(TextView view) {
this.view = view;
}
public ViewValueExtractor<SSN> withMessage(String message) {
this.message = message;
return this;
}
public FormValue<SSN> extractValue() {
String text = view.getText().toString();
String[] ssnParts = text.split("-");
if (ssnParts.length != 3) {
return FormValue.none(message);
}
try {
return FormValue.some(
new SSN(
Integer.valueOf(ssnParts[0]),
Integer.valueOf(ssnParts[1]),
Integer.valueOf(ssnParts[2])
)
);
} catch (NumberFormatException e) {
return FormValue.none(message);
}
}
}
Generally speaking, you need to provide some way to pull out the value from a visual component and turn it into Some if the conversion was successful or None (with an error message).
Validation
We have already an object representing a value from our solution domain; now it’s time to validate it. We do it implementing a Validator:
public interface Validator<T> {
Validator<T> withMessage(String message);
FormValue<T> validate(T value);
}
It can be a generic validator:
public class GreaterThatOrEqualValidator<T extends Comparable<T>> implements Validator<T> {
private T minValue;
private String message;
GreaterThatOrEqualValidator(T minValue) {
this.minValue = minValue;
}
public Validator<T> withMessage(String message) {
this.message = message;
return this;
}
public FormValue<T> validate(T value) {
if (value.compareTo(minValue) >= 0) {
return FormValue.some(value);
}
return FormValue.none(message);
}
}
Or a domain-specific validator, e.g. for our Social Security Number:
public class SSNValidator implements Validator<SSN> {
private String message;
public Validator<SSN> withMessage(String message) {
this.message = message;
return this;
}
public FormValue<SSN> validate(SSN value) {
if (areaValid(value.areaNumber)
&& groupValid(value.groupNumber)
&& serialValid(value.serialNumber)) {
return FormValue.some(value);
}
return FormValue.none(message);
}
private boolean areaValid(int areaNumber) {
return areaNumber >= 0 && areaNumber <= 999;
}
private boolean groupValid(int groupNumber) {
return groupNumber > 0 && groupNumber <= 99;
}
private boolean serialValid(int serialNumber) {
return serialNumber > 0 && serialNumber <= 9999;
}
}
If you go back to the implementation of FormValue.validateWith, you may notice that Validator.validate will be called only for Some value. In case of None (resulting e.g. from a wrong conversion), validateWith returns the same None object.
Complete example – conversion + validation
We convert the SSN field to SSN and check if it’s valid. In case of errors (during conversion or validation), an error message will be displayed in a Toast.
public class MonadicValidatorDemoActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
findViewById(R.id.validate).setOnClickListener(
new View.OnClickListener() {
public void onClick(View v) {
validateForm();
}
}
);
}
private void validateForm() {
TextView ssnField = (TextView) findViewById(R.id.ssn);
String wrongSSNFormatMsg = getString(R.string.wrongSSNFormat);
String invalidSSNMsg = getString(R.string.invalidSSN);
FormValue<SSN> ssn = new SSNExtractor(ssnField).withMessage(wrongSSNFormatMsg)
.extractValue()
.validateWith(new SSNValidator().withMessage(invalidSSNMsg));
if (ssn.hasValue()) {
Toast.makeText(this, "SSN: " + ssn.value(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "Error: " + ssn.error(), Toast.LENGTH_LONG).show();
}
}
}
Chained and dependent validation
You can combine validators, adding more validateWith calls. Dependent validation is also possible:
- either by implementing a validator factory accepting a
FormValue and implementing a no-op or identity validator (NoOpValidator) – see the code example below
- or by implementing a
Validator verifying a FormValue, e.g. GreaterThatOrEqualValidator<T extends Comparable> implements Validator<FormValue>
private void validateForm() {
// Field and message finders omitted
FormValue<Date> startDate = new DateExtractor(startDateField).withMessage(invalidStartDateFormat)
.extractValue()
.validateWith(greaterThatOrEqual(today()).withMessage(startDateInThePast));
FormValue<Date> endDate = new DateExtractor(endDateField).withMessage(invalidEndDateFormat)
.extractValue()
.validateWith(greaterThatOrEqual(today()).withMessage(endDateInThePast));
.validateWith(greaterThatOrEqual(startDate).withMessage(startDateAfterEndDate));
// Do something with form values and validation errors
}
private <T extends Comparable<T>> Validator<T> greaterThatOrEqual(T value) {
return new GreaterThatOrEqualValidator<T>(value);
}
private <T extends Comparable<T>> Validator<T> greaterThatOrEqual(FormValue<T> value) {
if (value.hasValue()) {
return new GreaterThatOrEqualValidator<T>(value.value());
}
return new NoOpValidator<T>();
}
public class NoOpValidator<T> implements Validator<T> {
private String message;
@Override
public Validator<T> withMessage(String message) {
this.message = message;
return this;
}
public FormValue<T> validate(T value) {
return FormValue.some(value);
}
}
Is it really a monad?
If a type wants to be a monad, it has to have:
- unit operation that “wraps” a value with monad. It can be a constructor or a factory method. In our case
FormValue.some represents the unit operation
- bind operation transforming the monad into a next monad, exposing its internal value for a transformation function.
FormValue.validateWith does this. Because Java doesn’t have first-class functions, we represent the validateWith operation argument as a method object – Validator
Furthermore, a monad must follow three monadic laws:
- identity – transforming to unit doesn’t change a monad
m.bind { x -> unit(x) } ≡ m
Previously mentioned NoOpValidator implements such a function:
public FormValue<T> validate(T value) {
return FormValue.some(value);
}
In case of Some, calling validateWith (bind method) with this Validator will return a new Some monad holding the same value. None‘s implementation returns always the same monad instance. Hence the first law is fulfilled.
- unit – unit must preserve the value inside the monad
unit(x).bind(f) ≡ f(x)
unit(x) corresponds to FormValue.some(x). Result of the execution of Some.validateWith is equivalent to the execution of the provided Validator:
public FormValue<T> validateWith(Validator<T> validator) {
return validator.validate(value);
}
- associativity – order of monad composition doesn’t matter
m.bind(f).bind(g) ≡ m.bind{ x -> f(x).bind(g) }
Since None‘s validateWith returns always none (ignoring the bind function), it satisfies the associativity law.
For Some it’s not so obvious. Let’s expand the left part of the theorem:
some.validateWith(validator1).validateWith(validator2) => validator1.validate(value).validateWith(validator2)
In order to implement the function in Java from the right side of the 3rd monadid law, we create an anonymous Validator:
some.validateWith(
new Validator<T>() {
public FormValue<SSN> validate(SSN value) {
validator1.validate(value).validateWith(validator2)
}
}
);
As you can see the result of the validation in the anonymous class is the same as of the expression expanded above.
Conclusions
Monads look abstract, esoteric, and, I admit, a little scary. Although from the functional programming domain, they can be implemented in imperative languages. They solve real-life problems (converstion and validation) in an elegant manner (we chained converters and validators with few ifs and no null checking).