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

More safety via Refined Types

More safety via Refined Types

Translated version of the LambdaHRO talk "Mehr Sicherheit durch Refined Types"

Jens Grassel

November 16, 2021
Tweet

More Decks by Jens Grassel

Other Decks in Programming

Transcript

  1. More safety via Refined Types Translated version of ”Mehr Sicherheit

    durch Refined Types” Jens Grassel . . Wegtam GmbH
  2. What are Types? • simply data types • val myString:

    String • val myNumber: Int • and so on...
  3. Where is the problem? Everything is shiny, or is it?

    def parsePort(s: String): Int = s.toInt parsePort("Ooops!") def startSocket(port: Int): Unit = { val serverSocket = new ServerSocket(port) // ... } startSocket(parsePort("-1")) // Ooops... That is only one example of many.
  4. And what are Refined Types doing? We complement the available

    types by further constraints. type PortNumber = Int Refined Interval.Closed[W.‘1‘.T, W.‘65535‘.T] def startSocket(p: PortNumber): Unit = { val serverSocket = new ServerSocket(p) // ... } startSocket(-1) // Does not compile!
  5. But what do Refined Types exactly? They turn runtime errors

    into compilation errors! And more but that comes later...
  6. Reduction of the implementation space ( ) The stricter your

    types are, the easier is the implementation. final case class Service(host: String, port: Int) def hashName(name: String) versus final case class Service(host: String Refined IPv4, port: PortNumber) def hashName(name: String Refined NonEmpty)
  7. Reduction of the implementation space ( ) Example: Integration into

    an existing system • product data via external provider • mostly compatible • old system does only support article number larger than final case class ArtNo(n: Int) { require(n > 4096, "Illegal article number!") } This is horrible! Don’t do that!
  8. Reduction of the implementation space ( ) This is slightly

    better: final case class ArtNo(n: Int) object ArtNo { def apply(n: Int): Option[ArtNo] = if (n > 4096) Option(new ArtNo(n)) else None } But we can have this... :-) type ArtNo = Int Refined Greater[4096]
  9. Configuration files ( ) Configuration files offer a broad variety

    of potential sources of errors database { driver = "org.postgresql.Driver" url = "jdbc:postgresql://localhost/db" user = "username" pass = "secret" }
  10. Configuration files ( ) Safeguard via refined and pureconfig final

    case class DatabaseConfig(driver: DatabaseDriverName, url: DatabaseUrl, user: DatabaseLogin, pass: DatabasePassword) object DatabaseConfig { implicit val configReader: ConfigReader[DatabaseConfig] = deriveReader[DatabaseConfig] } ⇒ Validated at runtime! (No own code necessary!)
  11. Serialisation and Deserialisation similar to configuration problem domain • parse

    or create of different formats • nowadays mostly JSON • gain more safety by stronger typing of the models • integration of refined with Circe (JSON library) • Decoder and Encoder for free
  12. Less tests via better (stronger) typing ( ) What do

    we really test? • very often simply input values (allowed range) "Creating a user" when { "name is empty" must { "throw an exception" in { the [IllegalArgumentException] thrownBy createUser(name = "", age = 18) must have message "Name must be present!" } } }
  13. Less tests via better (stronger) typing ( ) type UserName

    = String Refined NonEmpty type UserAge = Int Refined Positive def createUser(n: UserName)(a: UserAge): User = ??? Problem solved! :-)
  14. General advice Avoid ”stringified programming”! final case class Foo(login: String,

    email: String, collectedStars: Int) versus final case class Foo(login: LoginName, email: EMail, stars: CollectedStars) The compiler can help you, so let it!