Real world examples and practical techniques for functional programming in C# without the jargon and theory.
In Functional Programming in C#, Second Edition you will learn how to:
* Use higher-order functions to reduce duplication and do more with less code
* Use pure functions to write code that is easy to test and optimize
* Write pleasant APIs that accurately describe your program's behavior
* Use dedicated types to handle nullability, system errors, and validation rules predictably and elegantly
* Write composable code without the overhead of an IoC container
Functional Programming in C# has helped thousands of developers apply functional thinking to C# code. Its practical examples and spot-on treatment of FP concepts makes it the perfect guide for proficient C# programmers. This second edition is fully revised to cover new functional-inspired features in the most recent releases of C#, including tuples, async streams, pattern matching, and records. Each chapter is packed with awesome perspectives and epiphany moments on how functional programming can change the way you code.
Author(s): Enrico Buonanno
Edition: 2
Publisher: Manning Publications
Year: 2021
Language: English
Commentary: true epub
Pages: 448
preface
acknowledgments
about this book
about the author
Part 1. Getting started
1 Introducing functional programming
1.1 What is this thing called functional programming?
Functions as first-class values
Avoiding state mutation
Writing programs with strong guarantees
1.2 How functional a language is C#?
The functional nature of LINQ
Shorthand syntax for coding functionally
Language support for tuples
Pattern matching and record types
1.3 What you will learn in this book
2 Thinking in functions
2.1 What’s a function, anyway?
Functions as maps
Representing functions in C#
2.2 Higher-order functions (HOFs)
Functions that depend on other functions
Adapter functions
Functions that create other functions
2.3 Using HOFs to avoid duplication
Exercises
3 Why function purity matters
3.1 What is function purity?
Purity and side effects
Strategies for managing side effects
Avoid mutating arguments
3.2 Enabling parallelization by avoiding state mutation
Pure functions parallelize well
Parallelizing impure functions
Avoiding state mutation
3.3 Purity and testability
Isolating I/O effects
A business validation scenario
Why testing impure functions is hard
3.4 Testing code that performs I/O
Object-oriented dependency injection
Testability without so much boilerplate
3.5 Purity and the evolution of computing
Exercises
Part 2. Core techniques
4 Designing function signatures and types
4.1 Designing function signatures
Writing functions signatures with arrow notation
How informative is a signature?
4.2 Capturing data with data objects
Primitive types are often not specific enough
Constraining inputs with custom types
Writing honest functions
Composing values into complex data objects
4.3 Modeling the absence of data with Unit
Why void isn’t ideal
Bridging the gap between Action and Func
5 Modeling the possible absence of data
5.1 The bad APIs you use every day
5.2 An introduction to the Option type
5.3 Implementing Option
An idealized implementation of Option
Consuming an Option
Creating a None
Creating a Some
Optimizing the Option implementation
5.4 Option as the natural result type of partial functions
Parsing strings
Looking up data in a collection
The smart constructor pattern
5.5 Dealing with null
Why null is such a terrible idea
Gaining robustness by using Option instead of null
Non-nullable reference types?
Bulletproof against NullReferenceException
Exercises
6 Patterns in functional programming
6.1 Applying a function to a structure’s inner values
Mapping a function onto a sequence
Mapping a function onto an Option
How Option raises the level of abstraction
Introducing functors
6.2 Performing side effects with ForEach
6.3 Chaining functions with Bind
Combining Option-returning functions
Flattening nested lists with Bind
Actually, it’s called a monad
The Return function
Relationship between functors and monads
6.4 Filtering values with Where
6.5 Combining Option and IEnumerable with Bind
6.6 Coding at different levels of abstraction
Regular vs. elevated values
Crossing levels of abstraction
Map vs. Bind, revisited
Working at the right level of abstraction
Exercises
7 Designing programs with function composition
7.1 Function composition
Brushing up on function composition
Method chaining
Composition in the elevated world
7.2 Thinking in terms of data flow
Using LINQ’s composable API
Writing functions that compose well
7.3 Programming workflows
A simple workflow for validation
Refactoring with data flow in mind
Composition leads to greater flexibility
7.4 An introduction to functional domain modeling
7.5 An end-to-end server-side workflow
Expressions vs. statements
Declarative vs. imperative
The functional take on layering
Exercises
Part 3. Functional designs
8 Functional error handling
8.1 A safer way to represent outcomes
Capturing error details with Either
Core functions for working with Either
Comparing Option and Either
8.2 Chaining operations that may fail
8.3 Validation: A perfect use case for Either
Choosing a suitable representation for errors
Defining an Either-based API
Adding validation logic
8.4 Representing outcomes to client applications
Exposing an Option-like interface
Exposing an Either-like interface
Returning a result DTO
8.5 Variations on the Either theme
Changing between different error representations
Specialized versions of Either
Refactoring to Validation and Exceptional
Leaving exceptions behind?
Exercises
9 Structuring an application with functions
9.1 Partial application: Supplying arguments piecemeal
Manually enabling partial application
Generalizing partial application
Order of arguments matters
9.2 Overcoming the quirks of method resolution
9.3 Curried functions: Optimized for partial application
9.4 Creating a partial-application-friendly API
Types as documentation
Particularizing the data access function
9.5 Modularizing and composing an application
Modularity in OOP
Modularity in FP
Mapping functions to API endpoints
Comparing the two approaches
9.6 Reducing a list to a single value
LINQ’s Aggregate method
Aggregating validation results
Harvesting validation errors
Exercises
10 Working effectively with multi-argument functions
10.1 Function application in the elevated world
Understanding applicatives
Lifting functions
An introduction to property-based testing
10.2 Functors, applicatives, and monads
10.3 The monad laws
Right identity
Left identity
Associativity
Using Bind with multi-argument functions
10.4 Improving readability by using LINQ with any monad
Using LINQ with arbitrary functors
Using LINQ with arbitrary monads
The LINQ clauses let, where, and others
10.5 When to use Bind vs. Apply
Validation with smart constructors
Harvesting errors with the applicative flow
Failing fast with the monadic flow
Exercises
11 Representing state and change
11.1 The pitfalls of state mutation
11.2 Understanding state, identity, and change
Some things never change
Representing change without mutation
11.3 Using records to capture the state of domain entities
Fine-grained control on record initialization
Immutable all the way down
11.4 Separating data and logic
12 A short introduction to functional data structures
12.1 The classic functional linked list
Common list operations
Modifying an immutable list
Destructuring any IEnumerable
12.2 Binary trees
Common tree operations
Structure sharing
12.3 In conclusion
Exercises
13 Event sourcing: A functional approach to persistence
13.1 Thinking functionally about data storage
Why data storage should be append-only
Relax and forget about storing state
13.2 Event sourcing basics
Representing events
Persisting events
Representing state
Representing state transitions
Reconstructing the current state from past events
13.3 Architecture of an event-sourced system
Handling commands
Handling events
Adding validation
Creating views of the data from events
13.4 Comparing different approaches to immutable storage
Datomic vs. Event Store
How event-driven is your domain?
Part 4. Advanced techniques
14 Lazy computations, continuations, and the beauty of monadic composition
14.1 The virtue of laziness
Lazy APIs for working with Option
Composing lazy computations
14.2 Exception handling with Try
Representing computations that may fail
Safely extracting information from a JSON object
Composing computations that may fail
Monadic composition: What does it mean?
14.3 Creating a middleware pipeline for DB access
Composing functions that perform setup/teardown
A recipe against the pyramid of doom
Capturing the essence of a middleware function
Implementing the query pattern for middleware
Adding middleware that times the operation
Adding middleware that manages a DB transaction
15 Stateful programs and stateful computations
15.1 Programs that manage state
Caching data in memory
Refactoring for testability and error handling
Stateful computations
15.2 A language for generating random data
Generating random integers
Generating other primitives
Generating complex structures
15.3 A general pattern for stateful computations
16 Working with asynchronous computations
16.1 Asynchronous computations
The need for asynchrony
Representing asynchronous operations with Task
Task as a container for a future value
Handling failure
An HTTP API for currency conversion
If it fails, try a few more times
Running asynchronous operations in parallel
16.2 Async streams
Reading from a file as an async stream
Consuming async streams functionally
Consuming data from several streams
Aggregation and sorting with async streams
17 Traversable and stacked monads
17.1 Traversables: Working with lists of elevated values
Validating a list of values with monadic Traverse
Harvesting validation errors with applicative Traverse
Applying multiple validators to a single value
Using Traverse with Task to await multiple results
Defining Traverse for single-value structures
17.2 Combining asynchrony and validation (or any other two monadic effects)
The problem of stacked monads
Reducing the number of effects
LINQ expressions with a monad stack
18 Data streams and the Reactive Extensions
18.1 Representing data streams with IObservable
A sequence of values in time
Subscribing to an IObservable
18.2 Creating IObservables
Creating a timer
Using Subject to tell an IObservable when it should signal
Creating IObservables from callback-based subscriptions
Creating IObservables from simpler structures
18.3 Transforming and combining data streams
Stream transformations
Combining and partitioning streams
Error handling with IObservable
Putting it all together
18.4 Implementing logic that spans multiple events
Detecting sequences of pressed keys
Reacting to multiple event sources
Notifying when an account becomes overdrawn
18.5 When should you use IObservable?
19 An introduction to message-passing concurrency
19.1 The need for shared mutable state
19.2 Understanding message-passing concurrency
Implementing agents in C#
Getting started with agents
Using agents to handle concurrent requests
Agents vs. actors
19.3 Functional APIs, agent-based implementations
Agents as implementation details
Hiding agents behind a conventional API
19.4 Message-passing concurrency in LOB applications
Using an agent to synchronize access to account data
Keeping a registry of accounts
An agent is not an object
Putting it all together
Appendix A. Working with previous version of C#
Epilogue. What next?
index