diff --git a/.gitignore b/.gitignore index 7050558..332b6cf 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /bower_components/ /node_modules/ /output/ +package-lock.json diff --git a/LICENSE b/LICENSE index 567485e..311379c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,26 @@ -MIT License +Copyright 2018 PureScript -Copyright (c) 2017 PureScript +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +1. Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. +2. Redistributions in binary form must reproduce the above copyright notice, +this list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +3. Neither the name of the copyright holder nor the names of its contributors +may be used to endorse or promote products derived from this software without +specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/bower.json b/bower.json index 4626440..60b4465 100644 --- a/bower.json +++ b/bower.json @@ -2,7 +2,7 @@ "name": "purescript-record", "homepage": "https://github.com/purescript/purescript-record", "description": "Functions for working with records and polymorphic labels", - "license": "MIT", + "license": "BSD-3-Clause", "repository": { "type": "git", "url": "git://github.com/purescript/purescript-record.git" @@ -17,12 +17,13 @@ "package.json" ], "dependencies": { - "purescript-symbols": "^3.0.0", - "purescript-functions": "^3.0.0", - "purescript-typelevel-prelude": "^2.3.1", - "purescript-st": "^3.0.0" + "purescript-functions": "^4.0.0", + "purescript-prelude": "^4.0.0", + "purescript-st": "^4.0.0", + "purescript-typelevel-prelude": "^3.0.0", + "purescript-unsafe-coerce": "^4.0.0" }, "devDependencies": { - "purescript-assert": "^3.0.0" + "purescript-assert": "^4.0.0" } } diff --git a/package.json b/package.json index afdcbc5..afd0ec1 100644 --- a/package.json +++ b/package.json @@ -3,12 +3,12 @@ "scripts": { "clean": "rimraf output && rimraf .pulp-cache", "build": "eslint src && pulp build -- --censor-lib --strict", - "test": "pulp test" + "test": "pulp test --check-main-type Effect.Effect" }, "devDependencies": { - "eslint": "^3.17.1", - "purescript-psa": "^0.5.0", - "pulp": "^11.0.0", - "rimraf": "^2.6.1" + "eslint": "^4.19.1", + "purescript-psa": "^0.6.0", + "pulp": "^12.2.0", + "rimraf": "^2.6.2" } } diff --git a/src/Data/Record/ST.purs b/src/Data/Record/ST.purs deleted file mode 100644 index 671752f..0000000 --- a/src/Data/Record/ST.purs +++ /dev/null @@ -1,71 +0,0 @@ -module Data.Record.ST - ( STRecord - , freezeSTRecord - , thawSTRecord - , peekSTRecord - , pokeSTRecord - , runSTRecord - , pureSTRecord - ) where - -import Prelude - -import Control.Monad.Eff (Eff, runPure) -import Control.Monad.ST (ST) -import Data.Symbol (class IsSymbol, SProxy, reflectSymbol) - --- | A value of type `STRecord h r` represents a mutable record with fields `r`, --- | belonging to the state thread `h`. --- | --- | Create values of type `STRecord` using `thawSTRecord`. -foreign import data STRecord :: Type -> # Type -> Type - --- | Freeze a mutable record, creating a copy. -foreign import freezeSTRecord :: forall h r eff. STRecord h r -> Eff (st :: ST h | eff) (Record r) - --- | Thaw an immutable record, creating a copy. -foreign import thawSTRecord :: forall h r eff. Record r -> Eff (st :: ST h | eff) (STRecord h r) - --- | Run an ST computation safely, constructing a record. -foreign import runSTRecord :: forall r eff. (forall h. Eff (st :: ST h | eff) (STRecord h r)) -> Eff eff (Record r) - --- | Run an ST computation safely, constructing a record, assuming no other --- | types of effects. -pureSTRecord :: forall r. (forall h eff. Eff (st :: ST h | eff) (STRecord h r)) -> Record r -pureSTRecord st = runPure (runSTRecord st) - -foreign import unsafePeekSTRecord - :: forall a r h eff - . String - -> STRecord h r - -> Eff (st :: ST h | eff) a - --- | Read the current value of a field in a mutable record, by providing a --- | type-level representative for the label which should be read. -peekSTRecord - :: forall l h a r r1 eff - . RowCons l a r1 r - => IsSymbol l - => SProxy l - -> STRecord h r - -> Eff (st :: ST h | eff) a -peekSTRecord l = unsafePeekSTRecord (reflectSymbol l) - -foreign import unsafePokeSTRecord - :: forall a r h eff - . String - -> a - -> STRecord h r - -> Eff (st :: ST h | eff) Unit - --- | Modify a record in place, by providing a type-level representative for the label --- | which should be updated. -pokeSTRecord - :: forall l h a r r1 eff - . RowCons l a r1 r - => IsSymbol l - => SProxy l - -> a - -> STRecord h r - -> Eff (st :: ST h | eff) Unit -pokeSTRecord l = unsafePokeSTRecord (reflectSymbol l) diff --git a/src/Data/Record/Unsafe.js b/src/Data/Record/Unsafe.js deleted file mode 100644 index 5da054b..0000000 --- a/src/Data/Record/Unsafe.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; - -exports.unsafeGetFn = function(label, rec) { - return rec[label]; -}; - -exports.unsafeSetFn = function(label, value, rec) { - var copy = {}; - for (var key in rec) { - if ({}.hasOwnProperty.call(rec, key)) { - copy[key] = rec[key]; - } - } - copy[label] = value; - return copy; -}; - -exports.unsafeDeleteFn = function(label, rec) { - var copy = {}; - for (var key in rec) { - if (key !== label && {}.hasOwnProperty.call(rec, key)) { - copy[key] = rec[key]; - } - } - return copy; -}; - -exports.unsafeHasFn = function(label, rec) { - return {}.hasOwnProperty.call(rec, label); -}; diff --git a/src/Data/Record/Unsafe.purs b/src/Data/Record/Unsafe.purs deleted file mode 100644 index 23cf523..0000000 --- a/src/Data/Record/Unsafe.purs +++ /dev/null @@ -1,29 +0,0 @@ -module Data.Record.Unsafe - ( unsafeGetFn - , unsafeSetFn - , unsafeDeleteFn - , unsafeHasFn - , unsafeGet - , unsafeSet - , unsafeDelete - , unsafeHas - ) where - -import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3) - -foreign import unsafeGetFn :: forall r a. Fn2 String (Record r) a -foreign import unsafeSetFn :: forall r1 r2 a. Fn3 String a (Record r1) (Record r2) -foreign import unsafeDeleteFn :: forall r1 r2. Fn2 String (Record r1) (Record r2) -foreign import unsafeHasFn :: forall r1. Fn2 String (Record r1) Boolean - -unsafeGet :: forall r a. String -> Record r -> a -unsafeGet = runFn2 unsafeGetFn - -unsafeSet :: forall r1 r2 a. String -> a -> Record r1 -> Record r2 -unsafeSet = runFn3 unsafeSetFn - -unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2 -unsafeDelete = runFn2 unsafeDeleteFn - -unsafeHas :: forall r1. String -> Record r1 -> Boolean -unsafeHas = runFn2 unsafeHasFn diff --git a/src/Data/Record.purs b/src/Record.purs similarity index 55% rename from src/Data/Record.purs rename to src/Record.purs index 9048cbf..9982807 100644 --- a/src/Data/Record.purs +++ b/src/Record.purs @@ -1,4 +1,4 @@ -module Data.Record +module Record ( get , set , modify @@ -6,15 +6,21 @@ module Data.Record , delete , rename , equal + , merge + , union + , disjointUnion + , nub , class EqualFields , equalFields ) where -import Data.Function.Uncurried (runFn2, runFn3) -import Data.Record.Unsafe (unsafeGetFn, unsafeSetFn, unsafeDeleteFn) +import Data.Function.Uncurried (runFn2) +import Record.Unsafe (unsafeGet, unsafeSet, unsafeDelete) +import Record.Unsafe.Union (unsafeUnionFn) import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol) import Prelude (class Eq, (&&), (==)) -import Type.Row (class RowLacks, class RowToList, Cons, Nil, RLProxy(RLProxy), kind RowList) +import Type.Row (class Lacks, class Cons, class Nub, class RowToList, class Union, Cons, Nil, RLProxy(RLProxy), kind RowList) +import Unsafe.Coerce (unsafeCoerce) -- | Get a property for a label which is specified using a value-level proxy for -- | a type-level string. @@ -27,11 +33,11 @@ import Type.Row (class RowLacks, class RowToList, Cons, Nil, RLProxy(RLProxy), k get :: forall r r' l a . IsSymbol l - => RowCons l a r' r + => Cons l a r' r => SProxy l -> Record r -> a -get l r = runFn2 unsafeGetFn (reflectSymbol l) r +get l r = unsafeGet (reflectSymbol l) r -- | Set a property for a label which is specified using a value-level proxy for -- | a type-level string. @@ -45,13 +51,13 @@ get l r = runFn2 unsafeGetFn (reflectSymbol l) r set :: forall r1 r2 r l a b . IsSymbol l - => RowCons l a r r1 - => RowCons l b r r2 + => Cons l a r r1 + => Cons l b r r2 => SProxy l -> b -> Record r1 -> Record r2 -set l b r = runFn3 unsafeSetFn (reflectSymbol l) b r +set l b r = unsafeSet (reflectSymbol l) b r -- | Modify a property for a label which is specified using a value-level proxy for -- | a type-level string. @@ -65,8 +71,8 @@ set l b r = runFn3 unsafeSetFn (reflectSymbol l) b r modify :: forall r1 r2 r l a b . IsSymbol l - => RowCons l a r r1 - => RowCons l b r r2 + => Cons l a r r1 + => Cons l b r r2 => SProxy l -> (a -> b) -> Record r1 @@ -80,18 +86,18 @@ modify l f r = set l (f (get l r)) r -- | -- | ```purescript -- | insert (SProxy :: SProxy "x") --- | :: forall r a. RowLacks "x" r => a -> { | r } -> { x :: a | r } +-- | :: forall r a. Lacks "x" r => a -> { | r } -> { x :: a | r } -- | ``` insert :: forall r1 r2 l a . IsSymbol l - => RowLacks l r1 - => RowCons l a r1 r2 + => Lacks l r1 + => Cons l a r1 r2 => SProxy l -> a -> Record r1 -> Record r2 -insert l a r = runFn3 unsafeSetFn (reflectSymbol l) a r +insert l a r = unsafeSet (reflectSymbol l) a r -- | Delete a property for a label which is specified using a value-level proxy for -- | a type-level string. @@ -103,17 +109,17 @@ insert l a r = runFn3 unsafeSetFn (reflectSymbol l) a r -- | -- | ```purescript -- | delete (SProxy :: SProxy "x") --- | :: forall r a. RowLacks "x" r => { x :: a | r } -> { | r } +-- | :: forall r a. Lacks "x" r => { x :: a | r } -> { | r } -- | ``` delete :: forall r1 r2 l a . IsSymbol l - => RowLacks l r1 - => RowCons l a r1 r2 + => Lacks l r1 + => Cons l a r1 r2 => SProxy l -> Record r2 -> Record r1 -delete l r = runFn2 unsafeDeleteFn (reflectSymbol l) r +delete l r = unsafeDelete (reflectSymbol l) r -- | Rename a property for a label which is specified using a value-level proxy for -- | a type-level string. @@ -125,15 +131,15 @@ delete l r = runFn2 unsafeDeleteFn (reflectSymbol l) r -- | -- | ```purescript -- | rename (SProxy :: SProxy "x") (SProxy :: SProxy "y") --- | :: forall a r. RowLacks "x" r => RowLacks "y" r => { x :: a | r} -> { y :: a | r} +-- | :: forall a r. Lacks "x" r => Lacks "y" r => { x :: a | r} -> { y :: a | r} -- | ``` rename :: forall prev next ty input inter output . IsSymbol prev => IsSymbol next - => RowCons prev ty inter input - => RowLacks prev inter - => RowCons next ty inter output - => RowLacks next inter + => Cons prev ty inter input + => Lacks prev inter + => Cons next ty inter output + => Lacks next inter => SProxy prev -> SProxy next -> Record input @@ -141,6 +147,69 @@ rename :: forall prev next ty input inter output rename prev next record = insert next (get prev record) (delete prev record :: Record inter) +-- | Merges two records with the first record's labels taking precedence in the +-- | case of overlaps. +-- | +-- | For example: +-- | +-- | ```purescript +-- | merge { x: 1, y: "y" } { y: 2, z: true } +-- | :: { x :: Int, y :: String, z :: Boolean } +-- | ``` +merge + :: forall r1 r2 r3 r4 + . Union r1 r2 r3 + => Nub r3 r4 + => Record r1 + -> Record r2 + -> Record r4 +merge l r = runFn2 unsafeUnionFn l r + +-- | Merges two records with the first record's labels taking precedence in the +-- | case of overlaps. Unlike `merge`, this does not remove duplicate labels +-- | from the resulting record type. This can result in better inference for +-- | some pipelines, deferring the need for a `Nub` constraint. +-- | +-- | For example: +-- | +-- | ```purescript +-- | union { x: 1, y: "y" } { y: 2, z: true } +-- | :: { x :: Int, y :: String, y :: Int, z :: Boolean } +-- | ``` +union + :: forall r1 r2 r3 + . Union r1 r2 r3 + => Record r1 + -> Record r2 + -> Record r3 +union l r = runFn2 unsafeUnionFn l r + +-- | Merges two records where no labels overlap. This restriction exhibits +-- | better inference than `merge` when the resulting record type is known, +-- | but one argument is not. +-- | +-- | For example, hole `?help` is inferred to have type `{ b :: Int }` here: +-- | +-- | ```purescript +-- | disjoinUnion { a: 5 } ?help :: { a :: Int, b :: Int } +-- | ``` +disjointUnion + :: forall r1 r2 r3 + . Union r1 r2 r3 + => Nub r3 r3 + => Record r1 + -> Record r2 + -> Record r3 +disjointUnion l r = runFn2 unsafeUnionFn l r + +-- | A coercion which removes duplicate labels from a record's type. +nub + :: forall r1 r2 + . Nub r1 r2 + => Record r1 + -> Record r2 +nub = unsafeCoerce + -- | Check two records of the same type for equality. equal :: forall r rs @@ -158,7 +227,7 @@ instance equalFieldsCons :: ( IsSymbol name , Eq ty - , RowCons name ty tailRow row + , Cons name ty tailRow row , EqualFields tail row ) => EqualFields (Cons name ty tail) row where equalFields _ a b = get' a == get' b && equalRest a b diff --git a/src/Data/Record/Builder.js b/src/Record/Builder.js similarity index 69% rename from src/Data/Record/Builder.js rename to src/Record/Builder.js index 4b4a097..89df61c 100644 --- a/src/Data/Record/Builder.js +++ b/src/Record/Builder.js @@ -44,20 +44,3 @@ exports.unsafeRename = function(l1) { }; }; }; - -exports.unsafeMerge = function(r1) { - return function(r2) { - var copy = {}; - for (var k1 in r2) { - if ({}.hasOwnProperty.call(r2, k1)) { - copy[k1] = r2[k1]; - } - } - for (var k2 in r1) { - if ({}.hasOwnProperty.call(r1, k2)) { - copy[k2] = r1[k2]; - } - } - return copy; - }; -}; diff --git a/src/Data/Record/Builder.purs b/src/Record/Builder.purs similarity index 63% rename from src/Data/Record/Builder.purs rename to src/Record/Builder.purs index e7e0a85..9a9623b 100644 --- a/src/Data/Record/Builder.purs +++ b/src/Record/Builder.purs @@ -1,4 +1,4 @@ -module Data.Record.Builder +module Record.Builder ( Builder , build , insert @@ -6,19 +6,24 @@ module Data.Record.Builder , delete , rename , merge + , union + , disjointUnion + , nub ) where import Prelude +import Data.Function.Uncurried (runFn2) import Data.Symbol (class IsSymbol, SProxy, reflectSymbol) -import Type.Row (class RowLacks) +import Record.Unsafe.Union (unsafeUnionFn) +import Type.Row as Row +import Unsafe.Coerce (unsafeCoerce) foreign import copyRecord :: forall r1. Record r1 -> Record r1 foreign import unsafeInsert :: forall a r1 r2. String -> a -> Record r1 -> Record r2 foreign import unsafeModify :: forall a b r1 r2. String -> (a -> b) -> Record r1 -> Record r2 foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2 foreign import unsafeRename :: forall r1 r2. String -> String -> Record r1 -> Record r2 -foreign import unsafeMerge :: forall r1 r2 r3. Record r1 -> Record r2 -> Record r3 -- | A `Builder` can be used to `build` a record by incrementally adding -- | fields in-place, instead of using `insert` and repeatedly generating new @@ -43,8 +48,8 @@ derive newtype instance categoryBuilder :: Category Builder -- | Build by inserting a new field. insert :: forall l a r1 r2 - . RowCons l a r1 r2 - => RowLacks l r1 + . Row.Cons l a r1 r2 + => Row.Lacks l r1 => IsSymbol l => SProxy l -> a @@ -54,8 +59,8 @@ insert l a = Builder \r1 -> unsafeInsert (reflectSymbol l) a r1 -- | Build by modifying an existing field. modify :: forall l a b r r1 r2 - . RowCons l a r r1 - => RowCons l b r r2 + . Row.Cons l a r r1 + => Row.Cons l b r r2 => IsSymbol l => SProxy l -> (a -> b) @@ -66,8 +71,8 @@ modify l f = Builder \r1 -> unsafeModify (reflectSymbol l) f r1 delete :: forall l a r1 r2 . IsSymbol l - => RowLacks l r1 - => RowCons l a r1 r2 + => Row.Lacks l r1 + => Row.Cons l a r1 r2 => SProxy l -> Builder (Record r2) (Record r1) delete l = Builder \r2 -> unsafeDelete (reflectSymbol l) r2 @@ -76,10 +81,10 @@ delete l = Builder \r2 -> unsafeDelete (reflectSymbol l) r2 rename :: forall l1 l2 a r1 r2 r3 . IsSymbol l1 => IsSymbol l2 - => RowCons l1 a r2 r1 - => RowLacks l1 r2 - => RowCons l2 a r2 r3 - => RowLacks l2 r2 + => Row.Cons l1 a r2 r1 + => Row.Lacks l1 r2 + => Row.Cons l2 a r2 r3 + => Row.Lacks l2 r2 => SProxy l1 -> SProxy l2 -> Builder (Record r1) (Record r3) @@ -87,8 +92,36 @@ rename l1 l2 = Builder \r1 -> unsafeRename (reflectSymbol l1) (reflectSymbol l2) -- | Build by merging existing fields from another record. merge + :: forall r1 r2 r3 r4 + . Row.Union r1 r2 r3 + => Row.Nub r3 r4 + => Record r2 + -> Builder (Record r1) (Record r4) +merge r2 = Builder \r1 -> runFn2 unsafeUnionFn r1 r2 + +-- | Build by merging existing fields from another record. Unlike `merge`, +-- | this does not remove duplicate labels from the resulting record type. +-- | This can result in better inference for some pipelines, deferring the +-- | need for a `Nub` constraint. +union :: forall r1 r2 r3 - . Union r1 r2 r3 + . Row.Union r1 r2 r3 => Record r2 -> Builder (Record r1) (Record r3) -merge r2 = Builder \r1 -> unsafeMerge r1 r2 +union r2 = Builder \r1 -> runFn2 unsafeUnionFn r1 r2 + +-- | Build by merging some disjoint set of fields from another record. +disjointUnion + :: forall r1 r2 r3 + . Row.Union r1 r2 r3 + => Row.Nub r3 r3 + => Record r1 + -> Builder (Record r2) (Record r3) +disjointUnion r1 = Builder \r2 -> runFn2 unsafeUnionFn r1 r2 + +-- | A coercion which removes duplicate labels from a record's type. +nub + :: forall r1 r2 + . Row.Nub r1 r2 + => Builder (Record r1) (Record r2) +nub = Builder unsafeCoerce diff --git a/src/Data/Record/ST.js b/src/Record/ST.js similarity index 63% rename from src/Data/Record/ST.js rename to src/Record/ST.js index 12b97c4..70c20b0 100644 --- a/src/Data/Record/ST.js +++ b/src/Record/ST.js @@ -10,25 +10,19 @@ function copyRecord(rec) { return copy; } -exports.runSTRecord = function(rec) { - return function() { - return copyRecord(rec()); - }; -}; - -exports.freezeSTRecord = function(rec) { +exports.freeze = function(rec) { return function() { return copyRecord(rec); }; }; -exports.thawSTRecord = function(rec) { +exports.thaw = function(rec) { return function() { return copyRecord(rec); }; }; -exports.unsafePeekSTRecord = function(l) { +exports.unsafePeek = function(l) { return function(rec) { return function() { return rec[l]; @@ -36,7 +30,7 @@ exports.unsafePeekSTRecord = function(l) { }; }; -exports.unsafePokeSTRecord = function(l) { +exports.unsafePoke = function(l) { return function(a) { return function(rec) { return function() { @@ -45,3 +39,13 @@ exports.unsafePokeSTRecord = function(l) { }; }; }; + +exports.unsafeModify = function(l) { + return function(f) { + return function(rec) { + return function() { + rec[l] = f(rec[l]); + }; + }; + }; +}; diff --git a/src/Record/ST.purs b/src/Record/ST.purs new file mode 100644 index 0000000..56fe1f8 --- /dev/null +++ b/src/Record/ST.purs @@ -0,0 +1,82 @@ +module Record.ST + ( STRecord + , freeze + , thaw + , peek + , poke + , modify + ) where + +import Prelude + +import Control.Monad.ST (ST, kind Region) +import Data.Symbol (class IsSymbol, SProxy, reflectSymbol) +import Prim.Row as Row + +-- | A value of type `STRecord h r` represents a mutable record with fields `r`, +-- | belonging to the state thread `h`. +-- | +-- | Create values of type `STRecord` using `thaw`. +foreign import data STRecord :: Region -> # Type -> Type + +-- | Freeze a mutable record, creating a copy. +foreign import freeze :: forall h r. STRecord h r -> ST h (Record r) + +-- | Thaw an immutable record, creating a copy. +foreign import thaw :: forall h r. Record r -> ST h (STRecord h r) + +foreign import unsafePeek + :: forall a r h + . String + -> STRecord h r + -> ST h a + +-- | Read the current value of a field in a mutable record, by providing a +-- | type-level representative for the label which should be read. +peek + :: forall l h a r r1 + . Row.Cons l a r1 r + => IsSymbol l + => SProxy l + -> STRecord h r + -> ST h a +peek l = unsafePeek (reflectSymbol l) + +foreign import unsafePoke + :: forall a r h + . String + -> a + -> STRecord h r + -> ST h Unit + +-- | Modify a record in place, by providing a type-level representative for the label +-- | which should be updated. +poke + :: forall l h a r r1 + . Row.Cons l a r1 r + => IsSymbol l + => SProxy l + -> a + -> STRecord h r + -> ST h Unit +poke l = unsafePoke (reflectSymbol l) + +foreign import unsafeModify + :: forall a r h + . String + -> (a -> a) + -> STRecord h r + -> ST h Unit + +-- | Modify a record in place, +-- | by providing a type-level representative for the label to update +-- | and a function to update it. +modify + :: forall l h a r r1 + . Row.Cons l a r1 r + => IsSymbol l + => SProxy l + -> (a -> a) + -> STRecord h r + -> ST h Unit +modify l = unsafeModify (reflectSymbol l) diff --git a/src/Record/Unsafe/Union.js b/src/Record/Unsafe/Union.js new file mode 100644 index 0000000..637d503 --- /dev/null +++ b/src/Record/Unsafe/Union.js @@ -0,0 +1,16 @@ +"use strict"; + +exports.unsafeUnionFn = function(r1, r2) { + var copy = {}; + for (var k1 in r2) { + if ({}.hasOwnProperty.call(r2, k1)) { + copy[k1] = r2[k1]; + } + } + for (var k2 in r1) { + if ({}.hasOwnProperty.call(r1, k2)) { + copy[k2] = r1[k2]; + } + } + return copy; +}; diff --git a/src/Record/Unsafe/Union.purs b/src/Record/Unsafe/Union.purs new file mode 100644 index 0000000..79d1b48 --- /dev/null +++ b/src/Record/Unsafe/Union.purs @@ -0,0 +1,8 @@ +module Record.Unsafe.Union where + +import Data.Function.Uncurried (Fn2, runFn2) + +foreign import unsafeUnionFn :: forall r1 r2 r3. Fn2 (Record r1) (Record r2) (Record r3) + +unsafeUnion :: forall r1 r2 r3. Record r1 -> Record r2 -> Record r3 +unsafeUnion = runFn2 unsafeUnionFn diff --git a/test/Main.purs b/test/Main.purs index 33b7174..249a4cb 100644 --- a/test/Main.purs +++ b/test/Main.purs @@ -2,15 +2,16 @@ module Test.Main where import Prelude -import Control.Monad.Eff (Eff) -import Data.Record (delete, equal, get, insert, modify, rename, set) -import Data.Record.Builder as Builder -import Data.Record.ST (pokeSTRecord, pureSTRecord, thawSTRecord) -import Data.Record.Unsafe (unsafeHas) +import Effect (Effect) +import Record (delete, equal, get, insert, merge, modify, rename, set) +import Record.Builder as Builder +import Control.Monad.ST (run) as ST +import Record.ST (poke, thaw, freeze, modify) as ST +import Record.Unsafe (unsafeHas) import Data.Symbol (SProxy(..)) -import Test.Assert (ASSERT, assert') +import Test.Assert (assert') -main :: Eff (assert :: ASSERT) Unit +main :: Effect Unit main = do let x = SProxy :: SProxy "x" y = SProxy :: SProxy "y" @@ -32,19 +33,27 @@ main = do equal { a: 1, b: "b", c: true } { a: 1, b: "b", c: true } assert' "equal2" $ not $ equal { a: 1, b: "b", c: true } { a: 1, b: "b", c: false } + assert' "merge" $ + equal { x: 1, y: "y" } (merge { y: "y" } { x: 1, y: 2 }) assert' "unsafeHas1" $ unsafeHas "a" { a: 42 } assert' "unsafeHas2" $ not $ unsafeHas "b" { a: 42 } - let stTest1 = pureSTRecord do - rec <- thawSTRecord { x: 41, y: "" } - pokeSTRecord x 42 rec - pokeSTRecord y "testing" rec - pure rec + let + stTest1 = ST.run do + rec <- ST.thaw { x: 41, y: "" } + ST.poke x 42 rec + ST.poke y "testing" rec + ST.freeze rec + stTest2 = ST.run do + rec <- ST.thaw { x: 41 } + ST.modify x (_ + 1) rec + ST.freeze rec assert' "pokeSTRecord" $ stTest1.x == 42 && stTest1.y == "testing" + assert' "ST.modify" $ stTest2.x == 42 let testBuilder = Builder.build (Builder.insert x 42 >>> Builder.merge { y: true, z: "testing" } @@ -52,5 +61,5 @@ main = do >>> Builder.modify x show >>> Builder.rename z y) {} - assert' "Data.Record.Builder" $ + assert' "Record.Builder" $ testBuilder.x == "42" && testBuilder.y == "testing"