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

ScalaでつくるInvalid stateのない世界

takkkun
July 02, 2016

ScalaでつくるInvalid stateのない世界

takkkun

July 02, 2016
Tweet

More Decks by takkkun

Other Decks in Programming

Transcript

  1. class Person < ActiveRecord::Base validates :name, { presence: true }

    validates :age, { presence: true, numericality: {greater_than_or_equal_to: 0} } end person = Person.new(name: "takkkun", age: -1) if (person.invalid?) { # handle person.errors } ΦϒδΣΫτΛόϦσʔγϣϯ͢Δ "DUJWF3FDPSEͷྫ
  2. ΦϒδΣΫτΛόϦσʔγϣϯ͢Δ 4DBMBͷྫ class Person(val name: String, val age: Int) {

    def validate(): List[String] = { var errors: List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } errors } def isValid(): Boolean = validate().isEmpty }
  3. ѻ͏ଆ͕όϦσʔγϣϯ࣮ߦͷ੹೚Λ࣋ͭ val person = new Person("takkkun", -1) if (!person.isValid) {

    val errors = person.validate() // handle errors } ѻ͏ଆ͕όϦσʔγϣϯΛݺͼग़͠ɺؾʹ͔͚Δඞཁ͕͋Δ val person = new Person("takkkun", -1) // handle person όϦσʔγϣϯΛݺͼग़͞ͳ͍ϛε΋ى͜ΓಘΔ def handlePerson(person: Person) { if (person.isValid) { // handle person } } ؔ਺ͳͲͰڥքΛ·͍ͨͩ৔߹΋ؾʹ͔͚Δඞཁ͕͋Δ
  4. class Person(val name: String, val age: Int) { var errors:

    List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } if (errors.nonEmpty) { throw new Exception(errors.mkString(", ")) } } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δ
  5. class Person(val name: String, val age: Int) { var errors:

    List[String] = List.empty if (name.isEmpty) { errors :+= "name cannot be blank" } if (age < 0) { errors :+= "age must be zero or a positive number" } if (errors.nonEmpty) { throw new Exception(errors.mkString(", ")) } } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δ όϦσʔγϣϯʹࣦഊͨ͠ͱ͖͸ྫ֎ͱ͢Δ
  6. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } }
  7. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } ίϯετϥΫλΛQSJWBUFʹ͢ΔʢΫϥεࣗମ͸QVCMJDʣ
  8. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } TQBXOϝιουܦ༝ͰͷΈ1FSTPOΫϥεͷΠϯελϯεΛੜ੒Մೳ
  9. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { // ... } } ܕΛม͑ͯ௚઀౉ͤͳ͍Α͏ʹ͢Δ
  10. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  11. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } ೚ҙͷܕʢ"ʣͷ஋ΛऔΔɻWBMͰम০͍ͯ͠ͳ͍ͷͰɺ֎෦ʹެ։͞Εͳ͍ 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  12. class Indefinite[A](value: A) { def validateWith(validateValue: A => List[String]): Validity[A]

    = { val errors = validateValue(value) if (errors.isEmpty) Valid(value) else Invalid(errors) } } sealed trait Validity[+A] { def value: A def errors: List[String] } case class Valid[A](value: A) extends Validity[A] { val errors = List.empty } case class Invalid(errors: List[String]) extends Validity[Nothing] { def value = throw new NoSuchElementException("Invalid") } WBMJEBUF8JUIϝιουͰ಺แ͍ͯ͠Δ஋ͷ༗ޮແޮΛ͔֬ΊΔ 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ
  13. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } }
  14. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } } WBMJEBUF8JUIʹόϦσʔγϣϯ༻ͷؔ਺Λ ౉͠ɺ಺แ͞Ε͍ͯΔ஋ͷ༗ޮແޮΛಘΔ
  15. 4DBMBͰ"MXBZTWBMJEΛ࣮ݱ͢Δվ class Person private (val name: String, val age: Int)

    object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Person = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => new Person(validName, validAge) case (validityName, validityAge) => throw new Exception((validityName.errors ++ validityAge.errors).mkString(", ")) } } private def validateName(name: String): List[String] = { if (name.isEmpty) List("name cannot be blank") else List.empty } private def validateAge(age: Int): List[String] = { if (age < 0) List("age must be zero or a positive number") else List.empty } } ύλʔϯϚονͰ͢΂ͯ༗ޮͷͱ͖ͷΈ1FSTPOΫϥεͷΠϯελϯεΛੜ੒͢Δ
  16. ࢖༻ྫ try { val name = new Indefinite("takkkun") val age

    = new Indefinite(-1) val person = Person.spawn(name, age) // handle person } catch { case e: Exception => // handle exception }
  17. &JUIFSόʔδϣϯ object Person { def spawn(name: Indefinite[String], age: Indefinite[Int]): Either[List[String],

    Person] = { (name.validateWith(validateName), age.validateWith(validateAge)) match { case (Valid(validName), Valid(validAge)) => Right(new Person(validName, validAge)) case (validityName, validityAge) => Left(validityName.errors ++ validityAge.errors) } } // ... }
  18. &JUIFSόʔδϣϯ࢖༻ྫ val name = new Indefinite("takkkun") val age = new

    Indefinite(-1) val errorsOrPerson = Person.spawn(name, age) errorsOrPerson match { case Left(errors) => // handle errors case Right(person) => // handle person }