Notes: What Functional Programming Can Learn From Object-Oriented Programming by John De Goes

These are my abbreviated notes (Cliffsnotes) from watching the talk, What Functional Programming Can Learn From Object-Oriented Programming, by John De Goes. As a warning, the notes are incomplete, and just a reminder to me of some of the best and most important parts of the talk.

Relatively early on he notes that there are multiple ways to do Scala, but only one way to write Go code. This is both a pro and a con.

02: FP Challenges (about 22 minutes in)

  • "what Haskell is missing that hurts"
  • mentions "developer experience"
  • what’s GREAT about Haskell is that when it comes to DM, you have Sum types and Product types
    • credit card is a Product type, Payment Method is a Sum type
      • 2 different people will generally come up with the same solution
    • In Scala 2 you have the case class and sealed trait, and that’s it
    • this is great, because OOP is too much choice
      • interface? multiple interfaces? abstract classes?
      • he argues that it’s not possible to do good OOP DM

Modules:

  • module in Haskell is equivalent to a package name in Scala
  • modules in Haskell are not a first-class thing
    • mentions that a Postgres module should be able to be built from a SQL module
    • cannot have the modules you want without sub-typing
    • in Haskell you only have functions and data
  • Haskell is too simple in this case
    • code organization, architecture
  • you can fake modules and sub-typing in Haskell, but no one would use it (usability is important)

Dot Notation:

  • user.notify(notification)
  • called “type-directed name resolution”
  • "dot syntax"
  • why?
    • IDE autocomplete
    • helps a developer know where to look
    • you know the verbs for a given noun
    • also gives you namespacing
      • in Haskell you (almost) have to use global unique names

Haskell doesn’t have a notion of user-defined constructors

  • in Scala you can have as many constructors as you want
  • put them all in the companion object
  • All this stuff, these weaknesses, are about code organization:
  • code organization minimizes code maintenance
  • Haskell has weak tools for code organization
  • this is important for big applications (architecture)
  • good organization is HUGE for maintenance
    • a Haskell program is a collection of 10,000 functions
    • organization is limited to namespacing
  • so maybe there’s something interesting in OOP that can help with these problems

Leveraging FP and OOP (~34 minutes in)

FP gives us:

  • data (immutable)
    • benefits of immutable data are beyond dispute
  • purity
  • composition (requires purity)
    • all kinds of composition
    • lets you solve all kinds of problems
  • these are all "code maintenance tools"
    • they lower the cost of maintaining code

OOP gives us:

  • methods
  • constructors
    • many constructors
    • all located in one place
  • modules
  • these are all "code organization tools"

Combining the best of FP & OOP is what Scala gives us:

  • that’s a Pro
  • a Con is that there are different approaches, people do different things, fragments the community
  • Go (language) doesn’t have this problem, everyone writes it the same way
  • but what we CAN do is combine the best of FP & OOP
    • can create something unique
    • something true to Martin Odersky’s vision

Leveraging the best involves knowing when to use FP and when to use OOP:

  • DM == FP
  • DSLs == FP
  • Fine Grain Code Organization == OOP (methods, constructors)
  • Coarse Grain Code Organization == OOP (modules)

  • fine code organization

    • methods, constructors
    • starting around 42 minutes...
    • put functions of data on data classes
      • put the verbs on the nouns
      • IDE auto-complete
      • you can easily find all the methods
      • don’t do the Haskell way, with methods outside the classes
      • "." syntax
  • put all constructors in the companion object
    • should be a rule
    • companion objects should exist to construct things
      • don’t put anything else in a companion object
      • “invent a data type to put utility methods in its companion object”
      • this helps you find things, and know where to put things
    • do NOT use extension methods unless you have to
      • only third-party data types
  • adopt the Onion Architecture
    • an application is constructed in layers
      • outer: Infrastructure
        • Kubernetes, AWS, EmailService, SMSService, Kafka, etc.
      • middle: Middleware
        • UserRepoService, UserEventService, UserNotifyService
        • we don’t want to be talking about SMS and SMTP here
      • inner: Business logic
        • pure logic in your language
        • the basic Reminder is in here (in his example)

Point #3: model all services with interfaces/traits and ADTs

  • at about 48 minutes of the video
  • trait UserNotify { def notifyUser(user: User) ...}
  • use ADTs to model inputs and outputs to your services
    • final case class User(...)
    • final case class UserNotification(...)

Point #4: implement services with classes in terms of other services

  • think of it as a translation from a hi-level language to a lower-level language
  • you want to implement an inner layer in terms of the next layer outside
  • in OOP, in your outer layer, those are your "edge classes"; those are the ones you have to mock, or have test implementations for
  • in Scala, any implementation of any service should look like this:
final case class UserNotifyLive(
    email: Email,
    sms: SMS,
    push: Push
) extends UserNotifyInterface { ... }
  • the constructor takes interfaces, not classes
    • the interfaces are other services
    • btw, this is how you tell you’re in the middle of the Onion
      • at the edge of the Onion, your constructor list is empty
      • the Filesystem, the Socket // the edge of the Onion
      • services that don’t depend on other services
      • we typically stop before that because we have libraries for the edge

Summary:

  • don’t make things too complicated
  • just code to interfaces
    • as a beginner, you code to interfaces
    • in the intermediate, you do crazy things
    • as an expert, you code to interfaces
    • rule: never touch the outside world, except by calling methods on the interfaces that are passed into us
    • this is equivalent in reasoning power to "tagless final"
    • by doing this, you get the same “reasoning” capability in Java that you get with tagless final and ZIO (which is amazing!)

How do you do this in ZIO? (ZLayer):

  • the Onion is where the name ZLayer comes from
// same as OOP above
val userNotifyLive = ZLayer {
  for
      email <- ZIO.service[Email]
      sms   <- ZIO.service[SMS]
      push  <- ZIO.service[Push]
  yield
      UserNotifyLive(email, sms, push)
}
  • this is a simple example, hard to see the benefits, but as things get more complex, ZLayer scales however you need it to
    • layers scala with you
    • helps you follow this “best practice” architecture
    • he shows how to add a few things, and the basic ZLayer stuff doesn’t change (significantly)

Point #5: isolate Service Interfaces to separate compilation units

  • ~56 minutes in
  • put the interfaces somewhere else

Point #6: isolate Implementations to separate compilation units

  • /userrepo-jdbc/
    • src/
      • main/ ...
  • aspire to this (most of us do not do this)
  • adds a lot of overhead
  • but doing it can help you make decisions about when it’s time to break something up
  • imagine that creating another project is free, how would you really want to organize your code?
  • this project will depend on the interfaces
  • it will also depend on whatever this project needs
    • ex: jdbc, jdbc libs
  • the goal of #5 & #6 is to create a unidirectional graph where the interfaces are independent, they feed into the Service Implementations, and those implementations are deployed (in your application)
  • this is the Holy Grail of OOP
    • will minimize your pains in a big application

Recap/Summary

  • OOP & FP view the world differently
    • both have strengths and weaknesses
  • he thinks OOP brings you "Code Organization"
    • modules
    • swap out implementations (interfaces)
    • methods with "." syntax
    • constructors in one place
  • create something that is very powerful and very principled, you could write a book around what that style of FP looks like in Scala, and teach it, and say, this is how you do Scala/FP.

Teaching an opinionated way to do Scala is critical to growing Scala.

  • It can’t be “just do whatever you want.”
  • It’s easier to bring people on board now because we have a (mature) opinionated take on what it means to do FP in Scala.

If you’re doing data modeling, ignore the OOP stuff

  • in OOP DM there are too many choices, it’s too hard
  • choose FP for DSLs
  • choose OOP for code organization

After years of wrestling with Scala, he has come to appreciate the OOP features

  • he mentions code that “requires sub-typing” with the implication that this is a benefit of OOP

  • coarse grain code organization

    • modules