:::info Functional Tools and Principles for LoB Apps ::: Note: - Title not Functional programming - not a singluar concept - it is a collection of tools and principles - the barrier of entry is very low - independently - at any place - at any time - by any amount - LoB Apps are apps that aremainly constrained by business logic complexity - Function tools and principles are great for reducing code complexity --- :::info Why functional tools and principles ::: - Easy to understand - Less artificial complexity - Easy to test - Bug free code - High productivity - Low barrier of entry Note: - demonstrate with examples - except bug free code can not be - demonstrated with examples - proven with reasoning - testimonies of many others and also my experiences --- :::info Write pure functions ::: ```c# void impure_divide() { var a = readA(); var b = readB(); var c = a / b; saveC(c); } ``` Note: - overly simplified example: division - to make the point obvious - Why is this function not pure - void - read db - write to db - business log code should be pure - extract a pure function to - test - reuse ---- **pure function** ```c# int divide(int a, int b) => a / b; ``` mental model of pure functions ```c# (input1, input2, ...) => (output1, output2, ...) ``` Note: - why is this pure - all data from input - all result to output - does not mutate input - no side effect - whether to create real types for the input and output - depend on whether it makes business sense - try not to polute type space with classes that do not carry significant meaning - not strictly pure, but more on this later --- :::info Push IO to the edge ::: ```c# var inputModel = Query.ReadData(); var outputModel = BusinessLogic.Calculate(inputModel) var _ = Repository.Save(outputModel); ``` Note: same thing as Write pure function - it is just a pratically ways of writting pure function while still doing IO which is necessary in real applications ---- **A fancier version of the code above with marginal benefit** ```c# var _ = IOMonad .ReadData() .Select(BusinessLogic.Calculate) .Select(Repository.SaveResult) .Evaluate() ``` ---- This is the functional way of implementing - **Domain Driven** - **Onion Architecture** - **Clean Architecture** ![](https://i.imgur.com/5Wcml3i.png) Note: - domain code should ignore IO - not injecting IO interfaces to domain code --- :::info Write immutable code ::: ```c# var aggregate = new Aggregate(); aggregate.AssignA(a); aggregate.AssignB(b); var c = aggregate.DivideResult(); aggregate.Save(); ``` Note: - hard to read - logic coded in aggregate hard to reuse - e.g. interest calculation: - assign rate - assign amount - assign duration - asks for interest ---- **mutable code in the real world** ```c# var aggregate = new Aggregate(); ... service1.Foo(aggregate) ... service2.Bar(aggregate); ... var c = aggregate.DivideResult(); ... aggregate.Save(); ``` Note: imaging those four lines are - in different files - embed among other code - executed in different threads ---- **immutable code** ```c# var c = a / b; ``` --- :::info Think in types ::: - C# classes can be - Actors - internal states - state mutating behaviours - Subset of real-world values - data that satisfy constraints Note: two fundamentally different views of what c# classes are - both fit for different applications - the actor view fits applications that maintain state in memory - single-player games, decktop apps without a DB backend - the data view fits applications that maintain state separate from their running code - the code becomes stateless data processors ---- - Example types in C#: - ```object``` represents all real world values - ```int``` represents a subset of realworld values - number - interger - Min/Max - ```uint``` represents another subset of number values - ```NonZeroInt``` is another subset of int - ```String```, ```UserEnteredEmailAddress```, ```VerifiedEmailAddress``` - ```Loan, SubventionLoan, RetailLoan, RetailPersonalLoan```, … Note: data that satisfy certain constraint/validation ---- **A Web app made up of collaborating actors** ![](https://i.imgur.com/H5Nz7Uw.png) [Image credit to Scott Wlaschin](https://www.slideshare.net/ScottWlaschin/reinventing-the-transaction-script-ndc-london-2020) ---- **A Web app made up of functions** ![](https://i.imgur.com/D7hgepQ.png) Note: - the difference is in complexity. - Functions calling each other are easier to understand than actors calling each other --- :::info Functional error handling ::: ```c# T ReadObjectFromDB<T>(int id, IQueryable<T> tbl) ``` Note: problem with this code - what if id is invalid - error cases are not specified - error handling is not enforced - a source of NullReferenceException - problem is not return null - problem is return null without telling the compiler/consumer ---- ```c# T ReadObjectFromDB<T>(int id, IQueryable<T> tbl) => tbl.First(x=>x.Id==id); ``` ```c# T ReadObjectFromDB<T>(int id, IQueryable<T> tbl) { var t = tbl.FirstOrDefault(x=>x.Id==id); if(t==null) { throw new InvalidOperationException("t is null") } return t; } ``` Note: problem with this code - the 2 implementation are the same - mixing system exceptions with business logic exceptions - impossible for caller to handle it meaningfully - handling exception is not enforced by compiler ---- **declare error cases to the caller and the compiler** ```c# T? ReadObject<T>(int id, IQueryable<T> tbl) ``` ```c# Option<T> ReadObject<T>(int id, IQueryable<T> tbl) ``` ```c# Exception<T> ReadObject<T>(int id, IQueryable<T> tbl) ``` ```c# Either<T, Exception> ReadObject<T>(int id, IQueryable<T> tbl) ``` ```c# (T | Exception) ReadObject<T>(int id, IQueryable<T> tbl) ``` Note: - distinguish between businesslogic and system exceptions - tell compiler about the error case - will uncover business cases - divide by zero, - null column in db - missing data in db, - invalid input data - how to handle business logic exceptions is often case by case - controller command handler vs event consumer - borrower/lender vs staff user - global handler is good for system exceptions - invalid user input vs invalid internal data ---- **anything wrong with this function?** ```c# int divide(int a, int b) => a / b; ``` Note: - it doesn't always return int - mixing exception types ---- **functions with different levels of purity** ```c# int pure_divide(int a, int b) { if(b==0) ...; return a/b; } ``` ```c# Exception<int> pure_divide(int a, int b) { if(b==0) ...; return a/b; } ``` Note: what to do when b==0 - throw domain specific exception with error that user(staff, eng) understand - watchout for exception throwing BCL calls - IEnumerable.First, IQueryable.Sum, IDictionary[] ---- ```c# int pure_divide(int a, NonZeroInt b) => return a/b; ``` Note: leave what to do to the caller - leave decision to the user - no need to aim for 100% purity - our aim is code simplicity, not mathematically proven code - but make sure business logic error handling is enforced --- :::info Unit of reuse should be functions ::: - Interfaces are for polymorphic entities - Service functions are stateless and independent - Interfaces for a group of independent functions do not make sense - Interfaces for functions are their signatures ---- - **Reuse by interface** - **Hidden dependency** - **Mocking in unit test** ```c# public interface IHolidayService { public bool IsHoliday(DateTime date); } public class HolidayService : IHolidayService { public bool IsHoliday(DateTime date) => false; } public class BusinessDayCalculationService { private readonly IHolidayService _holidayService; public BusinessDayCalculationService(IHolidayService holidayService) { _holidayService = holidayService; } public DateTime? NthBusinessDay(int year, int month, int n) { int cur = 0; var bom = new DateTime(year, month, 1); var eom = new DateTime(year, month, DateTime.DaysInMonth(year, month)); for (var date = bom; date <= eom; date = date.AddDays(1)) { if (!_holidayService.IsHoliday(date)) cur++; if (cur == n) return date; } return null; } } ``` ---- **reuse by function** ```c# public static class BusinessDayCalculator { public static DateTime? NthBusinessDay(int year, int month, int n, Func<DateTime, bool> isBDay) => new DateTime(year, month, 1) .DaysInRange(new DateTime(year, month, DateTime.DaysInMonth(year, month))) .Where(isBDay) .Skip(n - 1) .FirstOrNull(); } ``` Note: - composition of small functions - Ultimate SPR ---- :::info Testing of pure functions is easy ::: ```c# [Test] [TestCase(2022, 4, 1, "2022-4-1")] [TestCase(2022, 4, 2, "2022-4-4")] [TestCase(2022, 4, 32, "")] public void NthBusinessDay_Tests(int year, int month, int n, string expectedDateStr) { Func<DateTime, bool> isBDay = date => date.DayOfWeek != DayOfWeek.Saturday && date.DayOfWeek != DayOfWeek.Sunday; var result = BusinessDayCalculator.NthBusinessDay(year, month, n, isBDay); var expectedDate = expectedDateStr.ToDateTime(); Assert.AreEqual(expectedDate, result); } ``` Note: - no test setup - no mocking --- :::info C# has decent support for functional programming ::: - lambda makes functions first class - static functions should be the default - records have value semantics and are immutable - LINQ for composing functions - value tuple and generics for composing types Note: - value symantics means data is data ---- :::info C# missing features ::: - discriminated union ```type UserAnswer = "yes" | "no"``` - final initializer ```var newRcd = rcd with { Prop1 = "new value" } ``` - pipe operator ```var a = b |> func1 |> func2``` Note: - discriminated union - final initializer, no easy way of validating newRecord - `Valid<T>` --- # Thanks Questions?
{"metaMigratedAt":"2023-06-16T22:42:14.997Z","metaMigratedFrom":"YAML","title":"Thanks","breaks":true,"slideOptions":"{\"transition\":\"slide\",\"showNotes\":false,\"spotlight\":{\"enabled\":true}}","contributors":"[{\"id\":\"ebf2559a-7f75-44f9-b53b-65ad0b4ff14c\",\"add\":22379,\"del\":26197}]"}
    248 views