diff --git a/Tasks/Lazy/Lazy.Tests/Lazy.Tests.fsproj b/Tasks/Lazy/Lazy.Tests/Lazy.Tests.fsproj new file mode 100644 index 0000000..76fefe2 --- /dev/null +++ b/Tasks/Lazy/Lazy.Tests/Lazy.Tests.fsproj @@ -0,0 +1,28 @@ + + + + net9.0 + latest + false + false + + + + + + + + + + + + + + + + + + + + + diff --git a/Tasks/Lazy/Lazy.Tests/LazyTests.fs b/Tasks/Lazy/Lazy.Tests/LazyTests.fs new file mode 100644 index 0000000..1f78db2 --- /dev/null +++ b/Tasks/Lazy/Lazy.Tests/LazyTests.fs @@ -0,0 +1,107 @@ +module Lazy.Tests + +open NUnit.Framework +open FsUnit + +open System +open System.Threading +open System.Threading.Tasks + +let numberOfGets = 256 +let numberOfThreads = 256 + +let mutable evaluationCount = 0 + +let testSupplier supplier = fun () -> + Interlocked.Increment &evaluationCount |> ignore + supplier () + +let testObject1, testObject2 = new Object (), new Object () + +let suppliersWithResult: (unit -> obj) list = [ + fun () -> 80808; + fun () -> "qwertyuiop".Substring (0, 6); + fun () -> Math.Round (782.192, 1); + fun () -> + if evaluationCount < 10 then testObject1 + else testObject2; + fun () -> + if evaluationCount < 10 then testObject2 + else raise (new SystemException ()); +] + +let suppliersWithException: (unit -> obj) list = [ + fun () -> + raise (new InvalidOperationException ()) + 100; + fun () -> + raise (new ArgumentException ()) + "abc"; +] + +let testSuppliersWithResult = suppliersWithResult |> List.map testSupplier +let testSuppliersWithException = suppliersWithException |> List.map testSupplier + +let results: obj list = [80808; "qwerty"; 782.2; testObject1; testObject2] +let exceptions = [typeof; typeof] + +let testCasesWithResult = Seq.zip testSuppliersWithResult results |> Seq.map TestCaseData +let testCasesWithException = Seq.zip testSuppliersWithException exceptions |> Seq.map TestCaseData + +let testForSingleThread_WithResult (result: 'a) (countConstraint: int -> bool) (testLazy: ILazy<'a>) = + for _ in { 1 .. numberOfGets } do + testLazy.Get () |> should equal result + countConstraint evaluationCount |> should be True + +let testForSingleThread_WithException (exc: Type) (countConstraint: int -> bool) (testLazy: ILazy<'a>) = + for _ in { 1 .. numberOfGets } do + testLazy.Get |> should throw exc + countConstraint evaluationCount |> should be True + +[] +let resetEvaluationCount () = + evaluationCount <- 0 + +[] +let testSingleThreadLazy_WithResult (supplier: unit -> obj, result: obj) = + supplier + |> SingleThreadLazy + |> testForSingleThread_WithResult result (( = ) 1) + +[] +let testSingleThreadLazy_WithException (supplier: unit -> obj, exc: Type) = + supplier + |> SingleThreadLazy + |> testForSingleThread_WithException exc (( = ) 1) + +[] +let testThreadSafeLazy_WithResult (supplier: unit -> obj, result: obj) = + let testLazy = supplier |> ThreadSafeLazy + { 1 .. numberOfThreads } + |> Seq.map (fun _ -> Task.Run ( + fun () -> testForSingleThread_WithResult result (( = ) 1) testLazy)) + |> Task.WaitAll + +[] +let testThreadSafeLazy_WithException (supplier: unit -> obj, exc: Type) = + let testLazy = supplier |> ThreadSafeLazy + { 1 .. numberOfThreads } + |> Seq.map (fun _ -> Task.Run ( + fun () -> testForSingleThread_WithException exc (( = ) 1) testLazy)) + |> Task.WaitAll + +[] +let testLockFreeLazy_WithResult (supplier: unit -> obj, result: obj) = + let testLazy = supplier |> LockFreeLazy + { 1 .. numberOfThreads } + |> Seq.map (fun _ -> Task.Run ( + fun () -> testForSingleThread_WithResult result (( >= ) numberOfGets) testLazy)) + |> Task.WaitAll + +[] +let testLockFreeLazy_WithException (supplier: unit -> obj, exc: Type) = + let testLazy = supplier |> LockFreeLazy + { 1 .. numberOfThreads } + |> Seq.map (fun _ -> Task.Run ( + fun () -> testForSingleThread_WithException exc (( >= ) numberOfGets) testLazy)) + |> Task.WaitAll diff --git a/Tasks/Lazy/Lazy.Tests/Program.fs b/Tasks/Lazy/Lazy.Tests/Program.fs new file mode 100644 index 0000000..96b133f --- /dev/null +++ b/Tasks/Lazy/Lazy.Tests/Program.fs @@ -0,0 +1,4 @@ +module Program + +[] +let main _ = 0 diff --git a/Tasks/Lazy/Lazy.sln b/Tasks/Lazy/Lazy.sln new file mode 100644 index 0000000..e547247 --- /dev/null +++ b/Tasks/Lazy/Lazy.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Lazy", "Lazy\Lazy.fsproj", "{ACEDBB1D-CD4A-4C1E-B4AC-88257DD98315}" +EndProject +Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Lazy.Tests", "Lazy.Tests\Lazy.Tests.fsproj", "{7AE9BEB1-1946-4B5A-865A-2062DA8FCB00}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {ACEDBB1D-CD4A-4C1E-B4AC-88257DD98315}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACEDBB1D-CD4A-4C1E-B4AC-88257DD98315}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACEDBB1D-CD4A-4C1E-B4AC-88257DD98315}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACEDBB1D-CD4A-4C1E-B4AC-88257DD98315}.Release|Any CPU.Build.0 = Release|Any CPU + {7AE9BEB1-1946-4B5A-865A-2062DA8FCB00}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AE9BEB1-1946-4B5A-865A-2062DA8FCB00}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AE9BEB1-1946-4B5A-865A-2062DA8FCB00}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AE9BEB1-1946-4B5A-865A-2062DA8FCB00}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/Tasks/Lazy/Lazy/ILazy.fs b/Tasks/Lazy/Lazy/ILazy.fs new file mode 100644 index 0000000..6fef851 --- /dev/null +++ b/Tasks/Lazy/Lazy/ILazy.fs @@ -0,0 +1,6 @@ +namespace Lazy + +/// An interface representing a lazy evaluation. +type ILazy<'a> = + /// Get the result evaluated lazily. + abstract member Get: unit -> 'a diff --git a/Tasks/Lazy/Lazy/Lazy.fsproj b/Tasks/Lazy/Lazy/Lazy.fsproj new file mode 100644 index 0000000..59ac8c0 --- /dev/null +++ b/Tasks/Lazy/Lazy/Lazy.fsproj @@ -0,0 +1,15 @@ + + + + net9.0 + true + + + + + + + + + + diff --git a/Tasks/Lazy/Lazy/LockFreeLazy.fs b/Tasks/Lazy/Lazy/LockFreeLazy.fs new file mode 100644 index 0000000..956b89d --- /dev/null +++ b/Tasks/Lazy/Lazy/LockFreeLazy.fs @@ -0,0 +1,42 @@ +namespace Lazy + +open System +open System.Threading + +/// Class representing thread safe lazy evaluation without locks. +/// Creates an instance with the given `supplier` to evaluate. +type LockFreeLazy<'a when 'a: equality>(supplier: unit -> 'a) = + [] + let mutable supplier = Some supplier + let initialResult: 'a option * exn option = None, None + let mutable result = initialResult + + /// Run supplier and set the result if it was not already set, then clear the supplier. + let tryEvaluate () = + let currentSupplier = supplier + match currentSupplier with + | Some func -> + try + let localResult = Some (func ()) + Interlocked.CompareExchange (&result, (localResult, None), initialResult) + |> ignore + with + | e -> + Interlocked.CompareExchange (&result, (None, Some e), initialResult) + |> ignore + supplier <- None + | None -> () + + /// Get the result evaluated lazily. + let getResult () = + tryEvaluate () + match result with + | Some value, _ -> value + | _, Some e -> raise e + | _ -> raise (new ArgumentException "unexpected match pattern") + + interface ILazy<'a> with + member _.Get (): 'a = getResult () + + /// Get the result evaluated lazily. + member _.Get (): 'a = getResult () diff --git a/Tasks/Lazy/Lazy/SingleThreadLazy.fs b/Tasks/Lazy/Lazy/SingleThreadLazy.fs new file mode 100644 index 0000000..7d3d0ce --- /dev/null +++ b/Tasks/Lazy/Lazy/SingleThreadLazy.fs @@ -0,0 +1,35 @@ +namespace Lazy + +open System + +/// Class representing a lazy evaluation for a single thread. +/// Creates an instance with the given `supplier` to evaluate. +type SingleThreadLazy<'a when 'a: equality>(supplier: unit -> 'a) = + let mutable supplier = Some supplier + let mutable result: 'a option = None + let mutable thrownException: Exception option = None + + /// Run supplier if it was not already run, set result and clear the supplier. + let tryEvaluate () = + match supplier with + | Some func -> + try + result <- Some (func ()) + with + | e -> thrownException <- Some e + supplier <- None + | None -> () + + /// Get the result evaluated lazily. + let getResult () = + tryEvaluate () + match result, thrownException with + | Some value, _ -> value + | _, Some e -> raise e + | _ -> raise (new ArgumentException "unexpected match pattern") + + interface ILazy<'a> with + member _.Get (): 'a = getResult () + + /// Get the result evaluated lazily. + member _.Get (): 'a = getResult () diff --git a/Tasks/Lazy/Lazy/ThreadSafeLazy.fs b/Tasks/Lazy/Lazy/ThreadSafeLazy.fs new file mode 100644 index 0000000..44e43ba --- /dev/null +++ b/Tasks/Lazy/Lazy/ThreadSafeLazy.fs @@ -0,0 +1,39 @@ +namespace Lazy + +open System + +/// Class representing thread safe lazy evaluation. +/// Creates an instance with the given `supplier` to evaluate. +type ThreadSafeLazy<'a when 'a: equality>(supplier: unit -> 'a) = + [] + let mutable supplier = Some supplier + let mutable result: 'a option = None + let mutable thrownException: Exception option = None + let lockObject = new Object () + + /// Run supplier if it was not already run, set result and clear the supplier. + let tryEvaluate () = + lock lockObject (fun () -> + match supplier with + | Some func -> + try + result <- Some (func ()) + with + | e -> thrownException <- Some e + supplier <- None + | None -> () + ) + + /// Get the result evaluated lazily. + let getResult () = + tryEvaluate () + match result, thrownException with + | Some value, _ -> value + | _, Some e -> raise e + | _ -> raise (new ArgumentException "unexpected match pattern") + + interface ILazy<'a> with + member _.Get (): 'a = getResult () + + /// Get the result evaluated lazily. + member _.Get (): 'a = getResult ()