├── .eslintrc.json ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── package.json ├── src ├── Record.purs └── Record │ ├── Builder.js │ ├── Builder.purs │ └── Unsafe │ ├── Union.js │ └── Union.purs └── test ├── Examples.purs └── Main.purs /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "sourceType": "module" 5 | }, 6 | "extends": "eslint:recommended", 7 | "rules": { 8 | "strict": [2, "global"], 9 | "block-scoped-var": 2, 10 | "consistent-return": 2, 11 | "eqeqeq": [2, "smart"], 12 | "guard-for-in": 2, 13 | "no-caller": 2, 14 | "no-extend-native": 2, 15 | "no-loop-func": 2, 16 | "no-new": 2, 17 | "no-param-reassign": 2, 18 | "no-return-assign": 2, 19 | "no-unused-expressions": 2, 20 | "no-use-before-define": 2, 21 | "radix": [2, "always"], 22 | "indent": [2, 2, { "SwitchCase": 1 }], 23 | "quotes": [2, "double"], 24 | "semi": [2, "always"] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Description of the change** 2 | 3 | Clearly and concisely describe the purpose of the pull request. If this PR relates to an existing issue or change proposal, please link to it. Include any other background context that would help reviewers understand the motivation for this PR. 4 | 5 | --- 6 | 7 | **Checklist:** 8 | 9 | - [ ] Added the change to the changelog's "Unreleased" section with a reference to this PR (e.g. "- Made a change (#0000)") 10 | - [ ] Linked any existing issues or proposals that this pull request should close 11 | - [ ] Updated or added relevant documentation 12 | - [ ] Added a test for the contribution (if applicable) 13 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: purescript-contrib/setup-purescript@main 16 | with: 17 | purescript: "unstable" 18 | 19 | - uses: actions/setup-node@v2 20 | with: 21 | node-version: "14.x" 22 | 23 | - name: Install dependencies 24 | run: | 25 | npm install -g bower 26 | npm install 27 | bower install --production 28 | 29 | - name: Build source 30 | run: npm run-script build 31 | 32 | - name: Run tests 33 | run: | 34 | bower install 35 | npm run-script test --if-present 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.* 2 | !/.gitignore 3 | !/.eslintrc.json 4 | !/.github/ 5 | /bower_components/ 6 | /node_modules/ 7 | /output/ 8 | package-lock.json 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Notable changes to this project are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 4 | 5 | ## [Unreleased] 6 | 7 | Breaking changes: 8 | 9 | New features: 10 | 11 | Bugfixes: 12 | 13 | Other improvements: 14 | 15 | ## [v4.0.0](https://github.com/purescript/purescript-record/releases/tag/v4.0.0) - 2022-04-27 16 | 17 | Breaking changes: 18 | - Migrate FFI to ES modules (#81 by @kl0tl and @JordanMartinez) 19 | - Replaced polymorphic proxies with monomorphic `Proxy` (#81 by @JordanMartinez) 20 | 21 | New features: 22 | 23 | Bugfixes: 24 | 25 | Other improvements: 26 | 27 | ## [v3.0.0](https://github.com/purescript/purescript-record/releases/tag/v3.0.0) - 2021-02-26 28 | 29 | Breaking changes: 30 | - Added support for PureScript 0.14 and dropped support for all previous versions (#66) 31 | - Updated `Record.Builder.merge` and `Record.Builder.union` so that they behave like `Record.merge` and `Record.union`: fields from the argument override those of the record being built in case of overlaps. (#73) 32 | - Removed `Record.ST` (#78) 33 | 34 | New features: 35 | - Added `buildFromScratch` for building from an empty record (#53) 36 | - Added `flip` function (#73) 37 | 38 | Bugfixes: 39 | 40 | Other improvements: 41 | - Replaced monomorphic proxies with `Type.Proxy.Proxy` and polymorphic variants (#67) 42 | - Removed `SProxy` from documentation (#70) 43 | - Migrated CI to GitHub Actions and updated installation instructions to use Spago (#69) 44 | - Added a changelog and pull request template (#74, #75) 45 | 46 | ## [v2.0.2](https://github.com/purescript/purescript-record/releases/tag/v2.0.2) - 2020-03-14 47 | 48 | - Fixed typo in docs (@i-am-the-slime) 49 | - Fixed travis build 50 | 51 | ## [v2.0.1](https://github.com/purescript/purescript-record/releases/tag/v2.0.1) - 2019-05-27 52 | 53 | - Dropped typelevel-prelude dependency (@hdgarrood) 54 | 55 | ## [v2.0.0](https://github.com/purescript/purescript-record/releases/tag/v2.0.0) - 2019-03-02 56 | 57 | - Bumped dependencies (in particular, now using v4.x of typelevel-prelude) (@justinwoo) 58 | - Added some examples to the README (@justinwoo) 59 | - Added comments explaining Builder (@chexxor) 60 | 61 | ## [v1.0.0](https://github.com/purescript/purescript-record/releases/tag/v1.0.0) - 2018-05-23 62 | 63 | - Updated for PureScript 0.12 64 | - The namespace has been changed from `Data.Record` to just `Record` 65 | - Added `modify` to the `ST` module (@matthewleon) 66 | - Added new functions for merging records (@natefaubion) 67 | - The `STRecord` prefixes have been dropped from the record functions for less repetition when using qualified imports 68 | - The function argument order has been changed so that `STRecord` is always the last argument 69 | 70 | ## [v0.2.6](https://github.com/purescript/purescript-record/releases/tag/v0.2.6) - 2018-01-28 71 | 72 | - Added `Builder.modify` (@justinwoo) 73 | 74 | ## [v0.2.5](https://github.com/purescript/purescript-record/releases/tag/v0.2.5) - 2017-11-15 75 | 76 | - Added `rename` and `Builder.rename` (@justinwoo) 77 | 78 | ## [v0.2.4](https://github.com/purescript/purescript-record/releases/tag/v0.2.4) - 2017-10-24 79 | 80 | - Added `Data.Record.ST` module (@paf31) 81 | 82 | ## [v0.2.3](https://github.com/purescript/purescript-record/releases/tag/v0.2.3) - 2017-09-26 83 | 84 | - Added `unsafeHas` (@natefaubion) 85 | 86 | ## [v0.2.2](https://github.com/purescript/purescript-record/releases/tag/v0.2.2) - 2017-09-10 87 | 88 | - Added `equal` function (@justinwoo) 89 | 90 | ## [v0.2.1](https://github.com/purescript/purescript-record/releases/tag/v0.2.1) - 2017-09-10 91 | 92 | - Added `ST` and `Builder` modules for modifying and building records in-place. 93 | 94 | ## [v0.2.0](https://github.com/purescript/purescript-record/releases/tag/v0.2.0) - 2017-07-24 95 | 96 | - Added unsafe versions of functions (@natefaubion) 97 | 98 | ## [v0.1.0](https://github.com/purescript/purescript-record/releases/tag/v0.1.0) - 2017-07-19 99 | 100 | - Initial versioned release 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 PureScript 2 | 3 | Redistribution and use in source and binary forms, with or without modification, 4 | are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this 7 | list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation and/or 11 | other materials provided with the distribution. 12 | 13 | 3. Neither the name of the copyright holder nor the names of its contributors 14 | may be used to endorse or promote products derived from this software without 15 | specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 18 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 19 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 20 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 21 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 22 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 23 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 24 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 25 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 26 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # purescript-record 2 | 3 | [![Latest release](http://img.shields.io/github/release/purescript/purescript-record.svg)](https://github.com/purescript/purescript-record/releases) 4 | [![Build status](https://github.com/purescript/purescript-record/workflows/CI/badge.svg?branch=master)](https://github.com/purescript/purescript-record/actions?query=workflow%3ACI+branch%3Amaster) 5 | [![Pursuit](https://pursuit.purescript.org/packages/purescript-record/badge)](https://pursuit.purescript.org/packages/purescript-record) 6 | 7 | Functions for working with records and polymorphic labels 8 | 9 | ## Installation 10 | 11 | ``` 12 | spago install record 13 | ``` 14 | 15 | ## Examples 16 | 17 | Given some Symbol ("type level String") Proxy and a constrained or concrete record type, you can use this library to generically modify records. 18 | 19 | ```purs 20 | x_ = Proxy :: Proxy "x" 21 | 22 | -- we can get a value out of a field 23 | gotX :: Int 24 | gotX = Record.get x_ { x: 1 } 25 | 26 | -- we can insert a value into a record that does not have a field at that label yet 27 | insertedX :: { x :: Int } 28 | insertedX = Record.insert x_ 1 {} 29 | 30 | -- we can delete a field from a record at a specific label 31 | deletedX :: {} 32 | deletedX = Record.delete x_ { x: 1 } 33 | 34 | -- we can set a new value for a field 35 | setX1 :: { x :: Int } 36 | setX1 = Record.set x_ 1 { x: 0 } 37 | 38 | -- we can also modify the type of the field by replacing the contents 39 | setX2 :: { x :: Unit } 40 | setX2 = Record.set x_ unit { x: 0 } 41 | 42 | -- we can modify the field value with a function 43 | modifyX :: { x :: Int } 44 | modifyX = Record.modify x_ (\value -> value + 1) { x: 0 } 45 | 46 | -- we can also merge two records 47 | mergedXY :: { x :: Int , y :: Int } 48 | mergedXY = Record.merge { x: 1 } { y: 1 } 49 | ``` 50 | 51 | See the [tests](./test/Main.purs) for more examples. 52 | 53 | If you need to combine multiple operations and avoid intermediate values, you might consider using [Record.Builder](https://pursuit.purescript.org/packages/purescript-record/docs/Record.Builder). 54 | 55 | You can also find an explanation and example of how to use this library [in this tutorial](https://purescript-simple-json.readthedocs.io/en/latest/inferred-record-types.html) of the Simple-JSON docs. 56 | 57 | ## Documentation 58 | 59 | Module documentation is [published on Pursuit](http://pursuit.purescript.org/packages/purescript-record). 60 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "purescript-record", 3 | "homepage": "https://github.com/purescript/purescript-record", 4 | "description": "Functions for working with records and polymorphic labels", 5 | "license": "BSD-3-Clause", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/purescript/purescript-record.git" 9 | }, 10 | "ignore": [ 11 | "**/.*", 12 | "bower_components", 13 | "node_modules", 14 | "output", 15 | "test", 16 | "bower.json", 17 | "package.json" 18 | ], 19 | "dependencies": { 20 | "purescript-functions": "^6.0.0", 21 | "purescript-prelude": "^6.0.0", 22 | "purescript-unsafe-coerce": "^6.0.0" 23 | }, 24 | "devDependencies": { 25 | "purescript-assert": "^6.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "clean": "rimraf output && rimraf .pulp-cache", 5 | "build": "eslint src && pulp build -- --censor-lib --strict", 6 | "test": "pulp test" 7 | }, 8 | "devDependencies": { 9 | "eslint": "^7.15.0", 10 | "purescript-psa": "^0.8.2", 11 | "pulp": "16.0.0-0", 12 | "rimraf": "^3.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Record.purs: -------------------------------------------------------------------------------- 1 | module Record 2 | ( get 3 | , set 4 | , modify 5 | , insert 6 | , delete 7 | , rename 8 | , equal 9 | , merge 10 | , union 11 | , disjointUnion 12 | , nub 13 | , class EqualFields 14 | , equalFields 15 | ) where 16 | 17 | import Prelude 18 | 19 | import Data.Function.Uncurried (runFn2) 20 | import Data.Symbol (class IsSymbol, reflectSymbol) 21 | import Prim.Row (class Lacks, class Cons, class Nub, class Union) 22 | import Prim.RowList (class RowToList, RowList, Cons, Nil) 23 | import Record.Unsafe (unsafeGet, unsafeSet, unsafeDelete) 24 | import Record.Unsafe.Union (unsafeUnionFn) 25 | import Type.Proxy (Proxy(..)) 26 | import Unsafe.Coerce (unsafeCoerce) 27 | 28 | -- | Get a property for a label which is specified using a value-level proxy for 29 | -- | a type-level string. 30 | -- | 31 | -- | For example: 32 | -- | 33 | -- | ```purescript 34 | -- | get (Proxy :: Proxy "x") :: forall r a. { x :: a | r } -> a 35 | -- | ``` 36 | get 37 | :: forall r r' l a 38 | . IsSymbol l 39 | => Cons l a r' r 40 | => Proxy l 41 | -> Record r 42 | -> a 43 | get l r = unsafeGet (reflectSymbol l) r 44 | 45 | -- | Set a property for a label which is specified using a value-level proxy for 46 | -- | a type-level string. 47 | -- | 48 | -- | For example: 49 | -- | 50 | -- | ```purescript 51 | -- | set (Proxy :: Proxy "x") 52 | -- | :: forall r a b. a -> { x :: b | r } -> { x :: a | r } 53 | -- | ``` 54 | set 55 | :: forall r1 r2 r l a b 56 | . IsSymbol l 57 | => Cons l a r r1 58 | => Cons l b r r2 59 | => Proxy l 60 | -> b 61 | -> Record r1 62 | -> Record r2 63 | set l b r = unsafeSet (reflectSymbol l) b r 64 | 65 | -- | Modify a property for a label which is specified using a value-level proxy for 66 | -- | a type-level string. 67 | -- | 68 | -- | For example: 69 | -- | 70 | -- | ```purescript 71 | -- | modify (Proxy :: Proxy "x") 72 | -- | :: forall r a b. (a -> b) -> { x :: a | r } -> { x :: b | r } 73 | -- | ``` 74 | modify 75 | :: forall r1 r2 r l a b 76 | . IsSymbol l 77 | => Cons l a r r1 78 | => Cons l b r r2 79 | => Proxy l 80 | -> (a -> b) 81 | -> Record r1 82 | -> Record r2 83 | modify l f r = set l (f (get l r)) r 84 | 85 | -- | Insert a new property for a label which is specified using a value-level proxy for 86 | -- | a type-level string. 87 | -- | 88 | -- | For example: 89 | -- | 90 | -- | ```purescript 91 | -- | insert (Proxy :: Proxy "x") 92 | -- | :: forall r a. Lacks "x" r => a -> { | r } -> { x :: a | r } 93 | -- | ``` 94 | insert 95 | :: forall r1 r2 l a 96 | . IsSymbol l 97 | => Lacks l r1 98 | => Cons l a r1 r2 99 | => Proxy l 100 | -> a 101 | -> Record r1 102 | -> Record r2 103 | insert l a r = unsafeSet (reflectSymbol l) a r 104 | 105 | -- | Delete a property for a label which is specified using a value-level proxy for 106 | -- | a type-level string. 107 | -- | 108 | -- | Note that the type of the resulting row must _lack_ the specified property. 109 | -- | Since duplicate labels are allowed, this is checked with a type class constraint. 110 | -- | 111 | -- | For example: 112 | -- | 113 | -- | ```purescript 114 | -- | delete (Proxy :: Proxy "x") 115 | -- | :: forall r a. Lacks "x" r => { x :: a | r } -> { | r } 116 | -- | ``` 117 | delete 118 | :: forall r1 r2 l a 119 | . IsSymbol l 120 | => Lacks l r1 121 | => Cons l a r1 r2 122 | => Proxy l 123 | -> Record r2 124 | -> Record r1 125 | delete l r = unsafeDelete (reflectSymbol l) r 126 | 127 | -- | Rename a property for a label which is specified using a value-level proxy for 128 | -- | a type-level string. 129 | -- | 130 | -- | Note that the type of the resulting row must _lack_ the specified property. 131 | -- | Since duplicate labels are allowed, this is checked with a type class constraint. 132 | -- | 133 | -- | For example: 134 | -- | 135 | -- | ```purescript 136 | -- | rename (Proxy :: Proxy "x") (Proxy :: Proxy "y") 137 | -- | :: forall a r. Lacks "x" r => Lacks "y" r => { x :: a | r} -> { y :: a | r} 138 | -- | ``` 139 | rename :: forall prev next ty input inter output 140 | . IsSymbol prev 141 | => IsSymbol next 142 | => Cons prev ty inter input 143 | => Lacks prev inter 144 | => Cons next ty inter output 145 | => Lacks next inter 146 | => Proxy prev 147 | -> Proxy next 148 | -> Record input 149 | -> Record output 150 | rename prev next record = 151 | insert next (get prev record) (delete prev record :: Record inter) 152 | 153 | -- | Merges two records with the first record's labels taking precedence in the 154 | -- | case of overlaps. 155 | -- | 156 | -- | For example: 157 | -- | 158 | -- | ```purescript 159 | -- | merge { x: 1, y: "y" } { y: 2, z: true } 160 | -- | :: { x :: Int, y :: String, z :: Boolean } 161 | -- | ``` 162 | merge 163 | :: forall r1 r2 r3 r4 164 | . Union r1 r2 r3 165 | => Nub r3 r4 166 | => Record r1 167 | -> Record r2 168 | -> Record r4 169 | merge l r = runFn2 unsafeUnionFn l r 170 | 171 | -- | Merges two records with the first record's labels taking precedence in the 172 | -- | case of overlaps. Unlike `merge`, this does not remove duplicate labels 173 | -- | from the resulting record type. This can result in better inference for 174 | -- | some pipelines, deferring the need for a `Nub` constraint. 175 | -- | 176 | -- | For example: 177 | -- | 178 | -- | ```purescript 179 | -- | union { x: 1, y: "y" } { y: 2, z: true } 180 | -- | :: { x :: Int, y :: String, y :: Int, z :: Boolean } 181 | -- | ``` 182 | union 183 | :: forall r1 r2 r3 184 | . Union r1 r2 r3 185 | => Record r1 186 | -> Record r2 187 | -> Record r3 188 | union l r = runFn2 unsafeUnionFn l r 189 | 190 | -- | Merges two records where no labels overlap. This restriction exhibits 191 | -- | better inference than `merge` when the resulting record type is known, 192 | -- | but one argument is not. 193 | -- | 194 | -- | For example, hole `?help` is inferred to have type `{ b :: Int }` here: 195 | -- | 196 | -- | ```purescript 197 | -- | disjointUnion { a: 5 } ?help :: { a :: Int, b :: Int } 198 | -- | ``` 199 | disjointUnion 200 | :: forall r1 r2 r3 201 | . Union r1 r2 r3 202 | => Nub r3 r3 203 | => Record r1 204 | -> Record r2 205 | -> Record r3 206 | disjointUnion l r = runFn2 unsafeUnionFn l r 207 | 208 | -- | A coercion which removes duplicate labels from a record's type. 209 | nub 210 | :: forall r1 r2 211 | . Nub r1 r2 212 | => Record r1 213 | -> Record r2 214 | nub = unsafeCoerce 215 | 216 | -- | Check two records of the same type for equality. 217 | equal 218 | :: forall r rs 219 | . RowToList r rs 220 | => EqualFields rs r 221 | => Record r 222 | -> Record r 223 | -> Boolean 224 | equal a b = equalFields (Proxy :: Proxy rs) a b 225 | 226 | class EqualFields (rs :: RowList Type) (row :: Row Type) | rs -> row where 227 | equalFields :: Proxy rs -> Record row -> Record row -> Boolean 228 | 229 | instance equalFieldsCons 230 | :: 231 | ( IsSymbol name 232 | , Eq ty 233 | , Cons name ty tailRow row 234 | , EqualFields tail row 235 | ) => EqualFields (Cons name ty tail) row where 236 | equalFields _ a b = get' a == get' b && equalRest a b 237 | where 238 | get' = get (Proxy :: Proxy name) 239 | equalRest = equalFields (Proxy :: Proxy tail) 240 | 241 | instance equalFieldsNil :: EqualFields Nil row where 242 | equalFields _ _ _ = true 243 | -------------------------------------------------------------------------------- /src/Record/Builder.js: -------------------------------------------------------------------------------- 1 | export function copyRecord(rec) { 2 | var copy = {}; 3 | for (var key in rec) { 4 | if ({}.hasOwnProperty.call(rec, key)) { 5 | copy[key] = rec[key]; 6 | } 7 | } 8 | return copy; 9 | } 10 | 11 | export function unsafeInsert(l) { 12 | return function(a) { 13 | return function(rec) { 14 | rec[l] = a; 15 | return rec; 16 | }; 17 | }; 18 | } 19 | 20 | export function unsafeModify(l) { 21 | return function (f) { 22 | return function(rec) { 23 | rec[l] = f(rec[l]); 24 | return rec; 25 | }; 26 | }; 27 | } 28 | 29 | export function unsafeDelete(l) { 30 | return function(rec) { 31 | delete rec[l]; 32 | return rec; 33 | }; 34 | } 35 | 36 | export function unsafeRename(l1) { 37 | return function (l2) { 38 | return function (rec) { 39 | rec[l2] = rec[l1]; 40 | delete rec[l1]; 41 | return rec; 42 | }; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /src/Record/Builder.purs: -------------------------------------------------------------------------------- 1 | module Record.Builder 2 | ( Builder 3 | , build 4 | , buildFromScratch 5 | , flip 6 | , insert 7 | , modify 8 | , delete 9 | , rename 10 | , merge 11 | , union 12 | , disjointUnion 13 | , nub 14 | ) where 15 | 16 | import Prelude hiding (flip) 17 | 18 | import Data.Function (flip) as Function 19 | import Data.Function.Uncurried (runFn2) 20 | import Data.Symbol (class IsSymbol, reflectSymbol) 21 | import Prim.Row as Row 22 | import Record.Unsafe.Union (unsafeUnionFn) 23 | import Type.Proxy (Proxy) 24 | import Unsafe.Coerce (unsafeCoerce) 25 | 26 | foreign import copyRecord :: forall r1. Record r1 -> Record r1 27 | foreign import unsafeInsert :: forall a r1 r2. String -> a -> Record r1 -> Record r2 28 | foreign import unsafeModify :: forall a b r1 r2. String -> (a -> b) -> Record r1 -> Record r2 29 | foreign import unsafeDelete :: forall r1 r2. String -> Record r1 -> Record r2 30 | foreign import unsafeRename :: forall r1 r2. String -> String -> Record r1 -> Record r2 31 | 32 | -- | A `Builder` can be used to `build` a record by incrementally adding 33 | -- | fields in-place, instead of using `insert` and repeatedly generating new 34 | -- | immutable records which need to be garbage collected. 35 | -- | 36 | -- | The mutations accumulated in a `Builder` are safe because intermediate states can't be 37 | -- | observed. These mutations, then, are performed all-at-once in the `build` function. 38 | -- | 39 | -- | The `Category` instance for `Builder` can be used to compose builders. 40 | -- | 41 | -- | For example: 42 | -- | 43 | -- | ```purescript 44 | -- | build (insert x 42 >>> insert y "testing") {} :: { x :: Int, y :: String } 45 | -- | ``` 46 | newtype Builder a b = Builder (a -> b) 47 | 48 | -- | Build a record, starting from some other record. 49 | build :: forall r1 r2. Builder (Record r1) (Record r2) -> Record r1 -> Record r2 50 | build (Builder b) r1 = b (copyRecord r1) 51 | 52 | -- | Build a record from scratch. 53 | buildFromScratch :: forall r. Builder (Record ()) (Record r) -> Record r 54 | buildFromScratch = Function.flip build {} 55 | 56 | -- | Flip a function of one argument returning a builder. 57 | flip :: forall r1 r2 r3. (Record r1 -> Builder (Record r2) (Record r3)) -> Record r2 -> Builder (Record r1) (Record r3) 58 | flip f b = Builder \a -> build (f a) b 59 | 60 | derive newtype instance semigroupoidBuilder :: Semigroupoid Builder 61 | derive newtype instance categoryBuilder :: Category Builder 62 | 63 | -- | Build by inserting a new field. 64 | insert 65 | :: forall l a r1 r2 66 | . Row.Cons l a r1 r2 67 | => Row.Lacks l r1 68 | => IsSymbol l 69 | => Proxy l 70 | -> a 71 | -> Builder (Record r1) (Record r2) 72 | insert l a = Builder \r1 -> unsafeInsert (reflectSymbol l) a r1 73 | 74 | -- | Build by modifying an existing field. 75 | modify 76 | :: forall l a b r r1 r2 77 | . Row.Cons l a r r1 78 | => Row.Cons l b r r2 79 | => IsSymbol l 80 | => Proxy l 81 | -> (a -> b) 82 | -> Builder (Record r1) (Record r2) 83 | modify l f = Builder \r1 -> unsafeModify (reflectSymbol l) f r1 84 | 85 | -- | Build by deleting an existing field. 86 | delete 87 | :: forall l a r1 r2 88 | . IsSymbol l 89 | => Row.Lacks l r1 90 | => Row.Cons l a r1 r2 91 | => Proxy l 92 | -> Builder (Record r2) (Record r1) 93 | delete l = Builder \r2 -> unsafeDelete (reflectSymbol l) r2 94 | 95 | -- | Build by renaming an existing field. 96 | rename :: forall l1 l2 a r1 r2 r3 97 | . IsSymbol l1 98 | => IsSymbol l2 99 | => Row.Cons l1 a r2 r1 100 | => Row.Lacks l1 r2 101 | => Row.Cons l2 a r2 r3 102 | => Row.Lacks l2 r2 103 | => Proxy l1 104 | -> Proxy l2 105 | -> Builder (Record r1) (Record r3) 106 | rename l1 l2 = Builder \r1 -> unsafeRename (reflectSymbol l1) (reflectSymbol l2) r1 107 | 108 | -- | Build by merging existing fields from another record, taking precedence 109 | -- | in the case of overlaps. 110 | -- | 111 | -- | For example: 112 | -- | 113 | -- | ```purescript 114 | -- | build (merge { x: 1, y: "y" }) { y: 2, z: true } 115 | -- | :: { x :: Int, y :: String, z :: Boolean } 116 | -- | ``` 117 | merge 118 | :: forall r1 r2 r3 r4 119 | . Row.Union r1 r2 r3 120 | => Row.Nub r3 r4 121 | => Record r1 122 | -> Builder (Record r2) (Record r4) 123 | merge r1 = Builder \r2 -> runFn2 unsafeUnionFn r1 r2 124 | 125 | -- | Build by merging existing fields from another record, taking precedence 126 | -- | in the case of overlaps. Unlike `merge`, this does not remove duplicate 127 | -- | labels from the resulting record type. This can result in better inference 128 | -- | for some pipelines, deferring the need for a `Nub` constraint. 129 | -- | 130 | -- | For example: 131 | -- | 132 | -- | ```purescript 133 | -- | build (union { x: 1, y: "y" }) { y: 2, z: true } 134 | -- | :: { x :: Int, y :: String, y :: Int, z :: Boolean } 135 | -- | ``` 136 | union 137 | :: forall r1 r2 r3 138 | . Row.Union r1 r2 r3 139 | => Record r1 140 | -> Builder (Record r2) (Record r3) 141 | union r1 = Builder \r2 -> runFn2 unsafeUnionFn r1 r2 142 | 143 | -- | Build by merging some disjoint set of fields from another record. 144 | disjointUnion 145 | :: forall r1 r2 r3 146 | . Row.Union r1 r2 r3 147 | => Row.Nub r3 r3 148 | => Record r1 149 | -> Builder (Record r2) (Record r3) 150 | disjointUnion r1 = Builder \r2 -> runFn2 unsafeUnionFn r1 r2 151 | 152 | -- | A coercion which removes duplicate labels from a record's type. 153 | nub 154 | :: forall r1 r2 155 | . Row.Nub r1 r2 156 | => Builder (Record r1) (Record r2) 157 | nub = Builder unsafeCoerce 158 | -------------------------------------------------------------------------------- /src/Record/Unsafe/Union.js: -------------------------------------------------------------------------------- 1 | export function unsafeUnionFn(r1, r2) { 2 | var copy = {}; 3 | for (var k1 in r2) { 4 | if ({}.hasOwnProperty.call(r2, k1)) { 5 | copy[k1] = r2[k1]; 6 | } 7 | } 8 | for (var k2 in r1) { 9 | if ({}.hasOwnProperty.call(r1, k2)) { 10 | copy[k2] = r1[k2]; 11 | } 12 | } 13 | return copy; 14 | } 15 | -------------------------------------------------------------------------------- /src/Record/Unsafe/Union.purs: -------------------------------------------------------------------------------- 1 | module Record.Unsafe.Union where 2 | 3 | import Data.Function.Uncurried (Fn2, runFn2) 4 | 5 | foreign import unsafeUnionFn :: forall r1 r2 r3. Fn2 (Record r1) (Record r2) (Record r3) 6 | 7 | unsafeUnion :: forall r1 r2 r3. Record r1 -> Record r2 -> Record r3 8 | unsafeUnion = runFn2 unsafeUnionFn 9 | -------------------------------------------------------------------------------- /test/Examples.purs: -------------------------------------------------------------------------------- 1 | module Examples where 2 | 3 | import Prelude 4 | 5 | import Record as Record 6 | import Type.Proxy (Proxy(..)) 7 | 8 | x_ = Proxy :: Proxy "x" 9 | y_ = Proxy :: Proxy "y" 10 | z_ = Proxy :: Proxy "z" 11 | 12 | gotX :: Int 13 | gotX = Record.get x_ { x: 1 } 14 | 15 | insertedX :: { x :: Int } 16 | insertedX = Record.insert x_ 1 {} 17 | 18 | deletedX :: {} 19 | deletedX = Record.delete x_ { x: 1 } 20 | 21 | setX1 :: { x :: Int } 22 | setX1 = Record.set x_ 1 { x: 0 } 23 | 24 | setX2 :: { x :: Unit } 25 | setX2 = Record.set x_ unit { x: 0 } 26 | 27 | modifyX :: { x :: Int } 28 | modifyX = Record.modify x_ (\value -> value + 1) { x: 0 } 29 | 30 | mergedXY :: { x :: Int , y :: Int } 31 | mergedXY = Record.merge { x: 1 } { y: 1 } 32 | -------------------------------------------------------------------------------- /test/Main.purs: -------------------------------------------------------------------------------- 1 | module Test.Main where 2 | 3 | import Prelude 4 | 5 | import Effect (Effect) 6 | import Record (delete, equal, get, insert, merge, modify, rename, set) 7 | import Record.Builder as Builder 8 | import Record.Unsafe (unsafeHas) 9 | import Test.Assert (assert') 10 | import Type.Proxy (Proxy(..)) 11 | 12 | main :: Effect Unit 13 | main = do 14 | let x = Proxy :: Proxy "x" 15 | y = Proxy :: Proxy "y" 16 | z = Proxy :: Proxy "z" 17 | 18 | assert' "insert, get" $ 19 | get x (insert x 42 {}) == 42 20 | assert' "insert, modify, get" $ 21 | get x (modify x (_ + 1) (insert x 42 {})) == 43 22 | assert' "set, get" $ 23 | get x (set x 0 { x: 42 }) == 0 24 | assert' "set, modify, get" $ 25 | get x (modify x (_ + 1) (set x 0 { x: 42 })) == 1 26 | assert' "delete, get" $ 27 | get x (delete y { x: 42, y: 1337 }) == 42 28 | assert' "rename" $ 29 | get y (rename x y { x: 42 }) == 42 30 | assert' "equal" $ 31 | equal { a: 1, b: "b", c: true } { a: 1, b: "b", c: true } 32 | assert' "equal2" $ 33 | not $ equal { a: 1, b: "b", c: true } { a: 1, b: "b", c: false } 34 | assert' "merge" $ 35 | equal { x: 1, y: "y" } (merge { y: "y" } { x: 1, y: 2 }) 36 | assert' "unsafeHas1" $ 37 | unsafeHas "a" { a: 42 } 38 | assert' "unsafeHas2" $ 39 | not $ unsafeHas "b" { a: 42 } 40 | 41 | let testBuilder = Builder.build (Builder.insert x 42 42 | >>> Builder.merge { y: true, z: "testing" } 43 | >>> Builder.delete y 44 | >>> Builder.modify x show 45 | >>> Builder.rename z y) {} 46 | 47 | assert' "Record.Builder" $ 48 | testBuilder.x == "42" && testBuilder.y == "testing" 49 | 50 | assert' "Record.Builder.merge" $ 51 | let { x, y, z } = Builder.build (Builder.merge { x: 1, y: "y" }) { y: 2, z: true } 52 | :: { x :: Int, y :: String, z :: Boolean } 53 | in x == 1 && y == "y" && z 54 | 55 | assert' "Record.Builder.union" $ 56 | let { x, y, z } = Builder.build (Builder.union { x: 1, y: "y" }) { y: 2, z: true } 57 | :: { x :: Int, y :: String, y :: Int, z :: Boolean } 58 | in x == 1 && y == "y" && z 59 | 60 | assert' "Record.Builder.flip merge" $ 61 | let { x, y, z } = Builder.build (Builder.flip Builder.merge { x: 1, y: "y" }) { y: 2, z: true } 62 | :: { x :: Int, y :: Int, z :: Boolean } 63 | in x == 1 && y == 2 && z 64 | 65 | assert' "Record.Builder.flip union" $ 66 | let { x, y, z } = Builder.build (Builder.flip Builder.union { x: 1, y: "y" }) { y: 2, z: true } 67 | :: { x :: Int, y :: Int, y :: String, z :: Boolean } 68 | in x == 1 && y == 2 && z 69 | --------------------------------------------------------------------------------