$30 off During Our Annual Pro Sale. View Details »

ML 21: Experience Report: Domain Modeling with F#

ML 21: Experience Report: Domain Modeling with F#

I describe my experience using algebraic data types in F# for modeling and communication of domain concepts between a development team, domain experts, and other stakeholders. This experience was very successful, and I describe the reasons why F# is well suited for this.

Scott Wlaschin

August 25, 2021
Tweet

More Decks by Scott Wlaschin

Other Decks in Technology

Transcript

  1. ML 21: Experience Report: Domain Modeling with F# @ScottWlaschin fsharpforfunandprofit.com

  2. Languages with Mathematical focus Languages with Business focus Proof assistants

    FP languages COBOL Java/C# F#
  3. Business-focused development • Understanding the domain is critical – “Correctness”

    is a lesser goal • Responding to feedback – Need to adjust domain model quickly and safely
  4. F# for business-focused development • Readable by non-developers – And

    easy to collaborate with them • Composable types for domain modeling – And type checking makes it safe to refactor • Eliminating errors through design – “Make illegal states unrepresentable”
  5. Collaborating with non-developers (Domain-driven design)

  6.  Waterfall methodology

  7. Agile methodology

  8. Agile methodology

  9. Domain-Driven Design

  10. None
  11. None
  12. Can you really make code represent the domain?

  13. What non-developers think source code looks like

  14. Shared language What DDD source code should look like module

    CardGame = type Suit = Club | Diamond | Spade | Heart type Rank = Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand
  15. module CardGame = type Suit = Club | Diamond |

    Spade | Heart type Rank = Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand
  16. module CardGame = type Suit = Club | Diamond |

    Spade | Heart type Rank = Two | Three | Four | Five | Six | Seven | Eight | Nine | Ten | Jack | Queen | King type Card = Suit * Rank type Hand = Card list type Deck = Card list type Player = { Name:string; Hand:Hand } type Game = { Deck:Deck; Players: Player list } type Deal = Deck –› (Deck * Card) type PickupCard = (Hand * Card) –› Hand Non-programmers can provide useful feedback
  17. Rapid feedback during the design stage

  18. Creating the domain model is an interactive process Not just

    for developers. Everyone participates
  19. ... type Deck = Card list type Deal = Deck

    –› (Deck * Card)
  20. ... type Deck = Card list type Deal = ShuffledDeck

    –› (ShuffledDeck * Card)
  21. ... type Deck = Card list type Deal = ShuffledDeck

    –› (ShuffledDeck * Card) type ShuffledDeck = Card list
  22. ... type Deck = Card list type Deal = ShuffledDeck

    –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck
  23. ... type Deck = Card list type Deal = ShuffledDeck

    –› (ShuffledDeck * Card) type ShuffledDeck = Card list type Shuffle = Deck –› ShuffledDeck
  24. Domain modeling with an algebraic type system

  25. I explain that new types are built from smaller types

    by: • Composing with AND • Composing with OR (“choices”)
  26. Example of requirements: We accept three forms of payment: Cash,

    PayPal, or Card. For Cash we don't need any extra information For PayPal we need a email address For Cards we need a card type and card number
  27. type EmailAddress = string type CardNumber = string Implement by

    composing types, like this:
  28. type EmailAddress = ... type CardNumber = … type CardType

    = Visa | Mastercard type CreditCardInfo = { CardType : CardType CardNumber : CardNumber }
  29. type EmailAddress = ... type CardNumber = ... type CardType

    = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo
  30. type EmailAddress = ... type CardNumber = ... type CardType

    = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo
  31. type EmailAddress = ... type CardNumber = ... type CardType

    = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD
  32. type EmailAddress = ... type CardNumber = ... type CardType

    = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD type Payment = { Amount : PaymentAmount Currency : Currency Method : PaymentMethod }
  33. type EmailAddress = ... type CardNumber = ... type CardType

    = ... type CreditCardInfo = ... type PaymentMethod = | Cash | PayPal of EmailAddress | Card of CreditCardInfo type PaymentAmount = decimal type Currency = EUR | USD type Payment = { Amount : PaymentAmount Currency : Currency Method : PaymentMethod }
  34. Eliminating errors through design “Make illegal states unrepresentable”

  35. Business Rules: • Rule 1: If the email is changed,

    the verified flag must be reset to false. • Rule 2: The verified flag can only be set by a special verification service type EmailContactInfo = { EmailAddress : string IsEmailVerified : bool }
  36. type EmailAddress = EmailAddress of string // with associated smart

    constructor type VerifiedEmail = VerifiedEmail of EmailAddress type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail Here’s the refactored model
  37. type EmailAddress = EmailAddress of string // with associated smart

    constructor type VerifiedEmail = VerifiedEmail of EmailAddress type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  38. type EmailAddress = EmailAddress of string // with associated smart

    constructor type VerifiedEmail = VerifiedEmail of EmailAddress type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail
  39. type EmailAddress = EmailAddress of string // with associated smart

    constructor type VerifiedEmail = VerifiedEmail of EmailAddress type VerificationService = (EmailAddress * VerificationHash) –› VerifiedEmail option type EmailContactInfo = | Unverified of EmailAddress | Verified of VerifiedEmail Those business rules are automatically enforced by the design!
  40. Nice-to-have language improvements • Zero-cost wrappers (e.g. newtype) • An

    easier way to do “smart constructors” that perform validation • An easier way to enforce constraints (refinement types?) • A more fine-grained way of controlling access to constructors (e.g. the VerifiedEmail constructor)
  41. Summary • Algebraic types are excellent for business-focused domain modeling

    • Non-developers can read and contribute • Lots of potential for ML languages here! Thanks!