Last active
April 13, 2022 17:37
-
-
Save swlaschin/4ed2e4e8ea5b63c968bc469fbce620b5 to your computer and use it in GitHub Desktop.
Code examples from fsharpforfunandprofit.com/posts/dependencies-3/
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(* =================================== | |
Code from my series of posts "Six approaches to dependency injection" | |
=================================== *) | |
open System | |
(* ====================================================================== | |
4. Dependency Injection | |
Pass in dependencies AFTER other parameters | |
====================================================================== *) | |
type ILogger = | |
abstract Debug : string -> unit | |
abstract Info : string -> unit | |
abstract Error : string -> unit | |
let defaultLogger = {new ILogger with | |
member __.Debug str = printfn "DEBUG %s" str | |
member __.Info str = printfn "INFO %s" str | |
member __.Error str = printfn "ERROR %s" str | |
} | |
type IConsole = | |
abstract ReadLn : unit -> string | |
abstract WriteLn : string -> unit | |
let defaultConsole = {new IConsole with | |
member __.ReadLn() = Console.ReadLine() | |
member __.WriteLn str = printfn "%s" str | |
} | |
type ComparisonResult = | |
| Bigger | |
| Smaller | |
| Equal | |
//======================================== | |
// Class with constructor injection | |
//======================================== | |
module OODependencyInjection = | |
// "infrastructure services" passed in via the constructor | |
type StringComparisons(logger:ILogger) = | |
member __.CompareTwoStrings str1 str2 = | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
result | |
// create the logger | |
let logger : ILogger = defaultLogger | |
// construct the class | |
let stringComparisons = StringComparisons logger | |
// call the method | |
stringComparisons.CompareTwoStrings "a" "b" | |
//======================================== | |
// logging using a dependency parameter in last place | |
//======================================== | |
module FPInjection_DependencyInLastPlace = | |
let compareTwoStrings str1 str2 (logger:ILogger) = | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
result | |
//======================================== | |
// returning a function | |
//======================================== | |
module FPInjection_InterpretedAsReturningAFunction = | |
let compareTwoStrings str1 str2 = | |
fun (logger:ILogger) -> | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
result | |
// ================================================== | |
// Reader monad | |
// ================================================== | |
type Reader<'env,'a> = Reader of action:('env -> 'a) | |
module Reader = | |
/// Run a Reader with a given environment | |
let run env (Reader action) = | |
action env // simply call the inner function | |
/// Create a Reader which returns the environment itself | |
let ask = Reader id | |
/// Map a function over a Reader | |
let map f reader = | |
Reader (fun env -> f (run env reader)) | |
/// flatMap a function over a Reader | |
let bind f reader = | |
let newAction env = | |
let x = run env reader | |
run env (f x) | |
Reader newAction | |
/// Transform a Reader's environment from subtype to supertype. | |
let withEnv (f:'superEnv->'subEnv) reader = | |
Reader (fun superEnv -> (run (f superEnv) reader)) | |
// The new Reader environment is now "superEnv" | |
type ReaderBuilder() = | |
member __.Return(x) = Reader (fun _ -> x) | |
member __.Bind(x,f) = Reader.bind f x | |
member __.Zero() = Reader (fun _ -> ()) | |
// the builder instance | |
let reader = ReaderBuilder() | |
module FPInjection_ReaderMonad = | |
let compareTwoStrings str1 str2 : Reader<ILogger,ComparisonResult> = | |
fun (logger:ILogger) -> | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
result | |
|> Reader // <------------------ NEW!!! | |
module FPInjection_ReaderComputationExpression = | |
let compareTwoStrings str1 str2 = | |
reader { | |
let! (logger:ILogger) = Reader.ask | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
return result | |
} | |
let readFromConsole_bad() = | |
reader { | |
let! (console:IConsole) = Reader.ask | |
// let! (logger:ILogger) = Reader.ask // error | |
console.WriteLn "Enter the first value" | |
let str1 = console.ReadLn() | |
console.WriteLn "Enter the second value" | |
let str2 = console.ReadLn() | |
return str1,str2 | |
} | |
(* ====================================================================== | |
Like any monad, Readers can be chained, composed, etc | |
Approach 1 - using inheritance | |
====================================================================== *) | |
module ReaderComposition_v1 = | |
let readFromConsole() = | |
reader { | |
let! (console:#IConsole) = Reader.ask | |
let! (logger:#ILogger) = Reader.ask // OK | |
console.WriteLn "Enter the first value" | |
let str1 = console.ReadLn() | |
console.WriteLn "Enter the second value" | |
let str2 = console.ReadLn() | |
return str1,str2 | |
} | |
let compareTwoStrings str1 str2 = | |
reader { | |
let! (logger:#ILogger) = Reader.ask | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
return result | |
} | |
let writeToConsole (result:ComparisonResult) = | |
reader { | |
let! (console:#IConsole) = Reader.ask | |
match result with | |
| Bigger -> | |
console.WriteLn "The first value is bigger" | |
| Smaller -> | |
console.WriteLn "The first value is smaller" | |
| Equal -> | |
console.WriteLn "The values are equal" | |
} | |
// compose them together in a program | |
type IServices = | |
inherit ILogger | |
inherit IConsole | |
let program :Reader<IServices,_> = reader { | |
let! str1,str2 = readFromConsole() | |
let! result = compareTwoStrings str1 str2 | |
do! writeToConsole result | |
} | |
let services = | |
{ new IServices | |
interface IConsole with | |
member __.ReadLn() = defaultConsole.ReadLn() | |
member __.WriteLn str = defaultConsole.WriteLn str | |
interface ILogger with | |
member __.Debug str = defaultLogger.Debug str | |
member __.Info str = defaultLogger.Info str | |
member __.Error str = defaultLogger.Error str | |
} | |
// test | |
(* | |
open ReaderComposition_v1 | |
Reader.run services program | |
*) | |
(* ====================================================================== | |
Like any monad, Readers can be chained, composed, etc | |
Approach 2 - using withEnv | |
====================================================================== *) | |
module ReaderComposition_v2 = | |
let readFromConsole() = | |
reader { | |
// ask for an IConsole,ILogger pair | |
let! (console:IConsole),(logger:ILogger) = Reader.ask // a tuple | |
console.WriteLn "Enter the first value" | |
let str1 = console.ReadLn() | |
console.WriteLn "Enter the second value" | |
let str2 = console.ReadLn() | |
return str1,str2 | |
} | |
let compareTwoStrings str1 str2 = | |
reader { | |
// ask for an ILogger | |
let! (logger:ILogger) = Reader.ask | |
logger.Debug "compareTwoStrings: Starting" | |
let result = | |
if str1 > str2 then | |
Bigger | |
else if str1 < str2 then | |
Smaller | |
else | |
Equal | |
logger.Info (sprintf "compareTwoStrings: result=%A" result) | |
logger.Debug "compareTwoStrings: Finished" | |
return result | |
} | |
let writeToConsole (result:ComparisonResult) = | |
reader { | |
// ask for an IConsole | |
let! (console:IConsole) = Reader.ask | |
match result with | |
| Bigger -> | |
console.WriteLn "The first value is bigger" | |
| Smaller -> | |
console.WriteLn "The first value is smaller" | |
| Equal -> | |
console.WriteLn "The values are equal" | |
} | |
type Services = { | |
Logger : ILogger | |
Console : IConsole | |
} | |
(* | |
let program_bad = reader { | |
let! str1, str2 = readFromConsole() | |
let! result = compareTwoStrings str1 str2 // error | |
do! writeToConsole result // error | |
} | |
*) | |
let program = reader { | |
// helper functions to transform the environment | |
let getConsole services = services.Console | |
let getLogger services = services.Logger | |
let getConsoleAndLogger services = services.Console,services.Logger // a tuple | |
let! str1, str2 = | |
readFromConsole() | |
|> Reader.withEnv getConsoleAndLogger | |
let! result = | |
compareTwoStrings str1 str2 | |
|> Reader.withEnv getLogger | |
do! writeToConsole result | |
|> Reader.withEnv getConsole | |
} | |
let services = { | |
Console = defaultConsole | |
Logger = defaultLogger | |
} | |
// test | |
(* | |
open ReaderComposition_v2 | |
Reader.run services program | |
*) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment