Separation of data and behavior models state with simple data types and puts operations in separate functions or modules. Rather than combining mutable fields and methods in a class, represent the current value directly and pass it to functions that return an updated value. This style favors composable, declarative code and keeps behavior independent of a type’s definition. section 2.1.3 section 2.8
A shopping basket without a stateful class
Model a basket as an immutable record containing its items. Place AddItem, Clear, and Checkout in a Basket module. Each operation accepts a basket, performs its work, and returns an updated basket instead of changing hidden internal state. The data remains a visible description of the basket; the module supplies the behaviors that use it. section 2.1.3
Use records, tuples, and unions to describe data shapes, then add simple functions only where a behavior is needed. This is especially useful when data changes shape over time: new behaviors can be created or removed without changing the source type. With immutability and expression-oriented code, attention can move from encapsulating mutable state to mapping data between shapes and handling boundaries such as databases and web APIs. section 10.1.2 section 10.7
A practical starting heuristic is:
- Treat incoming and domain data as data, not as classes that must own behavior. section 10.1.2
- Define records, tuples, or unions for the shape you need; use modules for operations over those values. section 10.1.2
- Prefer immutable values and functions that return replacements when an operation changes state. section 2.1.3 section 5.7
- Add members when OO features such as dot notation or overloading genuinely help, but begin with a clean split. section 10.1.2
The split does not require abandoning existing applications. In mixed F# and C# systems, keep F# expression-oriented, immutable, and organized around separate functions and data rather than imitating class-centered C# design. Introduce it through a horizontal tier such as data access, or implement a complete vertical slice in F# when avoiding cross-language compromise matters more. section 11.2.3
Figure 11.3 A horizontal tiers approach to introducing F# into a mixed-language application. Here, F# is used for all data access operations but is called from a C# layer on top.
A horizontal-tier layout illustrates one incremental adoption path: implement a shared data-access layer in F# while a C# layer calls it. section 11.2.3
