From 8a040f7de8971c44ab853e55a2966ea86546e902 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sat, 28 Aug 2021 22:31:15 +0300 Subject: [PATCH 01/16] Immutable dynamic object --- src/DynamicObj/DynamicObj.fsproj | 1 + src/DynamicObj/ImmutableDynamicObj.fs | 25 +++++++++++++++++++++++++ tests/UnitTests/ImmTests.fs | 25 +++++++++++++++++++++++++ tests/UnitTests/UnitTests.fsproj | 1 + 4 files changed, 52 insertions(+) create mode 100644 src/DynamicObj/ImmutableDynamicObj.fs create mode 100644 tests/UnitTests/ImmTests.fs diff --git a/src/DynamicObj/DynamicObj.fsproj b/src/DynamicObj/DynamicObj.fsproj index 49cf673..892fa30 100644 --- a/src/DynamicObj/DynamicObj.fsproj +++ b/src/DynamicObj/DynamicObj.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs new file mode 100644 index 0000000..87c2149 --- /dev/null +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -0,0 +1,25 @@ +module ImmutableDynamicObj + +type ImmutableDynamicObj (map : Map) = + + let properties = map + + member private this.Properties = properties + + new () = ImmutableDynamicObj Map.empty + + member this.Item + with get(index) = + this.Properties.[index] + + static member With name newValue (object : ImmutableDynamicObj) = + match Map.tryFind name object.Properties with + | Some(value) when value = newValue -> object + | _ -> ImmutableDynamicObj (Map.add name newValue object.Properties) + + override this.Equals o = + match o with + | :? ImmutableDynamicObj as other -> other.Properties = this.Properties + | _ -> false + + override this.GetHashCode () = ~~~map.GetHashCode() \ No newline at end of file diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs new file mode 100644 index 0000000..749cb91 --- /dev/null +++ b/tests/UnitTests/ImmTests.fs @@ -0,0 +1,25 @@ +module ImmTests + +open Xunit +open ImmutableDynamicObj + +[] +let ``No mutation test 1`` () = + let obj1 = + ImmutableDynamicObj () + |> ImmutableDynamicObj.With "aa" 5 + let obj2 = + ImmutableDynamicObj () + |> ImmutableDynamicObj.With "bb" 10 + + let objBase = ImmutableDynamicObj () + let objA = + objBase + |> ImmutableDynamicObj.With "aa" 5 + let objB = + objBase + |> ImmutableDynamicObj.With "bb" 10 + Assert.Equal(obj1, objA) + Assert.Equal(obj2, objB) + Assert.True((obj1 = objA)) + Assert.True((obj2 = objB)) diff --git a/tests/UnitTests/UnitTests.fsproj b/tests/UnitTests/UnitTests.fsproj index 1963190..e337e90 100644 --- a/tests/UnitTests/UnitTests.fsproj +++ b/tests/UnitTests/UnitTests.fsproj @@ -8,6 +8,7 @@ + From c771d73988b3a1654fa93ed453ff08fef03edba3 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 09:58:36 +0300 Subject: [PATCH 02/16] Operator for With, TryGetTypedValue added --- src/DynamicObj/ImmutableDynamicObj.fs | 10 ++++++++++ tests/UnitTests/ImmTests.fs | 23 +++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 87c2149..d22a953 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -17,6 +17,16 @@ type ImmutableDynamicObj (map : Map) = | Some(value) when value = newValue -> object | _ -> ImmutableDynamicObj (Map.add name newValue object.Properties) + static member (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + + member this.TryGetTypedValue<'a> name = + match (this.Properties.TryFind name) with + | None -> None + | Some o -> + match o with + | :? 'a as o -> o |> Some + | _ -> None + override this.Equals o = match o with | :? ImmutableDynamicObj as other -> other.Properties = this.Properties diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index 749cb91..cdc7d4b 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -21,5 +21,28 @@ let ``No mutation test 1`` () = |> ImmutableDynamicObj.With "bb" 10 Assert.Equal(obj1, objA) Assert.Equal(obj2, objB) + + Assert.True((obj1 = objA)) + Assert.True((obj2 = objB)) + +[] +let ``No mutation test 1 with operators`` () = + let obj1 = + ImmutableDynamicObj () + += ("aa", 5) + let obj2 = + ImmutableDynamicObj () + += ("bb", 10) + + let objBase = ImmutableDynamicObj () + let objA = + objBase + += ("aa", 5) + let objB = + objBase + += ("bb", 10) + Assert.Equal(obj1, objA) + Assert.Equal(obj2, objB) + Assert.True((obj1 = objA)) Assert.True((obj2 = objB)) From d741cd8c2057a420aec7174f2e863199d122b1f4 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 12:15:19 +0300 Subject: [PATCH 03/16] Docs and tests added --- src/DynamicObj/ImmutableDynamicObj.fs | 44 ++++++++++++++++++++--- tests/UnitTests/ImmTests.fs | 52 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index d22a953..198d1fa 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -1,26 +1,62 @@ module ImmutableDynamicObj +open DynamicObj + +/// Represents an DynamicObj's counterpart +/// with immutability enabled only. type ImmutableDynamicObj (map : Map) = let properties = map member private this.Properties = properties + member private this.NewIfNeeded map = + if obj.ReferenceEquals(map, this.Properties) then + this + else + ImmutableDynamicObj map + + /// Empty instance new () = ImmutableDynamicObj Map.empty + /// Indexes ; if no key found, throws member this.Item with get(index) = this.Properties.[index] + /// Returns an instance with: + /// 1. this property added if it wasn't present + /// 2. this property updated otherwise static member With name newValue (object : ImmutableDynamicObj) = - match Map.tryFind name object.Properties with - | Some(value) when value = newValue -> object - | _ -> ImmutableDynamicObj (Map.add name newValue object.Properties) + object.Properties + |> Map.add name newValue + |> object.NewIfNeeded + + /// Returns an instance: + /// 1. the same if there was no requested property + /// 2. without the requested property if there was + static member Without name (object : ImmutableDynamicObj) = + object.Properties + |> Map.remove name + |> object.NewIfNeeded + /// Returns an instance with: + /// 1. this property added if it wasn't present + /// 2. this property updated otherwise static member (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + /// Returns an instance: + /// 1. the same if there was no requested property + /// 2. without the requested property if there was + static member (-=) (object, name) = ImmutableDynamicObj.Without name object + + member this.TryGetValue name = + match properties.TryGetValue name with + | true, value -> Some value + | _ -> ReflectionUtils.tryGetPropertyValue this name + member this.TryGetTypedValue<'a> name = - match (this.Properties.TryFind name) with + match this.TryGetValue name with | None -> None | Some o -> match o with diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index cdc7d4b..a1ae126 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -46,3 +46,55 @@ let ``No mutation test 1 with operators`` () = Assert.True((obj1 = objA)) Assert.True((obj2 = objB)) + +[] +let ``Deterministic 1`` () = + let obj1 = + ImmutableDynamicObj () + += ("aaa", 5) + += ("bbb", "ccc") + + let obj2 = + ImmutableDynamicObj () + += ("aaa", 5) + += ("bbb", "ccc") + + Assert.Equal(obj1, obj2) + +[] +let ``Determinstic 2`` () = + let obj1 = + ImmutableDynamicObj () + += ("aaa", 5) + += ("bbb", "ccc") + -= "aaa" + + let obj2 = + ImmutableDynamicObj () + += ("bbb", "ccc") + + Assert.Equal(obj1, obj2) + +[] +let ``Determinstic 3`` () = + let obj1 = + ImmutableDynamicObj () + -= "aaa" + += ("bbb", "ccc") + + let obj2 = + ImmutableDynamicObj () + += ("bbb", "ccc") + + Assert.Equal(obj1, obj2) + +[] +let ``Non-equal test 1`` () = + let obj1 = + ImmutableDynamicObj () + + let obj2 = + ImmutableDynamicObj () + += ("quack", 5) + + Assert.NotEqual(obj1, obj2) \ No newline at end of file From d29b7ce481d86107cc06e6f5808d253a9cea7a72 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 12:52:24 +0300 Subject: [PATCH 04/16] Type conservation added --- src/DynamicObj/ImmutableDynamicObj.fs | 32 ++++++++++++++++----------- tests/UnitTests/ImmTests.fs | 21 +++++++++++++++++- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 198d1fa..2ca6f69 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -6,19 +6,25 @@ open DynamicObj /// with immutability enabled only. type ImmutableDynamicObj (map : Map) = - let properties = map + let mutable properties = map member private this.Properties = properties + member private this.ForceReplaceMap map = + properties <- map - member private this.NewIfNeeded map = - if obj.ReferenceEquals(map, this.Properties) then - this + static member inline private NewIfNeeded (a : ^ImmutableDynamicObj) map : ^ImmutableDynamicObj = + if obj.ReferenceEquals(map, (a :> ImmutableDynamicObj).Properties) then + a else - ImmutableDynamicObj map + let res = new ^ImmutableDynamicObj () + res.ForceReplaceMap map + res /// Empty instance new () = ImmutableDynamicObj Map.empty + + /// Indexes ; if no key found, throws member this.Item with get(index) = @@ -27,28 +33,28 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member With name newValue (object : ImmutableDynamicObj) = - object.Properties + static member inline With name newValue (object : ^ImmutableDynamicObj) = + (object :> ImmutableDynamicObj).Properties |> Map.add name newValue - |> object.NewIfNeeded + |> ImmutableDynamicObj.NewIfNeeded object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member Without name (object : ImmutableDynamicObj) = - object.Properties + static member inline Without name (object : ^ImmutableDynamicObj) = + (object :> ImmutableDynamicObj).Properties |> Map.remove name - |> object.NewIfNeeded + |> ImmutableDynamicObj.NewIfNeeded object /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + static member inline (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member (-=) (object, name) = ImmutableDynamicObj.Without name object + static member inline (-=) (object, name) = ImmutableDynamicObj.Without name object member this.TryGetValue name = match properties.TryGetValue name with diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index a1ae126..66736db 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -97,4 +97,23 @@ let ``Non-equal test 1`` () = ImmutableDynamicObj () += ("quack", 5) - Assert.NotEqual(obj1, obj2) \ No newline at end of file + Assert.NotEqual(obj1, obj2) + +type Quack () = + inherit ImmutableDynamicObj () + +[] +let ``Type preserved 1`` () = + let obj1 = + Quack () + += ("aaa", 5) + Assert.IsType(obj1) + +[] +let ``Type preserved 2`` () = + let obj1 = + Quack () + += ("aaa", 5) + -= "aaa" + += ("bbb", 5) + Assert.IsType(obj1) From 673097144631388053f196b820da55311eb6fb88 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 15:03:38 +0300 Subject: [PATCH 05/16] Attempt --- src/DynamicObj/ImmutableDynamicObj.fs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 2ca6f69..10d3e57 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -8,8 +8,10 @@ type ImmutableDynamicObj (map : Map) = let mutable properties = map - member private this.Properties = properties - member private this.ForceReplaceMap map = + // they're public, but because they're inline, + // they won't be visible from other assemblies + member inline this.Properties = properties + member inline this.ForceReplaceMap map = properties <- map static member inline private NewIfNeeded (a : ^ImmutableDynamicObj) map : ^ImmutableDynamicObj = @@ -49,7 +51,7 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member inline (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + static member (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object /// Returns an instance: /// 1. the same if there was no requested property From b6c92553dcf9de2f328c5f9a3f7fc68c6775c043 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 16:30:06 +0300 Subject: [PATCH 06/16] F# and C# now work --- DynamicObj.sln | 16 +++- src/DynamicObj/ImmutableDynamicObj.fs | 76 ++++++++++++++----- tests/CSharpTests/CSharpTests.csproj | 26 +++++++ tests/CSharpTests/InteropWorks.cs | 25 +++++++ tests/UnitTests/ImmTests.fs | 101 +++++++++++++++++++------- 5 files changed, 199 insertions(+), 45 deletions(-) create mode 100644 tests/CSharpTests/CSharpTests.csproj create mode 100644 tests/CSharpTests/InteropWorks.cs diff --git a/DynamicObj.sln b/DynamicObj.sln index 0fb0d2b..c607320 100644 --- a/DynamicObj.sln +++ b/DynamicObj.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.31515.178 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31521.260 MinimumVisualStudioVersion = 10.0.40219.1 Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "DynamicObj", "src\DynamicObj\DynamicObj.fsproj", "{B8BF1554-AAC3-434E-9502-FC83B43F3704}" EndProject @@ -38,6 +38,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8 docs\index.fsx = docs\index.fsx EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A-3A42-4E46-B233-B64F5C22524B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,10 +56,18 @@ Global {D009964D-9408-4344-B610-B73F54FE2A86}.Debug|Any CPU.Build.0 = Debug|Any CPU {D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.ActiveCfg = Release|Any CPU {D009964D-9408-4344-B610-B73F54FE2A86}.Release|Any CPU.Build.0 = Release|Any CPU + {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {D009964D-9408-4344-B610-B73F54FE2A86} = {988D804A-3A42-4E46-B233-B64F5C22524B} + {D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D} = {988D804A-3A42-4E46-B233-B64F5C22524B} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {6F5C3597-4524-4A4E-94EC-44857BD0BCEC} EndGlobalSection diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 10d3e57..7b657f2 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -1,6 +1,8 @@ -module ImmutableDynamicObj +namespace DynamicObj open DynamicObj +open System +open System.Runtime.CompilerServices /// Represents an DynamicObj's counterpart /// with immutability enabled only. @@ -10,17 +12,20 @@ type ImmutableDynamicObj (map : Map) = // they're public, but because they're inline, // they won't be visible from other assemblies - member inline this.Properties = properties - member inline this.ForceReplaceMap map = - properties <- map + member this.Properties = properties + + static member private NewIfNeededCLSCompliant (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = + if obj.ReferenceEquals(map, object.Properties) then + object + else + Activator.CreateInstance(typeof<'a>, [| map :> obj |]) :?> 'a - static member inline private NewIfNeeded (a : ^ImmutableDynamicObj) map : ^ImmutableDynamicObj = - if obj.ReferenceEquals(map, (a :> ImmutableDynamicObj).Properties) then - a + static member inline NewIfNeeded (object : ^T when ^T :> ImmutableDynamicObj) map : ^T = + if obj.ReferenceEquals(map, object.Properties) then + object else - let res = new ^ImmutableDynamicObj () - res.ForceReplaceMap map - res + (^T: (new: Map -> ^T) map) + /// Empty instance new () = ImmutableDynamicObj Map.empty @@ -35,28 +40,46 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member inline With name newValue (object : ^ImmutableDynamicObj) = - (object :> ImmutableDynamicObj).Properties + /// Use With from F#. This one is only for non-F# code. + static member WithCLSCompliant name newValue (object : 'a when 'a :> ImmutableDynamicObj) = + object.Properties + |> Map.add name newValue + |> ImmutableDynamicObj.NewIfNeededCLSCompliant object + + /// Returns an instance with: + /// 1. this property added if it wasn't present + /// 2. this property updated otherwise + static member inline With name newValue (object : ^T when ^T :> ImmutableDynamicObj) : ^T = + object.Properties |> Map.add name newValue |> ImmutableDynamicObj.NewIfNeeded object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member inline Without name (object : ^ImmutableDynamicObj) = - (object :> ImmutableDynamicObj).Properties + /// Use Without from F#. This one is only for non-F# code. + static member WithoutCLSCompliant name (object : 'a when 'a :> ImmutableDynamicObj) = + object.Properties + |> Map.remove name + |> ImmutableDynamicObj.NewIfNeededCLSCompliant object + + /// Returns an instance: + /// 1. the same if there was no requested property + /// 2. without the requested property if there was + static member inline Without name (object : ^T when ^T :> ImmutableDynamicObj) = + object.Properties |> Map.remove name |> ImmutableDynamicObj.NewIfNeeded object /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member (+=) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + static member inline (++) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member inline (-=) (object, name) = ImmutableDynamicObj.Without name object + static member inline (--) (object, name) = ImmutableDynamicObj.Without name object member this.TryGetValue name = match properties.TryGetValue name with @@ -76,4 +99,23 @@ type ImmutableDynamicObj (map : Map) = | :? ImmutableDynamicObj as other -> other.Properties = this.Properties | _ -> false - override this.GetHashCode () = ~~~map.GetHashCode() \ No newline at end of file + override this.GetHashCode () = ~~~map.GetHashCode() + +[] +type ImmutableDynamicObjExtensions = + + /// Returns an instance with: + /// 1. this property added if it wasn't present + /// 2. this property updated otherwise + /// use this one only from C# + [] + static member With (this : 'a when 'a :> ImmutableDynamicObj) (name, newValue) = + ImmutableDynamicObj.WithCLSCompliant name newValue this + + /// Returns an instance: + /// 1. the same if there was no requested property + /// 2. without the requested property if there was + /// use this one only from C# + [] + static member Without (this : 'a when 'a :> ImmutableDynamicObj, name) = + ImmutableDynamicObj.WithoutCLSCompliant name this \ No newline at end of file diff --git a/tests/CSharpTests/CSharpTests.csproj b/tests/CSharpTests/CSharpTests.csproj new file mode 100644 index 0000000..be1ad47 --- /dev/null +++ b/tests/CSharpTests/CSharpTests.csproj @@ -0,0 +1,26 @@ + + + + net5.0 + + false + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/CSharpTests/InteropWorks.cs b/tests/CSharpTests/InteropWorks.cs new file mode 100644 index 0000000..5ba7ab2 --- /dev/null +++ b/tests/CSharpTests/InteropWorks.cs @@ -0,0 +1,25 @@ +using System; +using Xunit; +using DynamicObj; + +namespace CSharpTests +{ + public class InteropWorks + { + [Fact] + public void Test1() + { + var obj1 = + new ImmutableDynamicObj() + .With("aa", 4) + .With("bb", 10) + .Without("aa") + ; + var obj2 = + new ImmutableDynamicObj() + .With("bb", 10) + ; + Assert.Equal(obj1, obj2); + } + } +} diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index 66736db..affb189 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -1,7 +1,7 @@ -module ImmTests +module Tests.ImmTests open Xunit -open ImmutableDynamicObj +open DynamicObj [] let ``No mutation test 1`` () = @@ -29,18 +29,18 @@ let ``No mutation test 1`` () = let ``No mutation test 1 with operators`` () = let obj1 = ImmutableDynamicObj () - += ("aa", 5) + ++ ("aa", 5) let obj2 = ImmutableDynamicObj () - += ("bb", 10) + ++ ("bb", 10) let objBase = ImmutableDynamicObj () let objA = objBase - += ("aa", 5) + ++ ("aa", 5) let objB = objBase - += ("bb", 10) + ++ ("bb", 10) Assert.Equal(obj1, objA) Assert.Equal(obj2, objB) @@ -51,13 +51,13 @@ let ``No mutation test 1 with operators`` () = let ``Deterministic 1`` () = let obj1 = ImmutableDynamicObj () - += ("aaa", 5) - += ("bbb", "ccc") + ++ ("aaa", 5) + ++ ("bbb", "ccc") let obj2 = ImmutableDynamicObj () - += ("aaa", 5) - += ("bbb", "ccc") + ++ ("aaa", 5) + ++ ("bbb", "ccc") Assert.Equal(obj1, obj2) @@ -65,13 +65,13 @@ let ``Deterministic 1`` () = let ``Determinstic 2`` () = let obj1 = ImmutableDynamicObj () - += ("aaa", 5) - += ("bbb", "ccc") - -= "aaa" + ++ ("aaa", 5) + ++ ("bbb", "ccc") + -- "aaa" let obj2 = ImmutableDynamicObj () - += ("bbb", "ccc") + ++ ("bbb", "ccc") Assert.Equal(obj1, obj2) @@ -79,12 +79,12 @@ let ``Determinstic 2`` () = let ``Determinstic 3`` () = let obj1 = ImmutableDynamicObj () - -= "aaa" - += ("bbb", "ccc") + -- "aaa" + ++ ("bbb", "ccc") let obj2 = ImmutableDynamicObj () - += ("bbb", "ccc") + ++ ("bbb", "ccc") Assert.Equal(obj1, obj2) @@ -95,25 +95,74 @@ let ``Non-equal test 1`` () = let obj2 = ImmutableDynamicObj () - += ("quack", 5) + ++ ("quack", 5) Assert.NotEqual(obj1, obj2) -type Quack () = - inherit ImmutableDynamicObj () +type Quack (map) = + inherit ImmutableDynamicObj (map) + + new() = Quack (Map.empty) + + +[] +let ``Type preserved F# 1`` () = + let obj1 = + Quack () + ++ ("aaa", 5) + Assert.IsType(obj1) [] -let ``Type preserved 1`` () = +let ``Type preserved F# 2`` () = let obj1 = Quack () - += ("aaa", 5) + ++ ("aaa", 5) + -- "aaa" + ++ ("bbb", 5) Assert.IsType(obj1) [] -let ``Type preserved 2`` () = +let ``Type preserved CLS Compliant 1`` () = let obj1 = Quack () - += ("aaa", 5) - -= "aaa" - += ("bbb", 5) + |> Quack.WithCLSCompliant "aaa" 5 Assert.IsType(obj1) + +[] +let ``Type preserved CLS Compliant 2`` () = + let obj1 = + Quack () + |> Quack.WithCLSCompliant "aaa" 5 + |> Quack.WithoutCLSCompliant "aaa" + |> Quack.WithCLSCompliant "bbb" 5 + Assert.IsType(obj1) + +[] +let ``Type preserved CLS Compliant and F# the same thing 1`` () = + let obj1 = + Quack () + |> Quack.WithCLSCompliant "aaa" 5 + |> Quack.WithCLSCompliant "bbb" 5 + + let obj2 = + Quack () + ++ ("aaa", 5) + ++ ("bbb", 5) + + Assert.Equal(obj1, obj2) + +[] +let ``Type preserved CLS Compliant and F# the same thing 2`` () = + let obj1 = + Quack () + |> Quack.WithCLSCompliant "aaa" 5 + |> Quack.WithCLSCompliant "bbb" 5 + |> Quack.WithoutCLSCompliant "aaa" + + let obj2 = + Quack () + ++ ("aaa", 5) + ++ ("bbb", 5) + -- "aaa" + + Assert.Equal(obj1, obj2) From 57e253e753c00c92849635f52e3e7dd744380af2 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 16:31:14 +0300 Subject: [PATCH 07/16] properties immutable --- src/DynamicObj/ImmutableDynamicObj.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 7b657f2..83b8d65 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -8,7 +8,7 @@ open System.Runtime.CompilerServices /// with immutability enabled only. type ImmutableDynamicObj (map : Map) = - let mutable properties = map + let properties = map // they're public, but because they're inline, // they won't be visible from other assemblies From 690f14b6af4d16655a34dfa477ff12c68612dcbb Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 16:38:51 +0300 Subject: [PATCH 08/16] Type preservation test --- tests/CSharpTests/InteropWorks.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/CSharpTests/InteropWorks.cs b/tests/CSharpTests/InteropWorks.cs index 5ba7ab2..8869d0d 100644 --- a/tests/CSharpTests/InteropWorks.cs +++ b/tests/CSharpTests/InteropWorks.cs @@ -1,6 +1,9 @@ using System; using Xunit; using DynamicObj; +using Microsoft.FSharp.Collections; +using System.Collections.Generic; +using System.Linq; namespace CSharpTests { @@ -21,5 +24,21 @@ public void Test1() ; Assert.Equal(obj1, obj2); } + + public class MyDynamicObject : ImmutableDynamicObj + { + public MyDynamicObject(FSharpMap map) : base(map) { } + public MyDynamicObject() : base(new(Enumerable.Empty>())) { } + } + + [Fact] + public void Test2() + { + var obj1 = + new MyDynamicObject() + .With("aaa", 5); + Assert.IsType(obj1); + Assert.Equal(5, obj1["aaa"]); + } } } From d596876c9c810956d110daae2dbc9d2da11786cf Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 16:48:12 +0300 Subject: [PATCH 09/16] wrong comment --- src/DynamicObj/ImmutableDynamicObj.fs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 83b8d65..c0a9e8c 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -10,8 +10,6 @@ type ImmutableDynamicObj (map : Map) = let properties = map - // they're public, but because they're inline, - // they won't be visible from other assemblies member this.Properties = properties static member private NewIfNeededCLSCompliant (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = From 76c0d3c4ced1e6be859560cc85643186acb5c379 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 17:01:52 +0300 Subject: [PATCH 10/16] Ugly named functions are now internal --- src/DynamicObj/ImmutableDynamicObj.fs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index c0a9e8c..0dd4756 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -4,6 +4,9 @@ open DynamicObj open System open System.Runtime.CompilerServices +[] +do() + /// Represents an DynamicObj's counterpart /// with immutability enabled only. type ImmutableDynamicObj (map : Map) = @@ -39,7 +42,7 @@ type ImmutableDynamicObj (map : Map) = /// 1. this property added if it wasn't present /// 2. this property updated otherwise /// Use With from F#. This one is only for non-F# code. - static member WithCLSCompliant name newValue (object : 'a when 'a :> ImmutableDynamicObj) = + static member internal WithCLSCompliant name newValue (object : 'a when 'a :> ImmutableDynamicObj) = object.Properties |> Map.add name newValue |> ImmutableDynamicObj.NewIfNeededCLSCompliant object @@ -56,7 +59,7 @@ type ImmutableDynamicObj (map : Map) = /// 1. the same if there was no requested property /// 2. without the requested property if there was /// Use Without from F#. This one is only for non-F# code. - static member WithoutCLSCompliant name (object : 'a when 'a :> ImmutableDynamicObj) = + static member internal WithoutCLSCompliant name (object : 'a when 'a :> ImmutableDynamicObj) = object.Properties |> Map.remove name |> ImmutableDynamicObj.NewIfNeededCLSCompliant object From b6e5d75af6a6a0aa3c818fe1c1df587c76486759 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 20:43:39 +0300 Subject: [PATCH 11/16] A reingeneered approach for type preservation Instead of relfection and SRTP, we constrain the inherited type to new() and after creation mutate its private field. No inline members needed, now all logic reside in assembly. --- src/DynamicObj/ImmutableDynamicObj.fs | 57 +++++++-------------- tests/UnitTests/ImmTests.fs | 73 ++++++++++----------------- 2 files changed, 45 insertions(+), 85 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 0dd4756..889baed 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -11,27 +11,24 @@ do() /// with immutability enabled only. type ImmutableDynamicObj (map : Map) = - let properties = map + let mutable properties = map - member this.Properties = properties + member private this.Properties = properties + member private this.MutateSetMap newMap = + properties <- newMap - static member private NewIfNeededCLSCompliant (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = + static member private NewIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = if obj.ReferenceEquals(map, object.Properties) then object else - Activator.CreateInstance(typeof<'a>, [| map :> obj |]) :?> 'a - - static member inline NewIfNeeded (object : ^T when ^T :> ImmutableDynamicObj) map : ^T = - if obj.ReferenceEquals(map, object.Properties) then - object - else - (^T: (new: Map -> ^T) map) - + let res = new 'a() + res.MutateSetMap map + res /// Empty instance new () = ImmutableDynamicObj Map.empty - + static member empty = ImmutableDynamicObj () /// Indexes ; if no key found, throws member this.Item @@ -41,16 +38,7 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - /// Use With from F#. This one is only for non-F# code. - static member internal WithCLSCompliant name newValue (object : 'a when 'a :> ImmutableDynamicObj) = - object.Properties - |> Map.add name newValue - |> ImmutableDynamicObj.NewIfNeededCLSCompliant object - - /// Returns an instance with: - /// 1. this property added if it wasn't present - /// 2. this property updated otherwise - static member inline With name newValue (object : ^T when ^T :> ImmutableDynamicObj) : ^T = + static member With name newValue (object : #ImmutableDynamicObj) = object.Properties |> Map.add name newValue |> ImmutableDynamicObj.NewIfNeeded object @@ -58,16 +46,7 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - /// Use Without from F#. This one is only for non-F# code. - static member internal WithoutCLSCompliant name (object : 'a when 'a :> ImmutableDynamicObj) = - object.Properties - |> Map.remove name - |> ImmutableDynamicObj.NewIfNeededCLSCompliant object - - /// Returns an instance: - /// 1. the same if there was no requested property - /// 2. without the requested property if there was - static member inline Without name (object : ^T when ^T :> ImmutableDynamicObj) = + static member Without name (object : #ImmutableDynamicObj) = object.Properties |> Map.remove name |> ImmutableDynamicObj.NewIfNeeded object @@ -75,15 +54,15 @@ type ImmutableDynamicObj (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member inline (++) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + static member (++) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member inline (--) (object, name) = ImmutableDynamicObj.Without name object + static member (--) (object, name) = ImmutableDynamicObj.Without name object member this.TryGetValue name = - match properties.TryGetValue name with + match this.Properties.TryGetValue name with | true, value -> Some value | _ -> ReflectionUtils.tryGetPropertyValue this name @@ -110,13 +89,13 @@ type ImmutableDynamicObjExtensions = /// 2. this property updated otherwise /// use this one only from C# [] - static member With (this : 'a when 'a :> ImmutableDynamicObj) (name, newValue) = - ImmutableDynamicObj.WithCLSCompliant name newValue this + static member With (this, name, newValue) = + ImmutableDynamicObj.With name newValue this /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was /// use this one only from C# [] - static member Without (this : 'a when 'a :> ImmutableDynamicObj, name) = - ImmutableDynamicObj.WithoutCLSCompliant name this \ No newline at end of file + static member Without (this, name) = + ImmutableDynamicObj.Without name this \ No newline at end of file diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index affb189..1986f29 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -3,6 +3,33 @@ open Xunit open DynamicObj +[] +let ``Value test 1`` () = + let obj1 = + ImmutableDynamicObj.empty + ++ ("aaa", 5) + Assert.Equal(obj1.["aaa"], 5); + +[] +let ``Value test 2`` () = + let obj1 = + ImmutableDynamicObj.empty + ++ ("aaa", 5) + ++ ("bbb", "quack") + Assert.Equal(obj1.["aaa"], 5); + Assert.Equal(obj1.["bbb"], "quack"); + +[] +let ``Value test 3`` () = + let obj1 = + ImmutableDynamicObj.empty + ++ ("aaa", 5) + -- "aaa" + match obj1.TryGetValue "aaa" with + | Some(value) -> Assert.False(true, "Should return None") + | _ -> () + + [] let ``No mutation test 1`` () = let obj1 = @@ -120,49 +147,3 @@ let ``Type preserved F# 2`` () = -- "aaa" ++ ("bbb", 5) Assert.IsType(obj1) - -[] -let ``Type preserved CLS Compliant 1`` () = - let obj1 = - Quack () - |> Quack.WithCLSCompliant "aaa" 5 - Assert.IsType(obj1) - -[] -let ``Type preserved CLS Compliant 2`` () = - let obj1 = - Quack () - |> Quack.WithCLSCompliant "aaa" 5 - |> Quack.WithoutCLSCompliant "aaa" - |> Quack.WithCLSCompliant "bbb" 5 - Assert.IsType(obj1) - -[] -let ``Type preserved CLS Compliant and F# the same thing 1`` () = - let obj1 = - Quack () - |> Quack.WithCLSCompliant "aaa" 5 - |> Quack.WithCLSCompliant "bbb" 5 - - let obj2 = - Quack () - ++ ("aaa", 5) - ++ ("bbb", 5) - - Assert.Equal(obj1, obj2) - -[] -let ``Type preserved CLS Compliant and F# the same thing 2`` () = - let obj1 = - Quack () - |> Quack.WithCLSCompliant "aaa" 5 - |> Quack.WithCLSCompliant "bbb" 5 - |> Quack.WithoutCLSCompliant "aaa" - - let obj2 = - Quack () - ++ ("aaa", 5) - ++ ("bbb", 5) - -- "aaa" - - Assert.Equal(obj1, obj2) From 4cef78760aca9e11f6b646f2480b674defbdb563 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Sun, 29 Aug 2021 21:03:31 +0300 Subject: [PATCH 12/16] Minor refactorings --- src/DynamicObj/ImmutableDynamicObj.fs | 12 +++++++----- tests/CSharpTests/InteropWorks.cs | 3 +-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 889baed..2df337b 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -9,20 +9,22 @@ do() /// Represents an DynamicObj's counterpart /// with immutability enabled only. -type ImmutableDynamicObj (map : Map) = +type ImmutableDynamicObj internal (map : Map) = let mutable properties = map - member private this.Properties = properties - member private this.MutateSetMap newMap = - properties <- newMap + member private this.Properties + with get () = + properties + and set value = + properties <- value static member private NewIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = if obj.ReferenceEquals(map, object.Properties) then object else let res = new 'a() - res.MutateSetMap map + res.Properties <- map res /// Empty instance diff --git a/tests/CSharpTests/InteropWorks.cs b/tests/CSharpTests/InteropWorks.cs index 8869d0d..907b8fe 100644 --- a/tests/CSharpTests/InteropWorks.cs +++ b/tests/CSharpTests/InteropWorks.cs @@ -27,8 +27,7 @@ public void Test1() public class MyDynamicObject : ImmutableDynamicObj { - public MyDynamicObject(FSharpMap map) : base(map) { } - public MyDynamicObject() : base(new(Enumerable.Empty>())) { } + } [Fact] From 459a227bf32e38a63942678bcdd15ad11ca36357 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Tue, 31 Aug 2021 09:46:58 +0300 Subject: [PATCH 13/16] Fields preservation --- src/DynamicObj/ImmutableDynamicObj.fs | 14 ++++++++++- tests/CSharpTests/InteropWorks.cs | 15 ++++++++++++ tests/UnitTests/ImmTests.fs | 34 +++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 2df337b..cb939a7 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -1,7 +1,7 @@ namespace DynamicObj open DynamicObj -open System +open System.Reflection open System.Runtime.CompilerServices [] @@ -19,11 +19,23 @@ type ImmutableDynamicObj internal (map : Map) = and set value = properties <- value + // Copies the fields of one object to the other one + // If their base is not ImmutableDynamicObj, then it + // will go over fields from the base instance + static member private copyMembers (ty : System.Type) sourceObject destinationObject = + for fi in ty.GetFields(BindingFlags.Instance ||| BindingFlags.NonPublic ||| BindingFlags.Public) do + let fieldValue = fi.GetValue sourceObject + fi.SetValue(destinationObject, fieldValue) + static member private NewIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = if obj.ReferenceEquals(map, object.Properties) then object else + // otherwise we create a new instance let res = new 'a() + + // and then copy all current fields the new instance + ImmutableDynamicObj.copyMembers (typeof<'a>) object res res.Properties <- map res diff --git a/tests/CSharpTests/InteropWorks.cs b/tests/CSharpTests/InteropWorks.cs index 907b8fe..57914d9 100644 --- a/tests/CSharpTests/InteropWorks.cs +++ b/tests/CSharpTests/InteropWorks.cs @@ -39,5 +39,20 @@ public void Test2() Assert.IsType(obj1); Assert.Equal(5, obj1["aaa"]); } + + public class MyDynamicObjectWithField : ImmutableDynamicObj + { + public int Aaa { get; init; } + } + + [Fact] + public void TestFieldsPreservedForInheritor() + { + var obj1 = + new MyDynamicObjectWithField() { Aaa = 100500 } + .With("aaa", 5) + .Without("aaa"); + Assert.Equal(100500, obj1.Aaa); + } } } diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index 1986f29..eea6d52 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -147,3 +147,37 @@ let ``Type preserved F# 2`` () = -- "aaa" ++ ("bbb", 5) Assert.IsType(obj1) + + +type QuackWithField (map, field) = + inherit ImmutableDynamicObj (map) + let field = field + member _.Field = field + + new() = QuackWithField (Map.empty, 5) + + +[] +let ``Fields of the closest inheritor preserved 1`` () = + let obj1 = + QuackWithField (Map.empty, 100) + ++ ("aaa", 5) + -- "bbb" + Assert.Equal(100, obj1.Field); + +type FarQuackWithField (someOtherField) = + inherit QuackWithField (Map.empty, 55) + let otherField = someOtherField + member _.OtherField = otherField + + new() = FarQuackWithField(3) + +[] +let ``Fields of a far inheritor preserved 1`` () = + let obj1 = + FarQuackWithField (1234) + ++ ("aaa", 5) + -- "bbb" + Assert.Equal(1234, obj1.OtherField); + Assert.Equal(55, obj1.Field); + Assert.Equal(5 :> obj, obj1.["aaa"]); \ No newline at end of file From c83cf62c4e5f6b7aef98d9281ee40709d15272a3 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Tue, 31 Aug 2021 09:50:01 +0300 Subject: [PATCH 14/16] API changed --- src/DynamicObj/ImmutableDynamicObj.fs | 22 +++++++++++----------- tests/CSharpTests/InteropWorks.cs | 14 +++++++------- tests/UnitTests/ImmTests.fs | 8 ++++---- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index cb939a7..989b24e 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -27,7 +27,7 @@ type ImmutableDynamicObj internal (map : Map) = let fieldValue = fi.GetValue sourceObject fi.SetValue(destinationObject, fieldValue) - static member private NewIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = + static member private newIfNeeded (object : 'a when 'a :> ImmutableDynamicObj) map : 'a = if obj.ReferenceEquals(map, object.Properties) then object else @@ -52,28 +52,28 @@ type ImmutableDynamicObj internal (map : Map) = /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member With name newValue (object : #ImmutableDynamicObj) = + static member add name newValue (object : #ImmutableDynamicObj) = object.Properties |> Map.add name newValue - |> ImmutableDynamicObj.NewIfNeeded object + |> ImmutableDynamicObj.newIfNeeded object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member Without name (object : #ImmutableDynamicObj) = + static member remove name (object : #ImmutableDynamicObj) = object.Properties |> Map.remove name - |> ImmutableDynamicObj.NewIfNeeded object + |> ImmutableDynamicObj.newIfNeeded object /// Returns an instance with: /// 1. this property added if it wasn't present /// 2. this property updated otherwise - static member (++) (object, (name, newValue)) = ImmutableDynamicObj.With name newValue object + static member (++) (object, (name, newValue)) = ImmutableDynamicObj.add name newValue object /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was - static member (--) (object, name) = ImmutableDynamicObj.Without name object + static member (--) (object, name) = ImmutableDynamicObj.remove name object member this.TryGetValue name = match this.Properties.TryGetValue name with @@ -103,13 +103,13 @@ type ImmutableDynamicObjExtensions = /// 2. this property updated otherwise /// use this one only from C# [] - static member With (this, name, newValue) = - ImmutableDynamicObj.With name newValue this + static member AddItem (this, name, newValue) = + ImmutableDynamicObj.add name newValue this /// Returns an instance: /// 1. the same if there was no requested property /// 2. without the requested property if there was /// use this one only from C# [] - static member Without (this, name) = - ImmutableDynamicObj.Without name this \ No newline at end of file + static member RemoveItem (this, name) = + ImmutableDynamicObj.remove name this \ No newline at end of file diff --git a/tests/CSharpTests/InteropWorks.cs b/tests/CSharpTests/InteropWorks.cs index 57914d9..d9c97ce 100644 --- a/tests/CSharpTests/InteropWorks.cs +++ b/tests/CSharpTests/InteropWorks.cs @@ -14,13 +14,13 @@ public void Test1() { var obj1 = new ImmutableDynamicObj() - .With("aa", 4) - .With("bb", 10) - .Without("aa") + .AddItem("aa", 4) + .AddItem("bb", 10) + .RemoveItem("aa") ; var obj2 = new ImmutableDynamicObj() - .With("bb", 10) + .AddItem("bb", 10) ; Assert.Equal(obj1, obj2); } @@ -35,7 +35,7 @@ public void Test2() { var obj1 = new MyDynamicObject() - .With("aaa", 5); + .AddItem("aaa", 5); Assert.IsType(obj1); Assert.Equal(5, obj1["aaa"]); } @@ -50,8 +50,8 @@ public void TestFieldsPreservedForInheritor() { var obj1 = new MyDynamicObjectWithField() { Aaa = 100500 } - .With("aaa", 5) - .Without("aaa"); + .AddItem("aaa", 5) + .RemoveItem("aaa"); Assert.Equal(100500, obj1.Aaa); } } diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index eea6d52..24a764a 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -34,18 +34,18 @@ let ``Value test 3`` () = let ``No mutation test 1`` () = let obj1 = ImmutableDynamicObj () - |> ImmutableDynamicObj.With "aa" 5 + |> ImmutableDynamicObj.add "aa" 5 let obj2 = ImmutableDynamicObj () - |> ImmutableDynamicObj.With "bb" 10 + |> ImmutableDynamicObj.add "bb" 10 let objBase = ImmutableDynamicObj () let objA = objBase - |> ImmutableDynamicObj.With "aa" 5 + |> ImmutableDynamicObj.add "aa" 5 let objB = objBase - |> ImmutableDynamicObj.With "bb" 10 + |> ImmutableDynamicObj.add "bb" 10 Assert.Equal(obj1, objA) Assert.Equal(obj2, objB) From f43e072d3e977c7eebe23f0da21dd45e19445101 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Thu, 2 Sep 2021 13:38:04 +0300 Subject: [PATCH 15/16] Operators moved to a separate module --- src/DynamicObj/DynamicObj.fsproj | 1 + src/DynamicObj/ImmutableDynamicObj.fs | 10 -------- src/DynamicObj/Operators.fs | 29 ++++++++++++++++++++++ tests/UnitTests/ImmTests.fs | 1 + tests/UnitTests/OperatorsTests.fs | 35 +++++++++++++++++++++++++++ tests/UnitTests/UnitTests.fsproj | 1 + 6 files changed, 67 insertions(+), 10 deletions(-) create mode 100644 src/DynamicObj/Operators.fs create mode 100644 tests/UnitTests/OperatorsTests.fs diff --git a/src/DynamicObj/DynamicObj.fsproj b/src/DynamicObj/DynamicObj.fsproj index 892fa30..46ddee0 100644 --- a/src/DynamicObj/DynamicObj.fsproj +++ b/src/DynamicObj/DynamicObj.fsproj @@ -26,6 +26,7 @@ + diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index 989b24e..c9e04ef 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -65,16 +65,6 @@ type ImmutableDynamicObj internal (map : Map) = |> Map.remove name |> ImmutableDynamicObj.newIfNeeded object - /// Returns an instance with: - /// 1. this property added if it wasn't present - /// 2. this property updated otherwise - static member (++) (object, (name, newValue)) = ImmutableDynamicObj.add name newValue object - - /// Returns an instance: - /// 1. the same if there was no requested property - /// 2. without the requested property if there was - static member (--) (object, name) = ImmutableDynamicObj.remove name object - member this.TryGetValue name = match this.Properties.TryGetValue name with | true, value -> Some value diff --git a/src/DynamicObj/Operators.fs b/src/DynamicObj/Operators.fs new file mode 100644 index 0000000..5798651 --- /dev/null +++ b/src/DynamicObj/Operators.fs @@ -0,0 +1,29 @@ +module DynamicObj.Operators + + + +/// Returns an instance with: +/// 1. this property added if it wasn't present +/// 2. this property updated otherwise +let (++) object (name, newValue) = ImmutableDynamicObj.add name newValue object + + +/// Returns an instance: +/// 1. the same if there was no requested property +/// 2. without the requested property if there was +let (--) object name = ImmutableDynamicObj.remove name object + + +/// Acts as (++) if the value is Some, +/// returns the same object otherwise +let (++?) object (name, newValue) = + match newValue with + | Some(value) -> object ++ (name, value) + | None -> object + +/// Acts as (++?) but maps the valid value +/// through the last argument +let (++??) object (name, newValue, f) = + match newValue with + | Some(value) -> object ++ (name, f value) + | None -> object \ No newline at end of file diff --git a/tests/UnitTests/ImmTests.fs b/tests/UnitTests/ImmTests.fs index 24a764a..312fbde 100644 --- a/tests/UnitTests/ImmTests.fs +++ b/tests/UnitTests/ImmTests.fs @@ -2,6 +2,7 @@ open Xunit open DynamicObj +open DynamicObj.Operators [] let ``Value test 1`` () = diff --git a/tests/UnitTests/OperatorsTests.fs b/tests/UnitTests/OperatorsTests.fs new file mode 100644 index 0000000..f80b4b7 --- /dev/null +++ b/tests/UnitTests/OperatorsTests.fs @@ -0,0 +1,35 @@ +module Tests.OperatorsTests + +open Xunit +open DynamicObj +open DynamicObj.Operators + +[] +let ``Test ++? 1`` () = + let obj1 = + ImmutableDynamicObj.empty + ++? ("aaa", Some(5)) + let expected = + ImmutableDynamicObj.empty + ++ ("aaa", 5) + Assert.Equal(expected, obj1) + +[] +let ``Test ++? 2`` () = + let obj1 = + ImmutableDynamicObj.empty + ++? ("aaa", None) + let expected = + ImmutableDynamicObj.empty + Assert.Equal(expected, obj1) + +[] +let ``Test ++?? 1`` () = + let obj1 = + ImmutableDynamicObj.empty + ++?? ("aaa", Some(5), fun c -> c * 10) + let expected = + ImmutableDynamicObj.empty + ++ ("aaa", 50) + Assert.Equal(expected, obj1) + diff --git a/tests/UnitTests/UnitTests.fsproj b/tests/UnitTests/UnitTests.fsproj index e337e90..36f7d20 100644 --- a/tests/UnitTests/UnitTests.fsproj +++ b/tests/UnitTests/UnitTests.fsproj @@ -10,6 +10,7 @@ + From 1187cb90138c0b754f25d0009e7647150adf1904 Mon Sep 17 00:00:00 2001 From: WhiteBlackGoose Date: Fri, 3 Sep 2021 18:24:20 +0300 Subject: [PATCH 16/16] Added addOpt and addOptBy --- src/DynamicObj/ImmutableDynamicObj.fs | 19 +++++++++++++++++++ src/DynamicObj/Operators.fs | 10 ++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/src/DynamicObj/ImmutableDynamicObj.fs b/src/DynamicObj/ImmutableDynamicObj.fs index c9e04ef..7562322 100644 --- a/src/DynamicObj/ImmutableDynamicObj.fs +++ b/src/DynamicObj/ImmutableDynamicObj.fs @@ -65,6 +65,25 @@ type ImmutableDynamicObj internal (map : Map) = |> Map.remove name |> ImmutableDynamicObj.newIfNeeded object + + + + + /// Acts as add if the value is Some, + /// returns the same object otherwise + static member addOpt name newValue object = + match newValue with + | Some(value) -> object |> ImmutableDynamicObj.add name value + | None -> object + + + /// Acts as addOpt but maps the valid value + /// through the last argument + static member addOptBy name newValue f object = + match newValue with + | Some(value) -> object |> ImmutableDynamicObj.add name (f value) + | None -> object + member this.TryGetValue name = match this.Properties.TryGetValue name with | true, value -> Some value diff --git a/src/DynamicObj/Operators.fs b/src/DynamicObj/Operators.fs index 5798651..9dcbb6b 100644 --- a/src/DynamicObj/Operators.fs +++ b/src/DynamicObj/Operators.fs @@ -17,13 +17,11 @@ let (--) object name = ImmutableDynamicObj.remove name object /// Acts as (++) if the value is Some, /// returns the same object otherwise let (++?) object (name, newValue) = - match newValue with - | Some(value) -> object ++ (name, value) - | None -> object + object + |> ImmutableDynamicObj.addOpt name newValue /// Acts as (++?) but maps the valid value /// through the last argument let (++??) object (name, newValue, f) = - match newValue with - | Some(value) -> object ++ (name, f value) - | None -> object \ No newline at end of file + object + |> ImmutableDynamicObj.addOptBy name newValue f \ No newline at end of file