Skip to content
Merged

Lazy #11

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions Tasks/Lazy/Lazy.Tests/Lazy.Tests.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="LazyTests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="fsunit" Version="7.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="NUnit" Version="4.2.2" />
<PackageReference Include="NUnit.Analyzers" Version="4.4.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.6.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Lazy\Lazy.fsproj" />
</ItemGroup>

</Project>
107 changes: 107 additions & 0 deletions Tasks/Lazy/Lazy.Tests/LazyTests.fs
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>; typeof<ArgumentException>]

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

[<SetUp>]
let resetEvaluationCount () =
evaluationCount <- 0

[<TestCaseSourceAttribute(nameof(testCasesWithResult))>]
let testSingleThreadLazy_WithResult (supplier: unit -> obj, result: obj) =
supplier
|> SingleThreadLazy
|> testForSingleThread_WithResult result (( = ) 1)

[<TestCaseSourceAttribute(nameof(testCasesWithException))>]
let testSingleThreadLazy_WithException (supplier: unit -> obj, exc: Type) =
supplier
|> SingleThreadLazy
|> testForSingleThread_WithException exc (( = ) 1)

[<TestCaseSourceAttribute(nameof(testCasesWithResult))>]
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

[<TestCaseSourceAttribute(nameof(testCasesWithException))>]
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

[<TestCaseSourceAttribute(nameof(testCasesWithResult))>]
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

[<TestCaseSourceAttribute(nameof(testCasesWithException))>]
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
4 changes: 4 additions & 0 deletions Tasks/Lazy/Lazy.Tests/Program.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module Program

[<EntryPoint>]
let main _ = 0
28 changes: 28 additions & 0 deletions Tasks/Lazy/Lazy.sln
Original file line number Diff line number Diff line change
@@ -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
6 changes: 6 additions & 0 deletions Tasks/Lazy/Lazy/ILazy.fs
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions Tasks/Lazy/Lazy/Lazy.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<Compile Include="ILazy.fs" />
<Compile Include="SingleThreadLazy.fs" />
<Compile Include="ThreadSafeLazy.fs" />
<Compile Include="LockFreeLazy.fs" />
</ItemGroup>

</Project>
42 changes: 42 additions & 0 deletions Tasks/Lazy/Lazy/LockFreeLazy.fs
Original file line number Diff line number Diff line change
@@ -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) =
[<VolatileField>]
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 ()
35 changes: 35 additions & 0 deletions Tasks/Lazy/Lazy/SingleThreadLazy.fs
Original file line number Diff line number Diff line change
@@ -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 ()
39 changes: 39 additions & 0 deletions Tasks/Lazy/Lazy/ThreadSafeLazy.fs
Original file line number Diff line number Diff line change
@@ -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) =
[<VolatileField>]
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 ()