Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Functional Validation With Arrow - Mila...

Android Functional Validation With Arrow - Milan Kotlin Community Conf

You'll discover some basic notion of functional programming using Arrow with a concrete use case.

Daniele Campogiani

June 14, 2018
Tweet

More Decks by Daniele Campogiani

Other Decks in Programming

Transcript

  1. OPTION sealed class Option<out A> data class Some<out A>(val value:

    A) : Option<A>() object None : Option<Nothing>()
  2. OPTION - FOLD sealed class Option<out A> { fun <T>

    fold(ifNone: () -> T, ifSome: (A) -> T): T = when (this) { is Some -> ifSome(value) None -> ifNone() } }
  3. OPTION - FOLD val option: Option<String> = Some("Daniele") val message

    = option.fold( { "Hello Stranger" }, { "Hello $it" }) //Hello Daniele
  4. OPTION - MAP sealed class Option<out A> { fun <B>

    map(f: (A) -> B): Option<B> = when (this) { is Some -> Some(f(value)) None -> None } }
  5. OPTION - FLATMAP sealed class Option<out A> { fun <B>

    flatMap(f: (A) -> Option<B>): Option<B> = when (this) { is Some -> f(value) None -> None } }
  6. OPTION - FLATMAP fun toInt(input: String): Option<Int> = try {

    Some(Integer.parseInt(input)) } catch (e: NumberFormatException) { None } val result = Some("42").flatMap { toInt(it) } //Some(value=42)
  7. APPLICATIVE ▸ A way to put a value in the

    context: Some(value) ▸ Map ▸ FlatMap ▸ For experienced FP programmers: we are taking a shortcut here, just wait few slides :)
  8. OPTION - APPLICATIVE fun validateData(inputMail: String, inputNumber: String): Option<Data> {

    val mail = inputMail.optionMail() val number = inputNumber.optionPhoneNumber() return Option.applicative().map(mail, number) { Data(it.a, it.b) } }
  9. OPTION - VIEWMODEL class OptionViewModel : ViewModel() { private val

    mutableValidation: MutableLiveData<Option<Data>> = MutableLiveData() val validation: LiveData<Option<Data>> get() = mutableValidation fun validate(mail: String, phoneNumber: String) { val validationResult = validateData(mail, phoneNumber) mutableValidation.value = validationResult } }
  10. OPTION - ACTIVITY class OptionActivity : AppCompatActivity() { override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_input) val viewModel = ViewModelProviders.of(this)[OptionViewModel::class.java] signup_button.setOnClickListener { viewModel.validate(email.text.toString(), number.text.toString()) } viewModel.validation.observe(this, Observer { it?.fold(this::handleNone, this::handleSome) }) } }
  11. OPTION - ACTIVITY - HANDLE RESULTS fun handleNone() { val

    message = getString(R.string.option_wrong) showMessage(message) } fun handleSome(data: Data) { val message = "Mail: ${data.mail} Phone: ${data.phone}" showMessage(message) }
  12. EITHER sealed class Either<out A, out B> data class Left<out

    A>(val value: A) : Either<A, Nothing>() data class Right<out B>(val value: B) : Either<Nothing, B>()
  13. EITHER - MAP sealed class Either<out A, out B> {

    fun <C> map(f: (B) -> C): Either<A, C> = when (this) { is Left -> Left(value) is Right -> Right(f(value)) } }
  14. EITHER - MAP val input: Either<Throwable, Int> = Right(42) val

    result = input.map { it * 2 } //Right(value=84)
  15. EITHER - FLATMAP sealed class Either<out A, out B> fun

    <A, B, C> Either<A, B>.flatMap(f: (B) -> Either<A, C>): Either<A, C> = when (this) { is Left -> Left(value) is Right -> f(value) }
  16. EITHER - FLATMAP fun toInt(input: String): Either<NumberFormatException, Int> = try

    { Right(Integer.parseInt(input)) } catch (e: NumberFormatException) { Left(e) } val result = Right("42").flatMap { toInt(it) } //Right(value=42)
  17. EITHER - MAIL fun String.eitherMail(): Either<String, String> = when {

    validMail(this) -> Right(this) else -> Left("Invalid email") }
  18. EITHER - NUMBER fun String.eitherPhoneNumber(): Either<String, String> = when {

    validNumber(this) -> Right(this) else -> Left("Invalid phone number") }
  19. EITHER - APPLICATIVE fun validateData(inputMail: String, inputNumber: String): Either<String, Data>

    { val mail = inputMail.eitherMail() val number = inputNumber.eitherPhoneNumber() return Either.applicative<String>().map(mail, number) { Data(it.a, it.b) } }
  20. EITHER - VIEWMODEL class EitherViewModel : ViewModel() { private val

    mutableValidation: MutableLiveData<Either<String, Data>> = MutableLiveData() val validation: LiveData<Either<String, Data>> get() = mutableValidation fun validate(mail: String, phoneNumber: String) { mutableValidation.value = validateData(mail, phoneNumber) } }
  21. EITHER - ACTIVITY class EitherActivity : AppCompatActivity() { override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_input) val viewModel = ViewModelProviders.of(this)[EitherViewModel::class.java] signup_button.setOnClickListener { viewModel.validate(email.text.toString(), number.text.toString()) } viewModel.validation.observe(this, Observer { it?.fold(this::handleLeft, this::handleRight) }) } }
  22. EITHER - ACTIVITY - HANDLE RESULTS fun handleLeft(error: String) {

    showMessage(error) } fun handleRight(data: Data) { val message = "Mail: ${data.mail} Phone: ${data.phone}" showMessage(message) }
  23. VALIDATED sealed class Validated<out A, out B> data class Invalid<out

    A>(val value: A) : Validated<A, Nothing>() data class Valid<out B>(val value: B) : Validated<Nothing, B>()
  24. VALIDATED - MAP sealed class Validated<out A, out B> {

    fun <C> map(f: (B) -> C): Validated<A, C> = when (this) { is Invalid -> Invalid(value) is Valid -> Valid(f(value)) } }
  25. VALIDATED - MAP val input: Validated<List<String>, Int> = Valid(42) val

    result = input.map { it * 2 } //Valid(value=84)
  26. SEMIGROUP interface Semigroup<A> { fun combine(x: A, y: A): A

    } object StringSemigroup : Semigroup<String> { fun combine(x: String, y: String) = "$x $y" } class ListSemigroup<T> : Semigroup<List<T>> { fun combine(x: List<T>, y: List<T>) = x + y }
  27. VALIDATED - APPLY fun Validated<A, B>.ap( SE: Semigroup<A>, f: Validated<A,

    (B) -> C>): Validated<A, C> = fold( { e -> f.fold({ Invalid(SE.combine(it, e)) },{ Invalid(e) })}, { a -> f.fold(::Invalid, { Valid(it(a)) }) })
  28. APPLICATIVE ▸ A way to put a value in the

    context: ▸ Map ▸ FlatMap ▸ Apply ▸ FlatMap is more powerful than Apply: if a class has FlatMap, Apply can be written using FlatMap
  29. VALIDATED - MAIL fun String.validatedMail(): Validated<Nel<ValidationError>, String> = when {

    validMail(this) -> Valid(this) else -> Invalid(Nel(ValidationError.InvalidMail)) }
  30. VALIDATED - NUMBER private fun String.validatedPhoneNumber(): Validated<Nel<ValidationError>, String> = when

    { validNumber(this) -> Valid(this) else -> Invalid(Nel(ValidationError.InvalidPhoneNumber)) }
  31. VALIDATED - APPLICATIVE fun validateData(inputMail: String, inputNumber: String): Validated<Nel<ValidationError>, Data>

    { val mail = inputMail.validatedMail() val phoneNumber = inputNumber.validatedPhoneNumber() val SE = Nel.semigroup<ValidationError>() return Validated.applicative(SE).map(mail, phoneNumber) { Data(it.a, it.b) } }
  32. VALIDATED - VIEWMODEL class ValidatedNelValidationErrorViewModel : ViewModel() { private val

    mutableValidation: MutableLiveData<Validated<Nel<ValidationError>, Data>> = MutableLiveData() val validation: LiveData<Validated<Nel<ValidationError>, Data>> get() = mutableValidation fun validate(mail: String, phoneNumber: String) { mutableValidation.value = validateData(mail, phoneNumber) } }
  33. VALIDATED - ACTIVITY class ValidatedNelValidationErrorActivity : AppCompatActivity() { override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_input) val viewModel = ViewModelProviders.of(this)[ValidatedNelValidationErrorViewModel::class.java] signup_button.setOnClickListener { viewModel.validate(email.text.toString(), number.text.toString()) } viewModel.validation.observe(this, Observer { it?.fold(this::handleInvalid, this::handleValid) }) } }
  34. VALIDATED - ACTIVITY - HANDLE RESULTS fun handleInvalid(errors: Nel<ValidationError>) =

    errors.map { getInvalidField(it) }.forEach { it.second.error = it.first } fun getInvalidField(validationError: ValidationError): Pair<String, TextInputLayout> = when (validationError) { ValidationError.InvalidMail -> Pair(getString(R.string.invalid_mail), emailWrapper) ValidationError.InvalidPhoneNumber -> Pair(getString(R.string.invalid_phone_number), numberWrapper) }
  35. RECAP ▸ Each data type has his own logic ▸

    We can write less code combining existing methods ▸ This is just the beginning :)