Android form conversion and validation with Option monad

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:

  1. identity – transforming to unit doesn’t change a monad
  2. 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.

  3. unit – unit must preserve the value inside the monad
  4. 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);
    }
    
  5. associativity – order of monad composition doesn’t matter
  6. 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).

    Advertisements