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

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

Avatar for takkkun takkkun
July 02, 2016

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

Avatar for takkkun

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 }