├── .gitignore ├── CHANGELOG.md ├── LICENCE ├── README.md ├── bench ├── Main.elm ├── elm-package.json └── prep-bench.sh ├── elm-package.json ├── src ├── Array │ ├── Hamt.elm │ └── JsArray.elm └── Native │ └── JsArray.js └── tests ├── .gitignore ├── Main.elm ├── Tests.elm └── elm-package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | elm-stuff 3 | bench/index.html 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # ChangeLog 2 | 3 | ## 2.0.5 4 | 5 | * Performance improvements. 6 | 7 | ## 2.0.4 8 | 9 | * Fixed runtime exception when slicing a large array to a small (< 32 elements) one. 10 | 11 | ## 2.0.3 12 | 13 | * Optimized append when appending a small array. 14 | 15 | ## 2.0.2 16 | 17 | * Fixed bug where slicing everything but elements from the tail could return erronous result. 18 | * Made append faster, especially when appending a small array. 19 | * Made initialize faster. 20 | 21 | ## 2.0.1 22 | 23 | * Fixed bug where slice could return an `Array` which would have failed a `==` operation. 24 | 25 | ## 2.0.0 26 | 27 | * `Array` is now an opaque type. 28 | * Performance improvements. 29 | 30 | ## 1.1.0 31 | 32 | * Added `toString` function 33 | 34 | ## 1.0.0 35 | 36 | * Initial release 37 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Robin Heggelund Hansen 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Robin Heggelund Hansen nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elm array exploration 2 | 3 | This library explores an alternate Array implementation for Elm, using as little native code as possible. 4 | 5 | ## Use 6 | 7 | This package is a drop-in replacement to the array implementation in Elm core. 8 | To use, simply `import Array.Hamt as Array` instead of `import Array`. 9 | 10 | ## License 11 | 12 | This library uses the BSD3 License. See LICENSE for more information. 13 | -------------------------------------------------------------------------------- /bench/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (main) 2 | 3 | import Benchmark.Runner exposing (BenchmarkProgram, program) 4 | import Benchmark exposing (Benchmark, describe, benchmark1, benchmark2, benchmark3) 5 | import Array.Hamt as Hamt 6 | import Array 7 | 8 | 9 | main : BenchmarkProgram 10 | main = 11 | program <| suite 10000 12 | 13 | 14 | suite : Int -> Benchmark 15 | suite n = 16 | let 17 | sampleHamt = 18 | Hamt.initialize n identity 19 | 20 | sampleArray = 21 | Array.initialize n identity 22 | 23 | sampleList = 24 | List.range 1 n 25 | 26 | countFn acc _ = 27 | acc + 1 28 | 29 | isEven n = 30 | n % 2 == 0 31 | in 32 | describe ("Array (" ++ toString n ++ " elements)") 33 | [ Benchmark.compare "initialize" 34 | (benchmark2 "Array" Array.initialize n identity) 35 | (benchmark2 "HAMT" Hamt.initialize n identity) 36 | , Benchmark.compare "get" 37 | (benchmark2 "Array" Array.get 5 sampleArray) 38 | (benchmark2 "HAMT" Hamt.get 5 sampleHamt) 39 | , Benchmark.compare "set" 40 | (benchmark3 "Array" Array.set 7 5 sampleArray) 41 | (benchmark3 "HAMT" Hamt.set 7 5 sampleHamt) 42 | , Benchmark.compare "push" 43 | (benchmark2 "Array" Array.push 5 sampleArray) 44 | (benchmark2 "HAMT" Hamt.push 5 sampleHamt) 45 | , Benchmark.compare "append" 46 | (benchmark2 "Array" Array.append sampleArray sampleArray) 47 | (benchmark2 "HAMT" Hamt.append sampleHamt sampleHamt) 48 | , Benchmark.compare "append (small)" 49 | (benchmark2 "Array" Array.append sampleArray (Array.initialize 31 identity)) 50 | (benchmark2 "HAMT" Hamt.append sampleHamt (Hamt.initialize 31 identity)) 51 | , Benchmark.compare "slice (beginning, small)" 52 | (benchmark3 "Array" Array.slice 3 n sampleArray) 53 | (benchmark3 "HAMT" Hamt.slice 3 n sampleHamt) 54 | , Benchmark.compare "slice (beginning, big)" 55 | (benchmark3 "Array" Array.slice (n // 2) n sampleArray) 56 | (benchmark3 "HAMT" Hamt.slice (n // 2) n sampleHamt) 57 | , Benchmark.compare "slice (end, small)" 58 | (benchmark3 "Array" Array.slice 0 -3 sampleArray) 59 | (benchmark3 "HAMT" Hamt.slice 0 -3 sampleHamt) 60 | , Benchmark.compare "slice (end, big)" 61 | (benchmark3 "Array" Array.slice 0 (n // 2) sampleArray) 62 | (benchmark3 "HAMT" Hamt.slice 0 (n // 2) sampleHamt) 63 | , Benchmark.compare "foldl" 64 | (benchmark3 "Array" Array.foldl countFn 0 sampleArray) 65 | (benchmark3 "HAMT" Hamt.foldl countFn 0 sampleHamt) 66 | , Benchmark.compare "foldr" 67 | (benchmark3 "Array" Array.foldr countFn 0 sampleArray) 68 | (benchmark3 "HAMT" Hamt.foldr countFn 0 sampleHamt) 69 | , Benchmark.compare "filter" 70 | (benchmark2 "Array" Array.filter isEven sampleArray) 71 | (benchmark2 "HAMT" Hamt.filter isEven sampleHamt) 72 | , Benchmark.compare "map" 73 | (benchmark2 "Array" Array.map identity sampleArray) 74 | (benchmark2 "HAMT" Hamt.map identity sampleHamt) 75 | , Benchmark.compare "indexedMap" 76 | (benchmark2 "Array" Array.indexedMap (,) sampleArray) 77 | (benchmark2 "HAMT" Hamt.indexedMap (,) sampleHamt) 78 | , Benchmark.compare "toList" 79 | (benchmark1 "Array" Array.toList sampleArray) 80 | (benchmark1 "HAMT" Hamt.toList sampleHamt) 81 | , Benchmark.compare "fromList" 82 | (benchmark1 "Array" Array.fromList sampleList) 83 | (benchmark1 "HAMT" Hamt.fromList sampleList) 84 | , Benchmark.compare "indexedList" 85 | (benchmark1 "Array" Array.toIndexedList sampleArray) 86 | (benchmark1 "HAMT" Hamt.toIndexedList sampleHamt) 87 | , Benchmark.compare "= (equal, best case)" 88 | (benchmark2 "Array" (==) sampleArray (Array.set 5 5 sampleArray)) 89 | (benchmark2 "HAMT" (==) sampleHamt (Hamt.set 5 5 sampleHamt)) 90 | , Benchmark.compare "= (equal, worst case)" 91 | (benchmark2 "Array" (==) sampleArray (Array.map identity sampleArray)) 92 | (benchmark2 "HAMT" (==) sampleHamt (Hamt.map identity sampleHamt)) 93 | , Benchmark.compare "= (not equal)" 94 | (benchmark2 "Array" (==) sampleArray (Array.set 5 7 sampleArray)) 95 | (benchmark2 "HAMT" (==) sampleHamt (Hamt.set 5 7 sampleHamt)) 96 | ] 97 | -------------------------------------------------------------------------------- /bench/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/Skinney/elm-array-exploration.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "native-modules": true, 12 | "dependencies": { 13 | "BrianHicks/elm-benchmark": "1.0.2 <= v < 2.0.0", 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 15 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | -------------------------------------------------------------------------------- /bench/prep-bench.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | elm-make --yes --output index.html Main.elm 6 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.5", 3 | "summary": "New Array implementation for Elm.", 4 | "repository": "https://github.com/Skinney/elm-array-exploration.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Array.Hamt" 11 | ], 12 | "native-modules": true, 13 | "dependencies": { 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | -------------------------------------------------------------------------------- /src/Array/Hamt.elm: -------------------------------------------------------------------------------- 1 | module Array.Hamt 2 | exposing 3 | ( Array 4 | , empty 5 | , isEmpty 6 | , length 7 | , initialize 8 | , repeat 9 | , fromList 10 | , get 11 | , set 12 | , push 13 | , toList 14 | , toIndexedList 15 | , foldr 16 | , foldl 17 | , filter 18 | , map 19 | , indexedMap 20 | , append 21 | , slice 22 | , toString 23 | ) 24 | 25 | {-| Fast immutable arrays. The elements in an array must have the same type. 26 | 27 | 28 | # Arrays 29 | 30 | @docs Array 31 | 32 | 33 | # Creation 34 | 35 | @docs empty, initialize, repeat, fromList 36 | 37 | 38 | # Query 39 | 40 | @docs isEmpty, length, get 41 | 42 | 43 | # Manipulate 44 | 45 | @docs set, push, append, slice 46 | 47 | 48 | # Lists 49 | 50 | @docs toList, toIndexedList 51 | 52 | 53 | # Transform 54 | 55 | @docs foldl, foldr, filter, map, indexedMap 56 | 57 | 58 | # Display 59 | 60 | @docs toString 61 | 62 | -} 63 | 64 | import Bitwise 65 | import Array.JsArray as JsArray exposing (JsArray) 66 | 67 | 68 | {-| The array in this module is implemented as a tree with a high branching 69 | factor (number of elements at each level). In comparision, the `Dict` has 70 | a branching factor of 2 (left or right). 71 | 72 | The higher the branching factor, the more elements are stored at each level. 73 | This makes writes slower (more to copy per level), but reads faster 74 | (fewer traversals). In practice, 32 is a good compromise. 75 | 76 | The branching factor has to be a power of two (8, 16, 32, 64...). This is 77 | because we use the index to tell us which path to take when navigating the 78 | tree, and we do this by dividing it into several smaller numbers (see 79 | `shiftStep` documentation). By dividing the index into smaller numbers, we 80 | will always get a range which is a power of two (2 bits gives 0-3, 3 gives 81 | 0-7, 4 gives 0-15...). 82 | 83 | -} 84 | branchFactor : Int 85 | branchFactor = 86 | 32 87 | 88 | 89 | {-| A number is made up of several bits. For bitwise operations in javascript, 90 | numbers are treated as 32-bits integers. The number 1 is represented by 31 91 | zeros, and a one. The important thing to take from this, is that a 32-bit 92 | integer has enough information to represent several smaller numbers. 93 | 94 | For a branching factor of 32, a 32-bit index has enough information to store 6 95 | different numbers in the range of 0-31 (5 bits), and one number in the range of 96 | 0-3 (2 bits). This means that the tree of an array can have, at most, a depth 97 | of 7. 98 | 99 | An index essentially functions as a map. To figure out which branch to take at 100 | any given level of the tree, we need to shift (or move) the correct amount of 101 | bits so that those bits are at the front. We can then perform a bitwise and to 102 | read which of the 32 branches to take. 103 | 104 | The `shiftStep` specifices how many bits are required to represent the branching 105 | factor. 106 | 107 | -} 108 | shiftStep : Int 109 | shiftStep = 110 | logBase 2 (toFloat branchFactor) 111 | |> ceiling 112 | 113 | 114 | {-| A mask which, when used in a bitwise and, reads the first `shiftStep` bits 115 | in a number as a number of its own. 116 | -} 117 | bitMask : Int 118 | bitMask = 119 | Bitwise.shiftRightZfBy (32 - shiftStep) 0xFFFFFFFF 120 | 121 | 122 | {-| Representation of fast immutable arrays. You can create arrays of integers 123 | (`Array Int`) or strings (`Array String`) or any other type of value you can 124 | dream up. 125 | -} 126 | type Array a 127 | = {- 128 | * length : Int = The length of the array. 129 | * startShift : Int = How many bits to shift the index to get the 130 | slot for the first level of the tree. 131 | * tree : Tree a = The actual tree. 132 | * tail : JsArray a = The tail of the array. Inserted into tree when 133 | number of elements is equal to the branching factor. This is an 134 | optimization. It makes operations at the end (push, pop, read, write) 135 | fast. 136 | -} 137 | Array Int Int (Tree a) (JsArray a) 138 | 139 | 140 | {-| Each level in the tree is represented by a `JsArray` of `Node`s. 141 | A `Node` can either be a subtree (the next level of the tree) or, if 142 | we're at the bottom, a `JsArray` of values (also known as a leaf). 143 | -} 144 | type Node a 145 | = SubTree (Tree a) 146 | | Leaf (JsArray a) 147 | 148 | 149 | type alias Tree a = 150 | JsArray (Node a) 151 | 152 | 153 | {-| Return an empty array. 154 | 155 | length empty == 0 156 | 157 | -} 158 | empty : Array a 159 | empty = 160 | {- 161 | `startShift` is only used when there is at least one `Node` in the 162 | `tree`. The minimal value is therefore equal to the `shiftStep`. 163 | -} 164 | Array 0 shiftStep JsArray.empty JsArray.empty 165 | 166 | 167 | {-| Determine if an array is empty. 168 | 169 | isEmpty empty == True 170 | 171 | -} 172 | isEmpty : Array a -> Bool 173 | isEmpty (Array length _ _ _) = 174 | length == 0 175 | 176 | 177 | {-| Return the length of an array. 178 | 179 | length (fromList [1,2,3]) == 3 180 | 181 | -} 182 | length : Array a -> Int 183 | length (Array length _ _ _) = 184 | length 185 | 186 | 187 | {-| Initialize an array. `initialize n f` creates an array of length `n` with 188 | the element at index `i` initialized to the result of `(f i)`. 189 | 190 | initialize 4 identity == fromList [0,1,2,3] 191 | initialize 4 (\n -> n*n) == fromList [0,1,4,9] 192 | initialize 4 (always 0) == fromList [0,0,0,0] 193 | 194 | -} 195 | initialize : Int -> (Int -> a) -> Array a 196 | initialize length fn = 197 | if length <= 0 then 198 | empty 199 | else 200 | let 201 | tailLen = 202 | rem length branchFactor 203 | 204 | tail = 205 | JsArray.initialize tailLen (length - tailLen) fn 206 | 207 | initialFromIndex = 208 | length - tailLen - branchFactor 209 | in 210 | initializeHelp fn initialFromIndex length [] tail 211 | 212 | 213 | initializeHelp : (Int -> a) -> Int -> Int -> List (Node a) -> JsArray a -> Array a 214 | initializeHelp fn fromIndex length nodeList tail = 215 | if fromIndex < 0 then 216 | builderToArray False 217 | { tail = tail 218 | , nodeList = nodeList 219 | , nodeListSize = length // branchFactor 220 | } 221 | else 222 | let 223 | leaf = 224 | Leaf <| JsArray.initialize branchFactor fromIndex fn 225 | in 226 | initializeHelp 227 | fn 228 | (fromIndex - branchFactor) 229 | length 230 | (leaf :: nodeList) 231 | tail 232 | 233 | 234 | {-| Creates an array with a given length, filled with a default element. 235 | 236 | repeat 5 0 == fromList [0,0,0,0,0] 237 | repeat 3 "cat" == fromList ["cat","cat","cat"] 238 | 239 | Notice that `repeat 3 x` is the same as `initialize 3 (always x)`. 240 | 241 | -} 242 | repeat : Int -> a -> Array a 243 | repeat n e = 244 | initialize n (\_ -> e) 245 | 246 | 247 | {-| Create an array from a `List`. 248 | -} 249 | fromList : List a -> Array a 250 | fromList ls = 251 | case ls of 252 | [] -> 253 | empty 254 | 255 | _ -> 256 | fromListHelp ls [] 0 257 | 258 | 259 | fromListHelp : List a -> List (Node a) -> Int -> Array a 260 | fromListHelp list nodeList nodeListSize = 261 | let 262 | ( jsArray, remainingItems ) = 263 | JsArray.initializeFromList branchFactor list 264 | in 265 | if JsArray.length jsArray < branchFactor then 266 | builderToArray True 267 | { tail = jsArray 268 | , nodeList = nodeList 269 | , nodeListSize = nodeListSize 270 | } 271 | else 272 | fromListHelp 273 | remainingItems 274 | ((Leaf jsArray) :: nodeList) 275 | (nodeListSize + 1) 276 | 277 | 278 | {-| Return the array represented as a string. 279 | 280 | (toString <| Array.fromList [1,2,3]) == "Array [1,2,3]" 281 | 282 | -} 283 | toString : Array a -> String 284 | toString array = 285 | let 286 | content = 287 | array 288 | |> map Basics.toString 289 | |> toList 290 | |> String.join "," 291 | in 292 | "Array [" ++ content ++ "]" 293 | 294 | 295 | {-| Return `Just` the element at the index or `Nothing`` if the index is out of 296 | range. 297 | 298 | get 0 (fromList [0,1,2]) == Just 0 299 | get 2 (fromList [0,1,2]) == Just 2 300 | get 5 (fromList [0,1,2]) == Nothing 301 | get -1 (fromList [0,1,2]) == Nothing 302 | 303 | -} 304 | get : Int -> Array a -> Maybe a 305 | get idx (Array length startShift tree tail) = 306 | if idx < 0 || idx >= length then 307 | Nothing 308 | else if idx >= tailIndex length then 309 | Just <| JsArray.unsafeGet (Bitwise.and bitMask idx) tail 310 | else 311 | Just <| getHelp startShift idx tree 312 | 313 | 314 | getHelp : Int -> Int -> Tree a -> a 315 | getHelp shift idx tree = 316 | let 317 | pos = 318 | Bitwise.and bitMask <| Bitwise.shiftRightZfBy shift idx 319 | in 320 | case JsArray.unsafeGet pos tree of 321 | SubTree subTree -> 322 | getHelp (shift - shiftStep) idx subTree 323 | 324 | Leaf values -> 325 | JsArray.unsafeGet (Bitwise.and bitMask idx) values 326 | 327 | 328 | {-| Given an array length, return the index of the first element in the tail. 329 | Commonly used to check if a given index references something in the tail. 330 | -} 331 | tailIndex : Int -> Int 332 | tailIndex len = 333 | len 334 | |> Bitwise.shiftRightZfBy 5 335 | |> Bitwise.shiftLeftBy 5 336 | 337 | 338 | {-| Set the element at a particular index. Returns an updated array. 339 | If the index is out of range, the array is unaltered. 340 | 341 | set 1 7 (fromList [1,2,3]) == fromList [1,7,3] 342 | 343 | -} 344 | set : Int -> a -> Array a -> Array a 345 | set idx val ((Array length startShift tree tail) as arr) = 346 | if idx < 0 || idx >= length then 347 | arr 348 | else if idx >= tailIndex length then 349 | Array length startShift tree <| 350 | JsArray.unsafeSet (Bitwise.and bitMask idx) val tail 351 | else 352 | Array 353 | length 354 | startShift 355 | (setHelp startShift idx val tree) 356 | tail 357 | 358 | 359 | setHelp : Int -> Int -> a -> Tree a -> Tree a 360 | setHelp shift idx val tree = 361 | let 362 | pos = 363 | Bitwise.and bitMask <| Bitwise.shiftRightZfBy shift idx 364 | in 365 | case JsArray.unsafeGet pos tree of 366 | SubTree subTree -> 367 | let 368 | newSub = 369 | setHelp (shift - shiftStep) idx val subTree 370 | in 371 | JsArray.unsafeSet pos (SubTree newSub) tree 372 | 373 | Leaf values -> 374 | let 375 | newLeaf = 376 | JsArray.unsafeSet (Bitwise.and bitMask idx) val values 377 | in 378 | JsArray.unsafeSet pos (Leaf newLeaf) tree 379 | 380 | 381 | {-| Push an element onto the end of an array. 382 | 383 | push 3 (fromList [1,2]) == fromList [1,2,3] 384 | 385 | -} 386 | push : a -> Array a -> Array a 387 | push a ((Array _ _ _ tail) as arr) = 388 | unsafeReplaceTail (JsArray.push a tail) arr 389 | 390 | 391 | {-| Replaces the tail of an array. If the length of the tail equals the 392 | `branchFactor`, it is inserted into the tree, and the tail cleared. 393 | 394 | WARNING: For performance reasons, this function does not check if the new tail 395 | has a length equal to or beneath the `branchFactor`. Make sure this is the case 396 | before using this function. 397 | 398 | -} 399 | unsafeReplaceTail : JsArray a -> Array a -> Array a 400 | unsafeReplaceTail newTail (Array length startShift tree tail) = 401 | let 402 | originalTailLen = 403 | JsArray.length tail 404 | 405 | newTailLen = 406 | JsArray.length newTail 407 | 408 | newArrayLen = 409 | length + (newTailLen - originalTailLen) 410 | in 411 | if newTailLen == branchFactor then 412 | let 413 | overflow = 414 | Bitwise.shiftRightZfBy shiftStep newArrayLen > Bitwise.shiftLeftBy startShift 1 415 | in 416 | if overflow then 417 | let 418 | newShift = 419 | startShift + shiftStep 420 | 421 | newTree = 422 | JsArray.singleton (SubTree tree) 423 | |> insertTailInTree newShift length newTail 424 | in 425 | Array 426 | newArrayLen 427 | newShift 428 | newTree 429 | JsArray.empty 430 | else 431 | Array 432 | newArrayLen 433 | startShift 434 | (insertTailInTree startShift length newTail tree) 435 | JsArray.empty 436 | else 437 | Array 438 | newArrayLen 439 | startShift 440 | tree 441 | newTail 442 | 443 | 444 | insertTailInTree : Int -> Int -> JsArray a -> Tree a -> Tree a 445 | insertTailInTree shift idx tail tree = 446 | let 447 | pos = 448 | Bitwise.and bitMask <| Bitwise.shiftRightZfBy shift idx 449 | in 450 | if pos >= JsArray.length tree then 451 | if shift == 5 then 452 | JsArray.push (Leaf tail) tree 453 | else 454 | let 455 | newSub = 456 | JsArray.empty 457 | |> insertTailInTree (shift - shiftStep) idx tail 458 | |> SubTree 459 | in 460 | JsArray.push newSub tree 461 | else 462 | let 463 | val = 464 | JsArray.unsafeGet pos tree 465 | in 466 | case val of 467 | SubTree subTree -> 468 | let 469 | newSub = 470 | subTree 471 | |> insertTailInTree (shift - shiftStep) idx tail 472 | |> SubTree 473 | in 474 | JsArray.unsafeSet pos newSub tree 475 | 476 | Leaf _ -> 477 | let 478 | newSub = 479 | JsArray.singleton val 480 | |> insertTailInTree (shift - shiftStep) idx tail 481 | |> SubTree 482 | in 483 | JsArray.unsafeSet pos newSub tree 484 | 485 | 486 | {-| Create a list of elements from an array. 487 | 488 | toList (fromList [3,5,8]) == [3,5,8] 489 | 490 | -} 491 | toList : Array a -> List a 492 | toList arr = 493 | foldr (::) [] arr 494 | 495 | 496 | {-| Create an indexed list from an array. Each element of the array will be 497 | paired with its index. 498 | 499 | toIndexedList (fromList ["cat","dog"]) == [(0,"cat"), (1,"dog")] 500 | 501 | -} 502 | toIndexedList : Array a -> List ( Int, a ) 503 | toIndexedList ((Array length _ _ _) as arr) = 504 | let 505 | helper n ( idx, ls ) = 506 | ( idx - 1, ( idx, n ) :: ls ) 507 | in 508 | foldr helper ( length - 1, [] ) arr 509 | |> Tuple.second 510 | 511 | 512 | {-| Reduce an array from the right. Read `foldr` as fold from the right. 513 | 514 | foldr (+) 0 (repeat 3 5) == 15 515 | 516 | -} 517 | foldr : (a -> b -> b) -> b -> Array a -> b 518 | foldr f init (Array _ _ tree tail) = 519 | let 520 | helper i acc = 521 | case i of 522 | SubTree subTree -> 523 | JsArray.foldr helper acc subTree 524 | 525 | Leaf values -> 526 | JsArray.foldr f acc values 527 | 528 | acc = 529 | JsArray.foldr f init tail 530 | in 531 | JsArray.foldr helper acc tree 532 | 533 | 534 | {-| Reduce an array from the left. Read `foldl` as fold from the left. 535 | 536 | foldl (::) [] (fromList [1,2,3]) == [3,2,1] 537 | 538 | -} 539 | foldl : (a -> b -> b) -> b -> Array a -> b 540 | foldl f init (Array _ _ tree tail) = 541 | let 542 | helper i acc = 543 | case i of 544 | SubTree subTree -> 545 | JsArray.foldl helper acc subTree 546 | 547 | Leaf values -> 548 | JsArray.foldl f acc values 549 | 550 | acc = 551 | JsArray.foldl helper init tree 552 | in 553 | JsArray.foldl f acc tail 554 | 555 | 556 | {-| Keep only elements that satisfy the predicate. 557 | 558 | filter isEven (fromList [1,2,3]) == (fromList [2]) 559 | 560 | -} 561 | filter : (a -> Bool) -> Array a -> Array a 562 | filter f arr = 563 | let 564 | helper n acc = 565 | if f n then 566 | n :: acc 567 | else 568 | acc 569 | in 570 | foldr helper [] arr 571 | |> fromList 572 | 573 | 574 | {-| Apply a function on every element in an array. 575 | 576 | map sqrt (fromList [1,4,9]) == fromList [1,2,3] 577 | 578 | -} 579 | map : (a -> b) -> Array a -> Array b 580 | map f (Array length startShift tree tail) = 581 | let 582 | helper i = 583 | case i of 584 | SubTree subTree -> 585 | SubTree <| JsArray.map helper subTree 586 | 587 | Leaf values -> 588 | Leaf <| JsArray.map f values 589 | in 590 | Array 591 | length 592 | startShift 593 | (JsArray.map helper tree) 594 | (JsArray.map f tail) 595 | 596 | 597 | {-| Apply a function on every element with its index as first argument. 598 | 599 | indexedMap (*) (fromList [5,5,5]) == fromList [0,5,10] 600 | 601 | -} 602 | indexedMap : (Int -> a -> b) -> Array a -> Array b 603 | indexedMap f ((Array length _ tree tail) as arr) = 604 | let 605 | helper node builder = 606 | case node of 607 | SubTree subTree -> 608 | JsArray.foldl helper builder subTree 609 | 610 | Leaf leaf -> 611 | let 612 | offset = 613 | builder.nodeListSize * branchFactor 614 | 615 | mappedLeaf = 616 | Leaf <| JsArray.indexedMap f offset leaf 617 | in 618 | { tail = builder.tail 619 | , nodeList = mappedLeaf :: builder.nodeList 620 | , nodeListSize = builder.nodeListSize + 1 621 | } 622 | 623 | initialBuilder = 624 | { tail = JsArray.indexedMap f (tailIndex length) tail 625 | , nodeList = [] 626 | , nodeListSize = 0 627 | } 628 | in 629 | JsArray.foldl helper initialBuilder tree 630 | |> builderToArray True 631 | 632 | 633 | {-| Append two arrays to a new one. 634 | 635 | append (repeat 2 42) (repeat 3 81) == fromList [42,42,81,81,81] 636 | 637 | -} 638 | append : Array a -> Array a -> Array a 639 | append ((Array _ _ _ aTail) as a) (Array bLen _ bTree bTail) = 640 | -- The magic number 4 has been found with benchmarks 641 | if bLen <= (branchFactor * 4) then 642 | let 643 | foldHelper node arr = 644 | case node of 645 | SubTree tree -> 646 | JsArray.foldl foldHelper arr tree 647 | 648 | Leaf leaf -> 649 | appendHelpTree leaf arr 650 | in 651 | JsArray.foldl foldHelper a bTree 652 | |> appendHelpTree bTail 653 | else 654 | let 655 | builder = 656 | builderFromArray a 657 | 658 | foldHelper node builder = 659 | case node of 660 | SubTree tree -> 661 | JsArray.foldl foldHelper builder tree 662 | 663 | Leaf leaf -> 664 | appendHelpBuilder leaf builder 665 | in 666 | JsArray.foldl foldHelper builder bTree 667 | |> appendHelpBuilder bTail 668 | |> builderToArray True 669 | 670 | 671 | appendHelpTree : JsArray a -> Array a -> Array a 672 | appendHelpTree toAppend ((Array length shiftStep tree tail) as arr) = 673 | let 674 | appended = 675 | JsArray.appendN branchFactor tail toAppend 676 | 677 | itemsToAppend = 678 | JsArray.length toAppend 679 | 680 | notAppended = 681 | branchFactor - (JsArray.length tail) - itemsToAppend 682 | 683 | newArray = 684 | unsafeReplaceTail appended arr 685 | in 686 | if notAppended < 0 then 687 | let 688 | nextTail = 689 | JsArray.slice notAppended itemsToAppend toAppend 690 | in 691 | unsafeReplaceTail nextTail newArray 692 | else 693 | newArray 694 | 695 | 696 | appendHelpBuilder : JsArray a -> Builder a -> Builder a 697 | appendHelpBuilder tail builder = 698 | let 699 | appended = 700 | JsArray.appendN branchFactor builder.tail tail 701 | 702 | tailLen = 703 | JsArray.length tail 704 | 705 | notAppended = 706 | branchFactor - (JsArray.length builder.tail) - tailLen 707 | in 708 | if notAppended < 0 then 709 | { tail = JsArray.slice notAppended tailLen tail 710 | , nodeList = (Leaf appended) :: builder.nodeList 711 | , nodeListSize = builder.nodeListSize + 1 712 | } 713 | else if notAppended == 0 then 714 | { tail = JsArray.empty 715 | , nodeList = (Leaf appended) :: builder.nodeList 716 | , nodeListSize = builder.nodeListSize + 1 717 | } 718 | else 719 | { tail = appended 720 | , nodeList = builder.nodeList 721 | , nodeListSize = builder.nodeListSize 722 | } 723 | 724 | 725 | {-| Get a sub-section of an array: `(slice start end array)`. The `start` is a 726 | zero-based index where we will start our slice. The `end` is a zero-based index 727 | that indicates the end of the slice. The slice extracts up to but not including 728 | `end`. 729 | 730 | slice 0 3 (fromList [0,1,2,3,4]) == fromList [0,1,2] 731 | slice 1 4 (fromList [0,1,2,3,4]) == fromList [1,2,3] 732 | 733 | Both the `start` and `end` indexes can be negative, indicating an offset from 734 | the end of the array. 735 | 736 | slice 1 -1 (fromList [0,1,2,3,4]) == fromList [1,2,3] 737 | slice -2 5 (fromList [0,1,2,3,4]) == fromList [3,4] 738 | 739 | This makes it pretty easy to `pop` the last element off of an array: 740 | `slice 0 -1 array` 741 | 742 | -} 743 | slice : Int -> Int -> Array a -> Array a 744 | slice from to arr = 745 | let 746 | correctFrom = 747 | translateIndex from arr 748 | 749 | correctTo = 750 | translateIndex to arr 751 | in 752 | if correctFrom > correctTo then 753 | empty 754 | else 755 | arr 756 | |> sliceRight correctTo 757 | |> sliceLeft correctFrom 758 | 759 | 760 | {-| Given a relative array index, convert it into an absolute one. 761 | 762 | translateIndex -1 someArray == someArray.length - 1 763 | translateIndex -10 someArray == someArray.length - 10 764 | translateIndex 5 someArray == 5 765 | 766 | -} 767 | translateIndex : Int -> Array a -> Int 768 | translateIndex idx (Array length _ _ _) = 769 | let 770 | posIndex = 771 | if idx < 0 then 772 | length + idx 773 | else 774 | idx 775 | in 776 | if posIndex < 0 then 777 | 0 778 | else if posIndex > length then 779 | length 780 | else 781 | posIndex 782 | 783 | 784 | {-| This function slices the tree from the right. 785 | 786 | First, two things are tested: 787 | 788 | 1. If the array does not need slicing, return the original array. 789 | 2. If the array can be sliced by only slicing the tail, slice the tail. 790 | 791 | Otherwise, we do the following: 792 | 793 | 1. Find the new tail in the tree, promote it to the root tail position and 794 | slice it. 795 | 2. Slice every sub tree. 796 | 3. Promote subTrees until the tree has the correct height. 797 | 798 | -} 799 | sliceRight : Int -> Array a -> Array a 800 | sliceRight end ((Array length startShift tree tail) as arr) = 801 | if end == length then 802 | arr 803 | else if end >= tailIndex length then 804 | Array end startShift tree <| 805 | JsArray.slice 0 (Bitwise.and bitMask end) tail 806 | else 807 | let 808 | endIdx = 809 | tailIndex end 810 | 811 | depth = 812 | (endIdx - 1) 813 | |> max 1 814 | |> toFloat 815 | |> logBase (toFloat branchFactor) 816 | |> floor 817 | 818 | newShift = 819 | max 5 <| depth * shiftStep 820 | in 821 | Array 822 | end 823 | newShift 824 | (tree 825 | |> sliceTree startShift endIdx 826 | |> hoistTree startShift newShift 827 | ) 828 | (fetchNewTail startShift end endIdx tree) 829 | 830 | 831 | {-| Slice and return the `Leaf` node after what is to be the last node 832 | in the sliced tree. 833 | -} 834 | fetchNewTail : Int -> Int -> Int -> Tree a -> JsArray a 835 | fetchNewTail shift end treeEnd tree = 836 | let 837 | pos = 838 | Bitwise.and bitMask <| Bitwise.shiftRightZfBy shift treeEnd 839 | in 840 | case JsArray.unsafeGet pos tree of 841 | SubTree sub -> 842 | fetchNewTail (shift - shiftStep) end treeEnd sub 843 | 844 | Leaf values -> 845 | JsArray.slice 0 (Bitwise.and bitMask end) values 846 | 847 | 848 | {-| Shorten the root `Node` of the tree so it is long enough to contain 849 | the `Node` indicated by `endIdx`. Then recursively perform the same operation 850 | to the last node of each `SubTree`. 851 | -} 852 | sliceTree : Int -> Int -> Tree a -> Tree a 853 | sliceTree shift endIdx tree = 854 | let 855 | lastPos = 856 | Bitwise.and bitMask <| Bitwise.shiftRightZfBy shift endIdx 857 | in 858 | case JsArray.unsafeGet lastPos tree of 859 | SubTree sub -> 860 | let 861 | newSub = 862 | sliceTree (shift - shiftStep) endIdx sub 863 | in 864 | if JsArray.length newSub == 0 then 865 | -- The sub is empty, slice it away 866 | JsArray.slice 0 lastPos tree 867 | else 868 | tree 869 | |> JsArray.slice 0 (lastPos + 1) 870 | |> JsArray.unsafeSet lastPos (SubTree newSub) 871 | 872 | -- This is supposed to be the new tail. Fetched by `fetchNewTail`. 873 | -- Slice up to, but not including, this point. 874 | Leaf _ -> 875 | JsArray.slice 0 lastPos tree 876 | 877 | 878 | {-| The tree is supposed to be of a certain depth. Since slicing removes 879 | elements, it could be that the tree should have a smaller depth 880 | than it had originally. This function shortens the height if it is necessary 881 | to do so. 882 | -} 883 | hoistTree : Int -> Int -> Tree a -> Tree a 884 | hoistTree oldShift newShift tree = 885 | if oldShift <= newShift || JsArray.length tree == 0 then 886 | tree 887 | else 888 | case JsArray.unsafeGet 0 tree of 889 | SubTree sub -> 890 | hoistTree (oldShift - shiftStep) newShift sub 891 | 892 | Leaf _ -> 893 | tree 894 | 895 | 896 | {-| This function slices the tree from the left. Such an operation will change 897 | the index of every element after the slice. Which means that we will have to 898 | rebuild the array. 899 | 900 | First, two things are tested: 901 | 902 | 1. If the array does not need slicing, return the original array. 903 | 2. If the slice removes every element but those in the tail, slice the tail and 904 | set the tree to the empty array. 905 | 906 | Otherwise, we do the following: 907 | 908 | 1. Add every leaf node in the tree to a list. 909 | 2. Drop the nodes which are supposed to be sliced away. 910 | 3. Slice the head node of the list, which represents the start of the new array. 911 | 4. Create a builder with the tail set as the node from the previous step. 912 | 5. Append the remaining nodes into this builder, and create the array. 913 | 914 | -} 915 | sliceLeft : Int -> Array a -> Array a 916 | sliceLeft from ((Array length _ tree tail) as arr) = 917 | if from == 0 then 918 | arr 919 | else if from >= tailIndex length then 920 | Array (length - from) shiftStep JsArray.empty <| 921 | JsArray.slice (from - tailIndex length) (JsArray.length tail) tail 922 | else 923 | let 924 | helper node acc = 925 | case node of 926 | SubTree subTree -> 927 | JsArray.foldr helper acc subTree 928 | 929 | Leaf leaf -> 930 | leaf :: acc 931 | 932 | leafNodes = 933 | JsArray.foldr helper [ tail ] tree 934 | 935 | skipNodes = 936 | from // branchFactor 937 | 938 | nodesToInsert = 939 | List.drop skipNodes leafNodes 940 | in 941 | case nodesToInsert of 942 | [] -> 943 | empty 944 | 945 | head :: rest -> 946 | let 947 | firstSlice = 948 | from - (skipNodes * branchFactor) 949 | 950 | initialBuilder = 951 | { tail = 952 | JsArray.slice 953 | firstSlice 954 | (JsArray.length head) 955 | head 956 | , nodeList = [] 957 | , nodeListSize = 0 958 | } 959 | in 960 | List.foldl appendHelpBuilder initialBuilder rest 961 | |> builderToArray True 962 | 963 | 964 | {-| A builder contains all information necessary to build an array. Adding 965 | information to the builder is fast. A builder is therefore a suitable 966 | intermediary for constructing arrays. 967 | -} 968 | type alias Builder a = 969 | { tail : JsArray a 970 | , nodeList : List (Node a) 971 | , nodeListSize : Int 972 | } 973 | 974 | 975 | {-| The empty builder. 976 | -} 977 | emptyBuilder : Builder a 978 | emptyBuilder = 979 | { tail = JsArray.empty 980 | , nodeList = [] 981 | , nodeListSize = 0 982 | } 983 | 984 | 985 | {-| Converts an array to a builder. 986 | -} 987 | builderFromArray : Array a -> Builder a 988 | builderFromArray (Array length _ tree tail) = 989 | let 990 | helper node acc = 991 | case node of 992 | SubTree tree -> 993 | JsArray.foldl helper acc tree 994 | 995 | Leaf _ -> 996 | node :: acc 997 | in 998 | { tail = tail 999 | , nodeList = JsArray.foldl helper [] tree 1000 | , nodeListSize = length // branchFactor 1001 | } 1002 | 1003 | 1004 | {-| Construct an array with the information in a given builder. 1005 | 1006 | Due to the nature of `List` the list of nodes in a builder will often 1007 | be in reverse order (that is, the first leaf of the array is the last 1008 | node in the node list). This function therefore allows the caller to 1009 | specify if the node list should be reversed before building the array. 1010 | 1011 | -} 1012 | builderToArray : Bool -> Builder a -> Array a 1013 | builderToArray reverseNodeList builder = 1014 | if builder.nodeListSize == 0 then 1015 | Array 1016 | (JsArray.length builder.tail) 1017 | shiftStep 1018 | JsArray.empty 1019 | builder.tail 1020 | else 1021 | let 1022 | treeLen = 1023 | builder.nodeListSize * branchFactor 1024 | 1025 | depth = 1026 | (treeLen - 1) 1027 | |> toFloat 1028 | |> logBase (toFloat branchFactor) 1029 | |> floor 1030 | 1031 | correctNodeList = 1032 | if reverseNodeList then 1033 | List.reverse builder.nodeList 1034 | else 1035 | builder.nodeList 1036 | 1037 | tree = 1038 | treeFromBuilder correctNodeList builder.nodeListSize 1039 | in 1040 | Array 1041 | (JsArray.length builder.tail + treeLen) 1042 | (max 5 <| depth * shiftStep) 1043 | tree 1044 | builder.tail 1045 | 1046 | 1047 | {-| Takes a list of leaves and an `Int` specifying how many leaves there are, 1048 | and builds a tree structure to be used in an `Array`. 1049 | -} 1050 | treeFromBuilder : List (Node a) -> Int -> Tree a 1051 | treeFromBuilder nodeList nodeListSize = 1052 | let 1053 | newNodeSize = 1054 | ((toFloat nodeListSize) / (toFloat branchFactor)) 1055 | |> ceiling 1056 | in 1057 | if newNodeSize == 1 then 1058 | JsArray.initializeFromList branchFactor nodeList 1059 | |> Tuple.first 1060 | else 1061 | treeFromBuilder 1062 | (compressNodes nodeList []) 1063 | newNodeSize 1064 | 1065 | 1066 | {-| Takes a list of nodes and return a list of `SubTree`s containing those 1067 | nodes. 1068 | -} 1069 | compressNodes : List (Node a) -> List (Node a) -> List (Node a) 1070 | compressNodes nodes acc = 1071 | let 1072 | ( node, remainingNodes ) = 1073 | JsArray.initializeFromList branchFactor nodes 1074 | 1075 | newAcc = 1076 | (SubTree node) :: acc 1077 | in 1078 | case remainingNodes of 1079 | [] -> 1080 | List.reverse newAcc 1081 | 1082 | _ -> 1083 | compressNodes remainingNodes newAcc 1084 | -------------------------------------------------------------------------------- /src/Array/JsArray.elm: -------------------------------------------------------------------------------- 1 | module Array.JsArray 2 | exposing 3 | ( JsArray 4 | , empty 5 | , singleton 6 | , length 7 | , initialize 8 | , initializeFromList 9 | , unsafeGet 10 | , unsafeSet 11 | , push 12 | , foldl 13 | , foldr 14 | , map 15 | , indexedMap 16 | , slice 17 | , appendN 18 | ) 19 | 20 | {-| This library provides an immutable version of native javascript arrays. 21 | 22 | NOTE: All manipulations causes a copy of the entire array, this can be slow. 23 | For general purpose use, try the `Array` module instead. 24 | 25 | # Arrays 26 | @docs JsArray 27 | 28 | # Creation 29 | @docs empty, singleton, initialize, listInitialize 30 | 31 | # Basics 32 | @docs length, unsafeGet, unsafeSet, push 33 | 34 | # Transformation 35 | @docs foldl, foldr, map, slice, merge 36 | 37 | -} 38 | 39 | import Native.JsArray 40 | 41 | 42 | {-| Representation of a javascript array. 43 | -} 44 | type JsArray a 45 | = JsArray a 46 | 47 | 48 | {-| Return an empty array. 49 | -} 50 | empty : JsArray a 51 | empty = 52 | Native.JsArray.empty 53 | 54 | 55 | {-| Return an array containing a single value. 56 | -} 57 | singleton : a -> JsArray a 58 | singleton = 59 | Native.JsArray.singleton 60 | 61 | 62 | {-| Return the length of the array. 63 | -} 64 | length : JsArray a -> Int 65 | length = 66 | Native.JsArray.length 67 | 68 | 69 | {-| Initialize an array. `initalize n offset fn` creates an array of length `n` 70 | with the element at index `i` initialized to the result of `(f (i + offset))`. 71 | 72 | The offset parameter is there so one can avoid creating a closure for this use 73 | case. This is an optimization that has proved useful in the `Array` module. 74 | 75 | initialize 3 5 identity == [5,6,7] 76 | -} 77 | initialize : Int -> Int -> (Int -> a) -> JsArray a 78 | initialize = 79 | Native.JsArray.initialize 80 | 81 | 82 | {-| Initialize an array from a list. `initializeFromList n ls` creates an array of, 83 | at most, `n` elements from the list. The return value is a tuple containing the 84 | created array as well as a list without the first `n` elements. 85 | 86 | This function was created specifically for the `Array` module, which never wants 87 | to create `JsArray`s above a certain size. That being said, because every 88 | manipulation of `JsArray` results in a copy, users should always try to keep 89 | these as small as possible. The `n` parameter should always be set to a 90 | reasonably small value. 91 | -} 92 | initializeFromList : Int -> List a -> ( JsArray a, List a ) 93 | initializeFromList = 94 | Native.JsArray.initializeFromList 95 | 96 | 97 | {-| Returns the element at the given index. 98 | 99 | WARNING: This function does not perform bounds checking. 100 | Make sure you know the index is within bounds when using this function. 101 | -} 102 | unsafeGet : Int -> JsArray a -> a 103 | unsafeGet = 104 | Native.JsArray.unsafeGet 105 | 106 | 107 | {-| Sets the element at the given index. 108 | 109 | WARNING: This function does not perform bounds checking. 110 | Make sure you know the index is within bounds when using this function. 111 | -} 112 | unsafeSet : Int -> a -> JsArray a -> JsArray a 113 | unsafeSet = 114 | Native.JsArray.unsafeSet 115 | 116 | 117 | {-| Push an element onto the array. 118 | -} 119 | push : a -> JsArray a -> JsArray a 120 | push = 121 | Native.JsArray.push 122 | 123 | 124 | {-| Reduce the array from the left. 125 | -} 126 | foldl : (a -> b -> b) -> b -> JsArray a -> b 127 | foldl = 128 | Native.JsArray.foldl 129 | 130 | 131 | {-| Reduce the array from the right. 132 | -} 133 | foldr : (a -> b -> b) -> b -> JsArray a -> b 134 | foldr = 135 | Native.JsArray.foldr 136 | 137 | 138 | {-| Apply a function on every element in an array. 139 | -} 140 | map : (a -> b) -> JsArray a -> JsArray b 141 | map = 142 | Native.JsArray.map 143 | 144 | 145 | {-| Apply a function on every element and its index in an array. 146 | An offset allows to modify the index passed to the function. 147 | 148 | indexedMap (,) 5 (repeat 3 3) == Array [(5,3), (6,3), (7,3)] 149 | -} 150 | indexedMap : (Int -> a -> b) -> Int -> JsArray a -> JsArray b 151 | indexedMap = 152 | Native.JsArray.indexedMap 153 | 154 | 155 | {-| Get a sub section of an array: `(slice start end array)`. 156 | The `start` is a zero-based index where we will start our slice. 157 | The `end` is a zero-based index that indicates the end of the slice. 158 | The slice extracts up to, but no including, the `end`. 159 | 160 | Both `start` and `end` can be negative, indicating an offset from the end 161 | of the array. Popping the last element of the array is therefore: 162 | `slice 0 -1 arr`. 163 | 164 | In the case of an impossible slice, the empty array is returned. 165 | -} 166 | slice : Int -> Int -> JsArray a -> JsArray a 167 | slice = 168 | Native.JsArray.slice 169 | 170 | 171 | {-| Appends `n` elements from array `b` onto array `a`: `(appendN n a b)`. 172 | 173 | The `n` parameter is required by the `Array` module, which never wants to 174 | create `JsArray`s above a certain size, even when appending. 175 | -} 176 | appendN : Int -> JsArray a -> JsArray a -> JsArray a 177 | appendN = 178 | Native.JsArray.appendN 179 | -------------------------------------------------------------------------------- /src/Native/JsArray.js: -------------------------------------------------------------------------------- 1 | var _Skinney$elm_array_exploration$Native_JsArray = function() { 2 | 3 | var empty = []; 4 | 5 | function singleton(val) { 6 | return [val]; 7 | } 8 | 9 | function length(arr) { 10 | return arr.length; 11 | } 12 | 13 | function initialize(size, offset, f) { 14 | var result = new Array(size); 15 | 16 | for (var i = 0; i < size; i++) { 17 | result[i] = f(offset + i); 18 | } 19 | 20 | return result; 21 | } 22 | 23 | function initializeFromList(max, ls) { 24 | var result = new Array(max); 25 | 26 | for (var i = 0; i < max && ls.ctor !== '[]'; i++) { 27 | result[i] = ls._0; 28 | ls = ls._1; 29 | } 30 | 31 | result.length = i; 32 | 33 | return { 34 | ctor: '_Tuple2', 35 | _0: result, 36 | _1: ls 37 | }; 38 | } 39 | 40 | function unsafeGet(idx, arr) { 41 | return arr[idx]; 42 | } 43 | 44 | function unsafeSet(idx, val, arr) { 45 | var length = arr.length; 46 | var result = new Array(length); 47 | 48 | for (var i = 0; i < length; i++) { 49 | result[i] = arr[i]; 50 | } 51 | 52 | result[idx] = val; 53 | return result; 54 | } 55 | 56 | function push(val, arr) { 57 | var length = arr.length; 58 | var result = new Array(length + 1); 59 | 60 | for (var i = 0; i < length; i++) { 61 | result[i] = arr[i]; 62 | } 63 | 64 | result[length] = val; 65 | return result; 66 | } 67 | 68 | function foldl(f, acc, arr) { 69 | var length = arr.length; 70 | 71 | for (var i = 0; i < length; i++) { 72 | acc = A2(f, arr[i], acc); 73 | } 74 | 75 | return acc; 76 | } 77 | 78 | function foldr(f, acc, arr) { 79 | for (var i = arr.length - 1; i >= 0; i--) { 80 | acc = A2(f, arr[i], acc); 81 | } 82 | 83 | return acc; 84 | } 85 | 86 | function map(f, arr) { 87 | var length = arr.length; 88 | var result = new Array(length); 89 | 90 | for (var i = 0; i < length; i++) { 91 | result[i] = f(arr[i]); 92 | } 93 | 94 | return result; 95 | } 96 | 97 | function indexedMap(f, offset, arr) { 98 | var length = arr.length; 99 | var result = new Array(length); 100 | 101 | for (var i = 0; i < length; i++) { 102 | result[i] = A2(f, offset + i, arr[i]); 103 | } 104 | 105 | return result; 106 | } 107 | 108 | function slice(from, to, arr) { 109 | return arr.slice(from, to); 110 | } 111 | 112 | function appendN(n, dest, source) { 113 | var destLen = dest.length; 114 | var itemsToCopy = n - destLen; 115 | 116 | if (itemsToCopy > source.length) { 117 | itemsToCopy = source.length; 118 | } 119 | 120 | var size = destLen + itemsToCopy; 121 | var result = new Array(size); 122 | 123 | for (var i = 0; i < destLen; i++) { 124 | result[i] = dest[i]; 125 | } 126 | 127 | for (var i = 0; i < itemsToCopy; i++) { 128 | result[i + destLen] = source[i]; 129 | } 130 | 131 | return result; 132 | } 133 | 134 | return { 135 | empty: empty, 136 | singleton: singleton, 137 | length: length, 138 | initialize: F3(initialize), 139 | initializeFromList: F2(initializeFromList), 140 | unsafeGet: F2(unsafeGet), 141 | unsafeSet: F3(unsafeSet), 142 | push: F2(push), 143 | foldl: F3(foldl), 144 | foldr: F3(foldr), 145 | map: F2(map), 146 | indexedMap: F3(indexedMap), 147 | slice: F3(slice), 148 | appendN: F3(appendN) 149 | }; 150 | 151 | }(); 152 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | /elm-stuff/ 2 | -------------------------------------------------------------------------------- /tests/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Tests 4 | import Test.Runner.Node exposing (runWithOptions, TestProgram) 5 | import Json.Encode exposing (Value) 6 | 7 | 8 | main : TestProgram 9 | main = 10 | (runWithOptions { runs = 500, seed = Nothing }) emit Tests.all 11 | 12 | 13 | port emit : ( String, Value ) -> Cmd msg 14 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (all) 2 | 3 | import Test exposing (Test, describe, test, fuzz, fuzz2) 4 | import Fuzz exposing (Fuzzer, intRange) 5 | import Expect 6 | import Array.Hamt as Array exposing (..) 7 | 8 | 9 | all : Test 10 | all = 11 | describe "Array" 12 | [ initTests 13 | , isEmptyTests 14 | , lengthTests 15 | , getSetTests 16 | , conversionTests 17 | , transformTests 18 | , sliceTests 19 | , runtimeCrashTests 20 | ] 21 | 22 | 23 | {-| 24 | 25 | > 33000 elements requires 3 levels in the tree 26 | -} 27 | defaultSizeRange : Fuzzer Int 28 | defaultSizeRange = 29 | (intRange 1 33000) 30 | 31 | 32 | initTests : Test 33 | initTests = 34 | describe "Initialization" 35 | [ fuzz defaultSizeRange "initialize" <| 36 | \size -> 37 | toList (initialize size identity) 38 | |> Expect.equal (List.range 0 (size - 1)) 39 | , fuzz defaultSizeRange "push" <| 40 | \size -> 41 | List.foldl push empty (List.range 0 (size - 1)) 42 | |> Expect.equal (initialize size identity) 43 | , test "initialize non-identity" <| 44 | \() -> 45 | toList (initialize 4 (\n -> n * n)) 46 | |> Expect.equal [ 0, 1, 4, 9 ] 47 | , test "initialize empty" <| 48 | \() -> 49 | toList (initialize 0 identity) 50 | |> Expect.equal [] 51 | , test "initialize negative" <| 52 | \() -> 53 | toList (initialize -2 identity) 54 | |> Expect.equal [] 55 | ] 56 | 57 | 58 | isEmptyTests : Test 59 | isEmptyTests = 60 | describe "isEmpty" 61 | [ test "all empty arrays are equal" <| 62 | \() -> 63 | Expect.equal empty (fromList []) 64 | , test "empty array" <| 65 | \() -> 66 | isEmpty empty 67 | |> Expect.equal True 68 | , test "empty converted array" <| 69 | \() -> 70 | isEmpty (fromList []) 71 | |> Expect.equal True 72 | , test "non-empty array" <| 73 | \() -> 74 | isEmpty (fromList [ 1 ]) 75 | |> Expect.equal False 76 | ] 77 | 78 | 79 | lengthTests : Test 80 | lengthTests = 81 | describe "Length" 82 | [ test "empty array" <| 83 | \() -> 84 | length empty 85 | |> Expect.equal 0 86 | , fuzz defaultSizeRange "non-empty array" <| 87 | \size -> 88 | length (initialize size identity) 89 | |> Expect.equal size 90 | , fuzz defaultSizeRange "push" <| 91 | \size -> 92 | length (push size (initialize size identity)) 93 | |> Expect.equal (size + 1) 94 | , fuzz defaultSizeRange "append" <| 95 | \size -> 96 | length (append (initialize size identity) (initialize (size // 2) identity)) 97 | |> Expect.equal (size + (size // 2)) 98 | , fuzz defaultSizeRange "set does not increase" <| 99 | \size -> 100 | length (set (size // 2) 1 (initialize size identity)) 101 | |> Expect.equal size 102 | , fuzz (intRange 100 10000) "big slice" <| 103 | \size -> 104 | length (slice 35 -35 (initialize size identity)) 105 | |> Expect.equal (size - 70) 106 | , fuzz2 (intRange -32 -1) (intRange 100 10000) "small slice end" <| 107 | \n size -> 108 | length (slice 0 n (initialize size identity)) 109 | |> Expect.equal (size + n) 110 | ] 111 | 112 | 113 | getSetTests : Test 114 | getSetTests = 115 | describe "Get and set" 116 | [ fuzz2 defaultSizeRange defaultSizeRange "can retrieve element" <| 117 | \x y -> 118 | let 119 | n = 120 | min x y 121 | 122 | size = 123 | max x y 124 | in 125 | get n (initialize (size + 1) identity) 126 | |> Expect.equal (Just n) 127 | , fuzz2 (intRange 1 50) (intRange 100 33000) "out of bounds retrieval returns nothing" <| 128 | \n size -> 129 | let 130 | arr = 131 | initialize size identity 132 | in 133 | ( get (negate n) arr 134 | , get (size + n) arr 135 | ) 136 | |> Expect.equal ( Nothing, Nothing ) 137 | , fuzz2 defaultSizeRange defaultSizeRange "set replaces value" <| 138 | \x y -> 139 | let 140 | n = 141 | min x y 142 | 143 | size = 144 | max x y 145 | in 146 | get n (set n 5 (initialize (size + 1) identity)) 147 | |> Expect.equal (Just 5) 148 | , fuzz2 (intRange 1 50) defaultSizeRange "set out of bounds returns original array" <| 149 | \n size -> 150 | let 151 | arr = 152 | initialize size identity 153 | in 154 | set (negate n) 5 arr 155 | |> set (size + n) 5 156 | |> Expect.equal arr 157 | , test "Retrieval works from tail" <| 158 | \() -> 159 | get 1030 (set 1030 5 (initialize 1035 identity)) 160 | |> Expect.equal (Just 5) 161 | ] 162 | 163 | 164 | conversionTests : Test 165 | conversionTests = 166 | describe "Conversion" 167 | [ fuzz defaultSizeRange "back and forth" <| 168 | \size -> 169 | let 170 | ls = 171 | List.range 0 (size - 1) 172 | in 173 | toList (fromList ls) 174 | |> Expect.equal ls 175 | , fuzz defaultSizeRange "indexed" <| 176 | \size -> 177 | toIndexedList (initialize size ((+) 1)) 178 | |> Expect.equal (toList (initialize size (\idx -> ( idx, idx + 1 )))) 179 | ] 180 | 181 | 182 | transformTests : Test 183 | transformTests = 184 | describe "Transform" 185 | [ fuzz defaultSizeRange "foldl" <| 186 | \size -> 187 | foldl (::) [] (initialize size identity) 188 | |> Expect.equal (List.reverse (List.range 0 (size - 1))) 189 | , fuzz defaultSizeRange "foldr" <| 190 | \size -> 191 | foldr (\n acc -> n :: acc) [] (initialize size identity) 192 | |> Expect.equal (List.range 0 (size - 1)) 193 | , fuzz defaultSizeRange "filter" <| 194 | \size -> 195 | toList (filter (\a -> a % 2 == 0) (initialize size identity)) 196 | |> Expect.equal (List.filter (\a -> a % 2 == 0) (List.range 0 (size - 1))) 197 | , fuzz defaultSizeRange "map" <| 198 | \size -> 199 | map ((+) 1) (initialize size identity) 200 | |> Expect.equal (initialize size ((+) 1)) 201 | , fuzz defaultSizeRange "indexedMap" <| 202 | \size -> 203 | indexedMap (*) (repeat size 5) 204 | |> Expect.equal (initialize size ((*) 5)) 205 | , fuzz defaultSizeRange "push appends one element" <| 206 | \size -> 207 | push size (initialize size identity) 208 | |> Expect.equal (initialize (size + 1) identity) 209 | , fuzz (intRange 1 1050) "append" <| 210 | \size -> 211 | append (initialize size identity) (initialize size ((+) size)) 212 | |> Expect.equal (initialize (size * 2) identity) 213 | , fuzz2 defaultSizeRange (intRange 1 32) "small appends" <| 214 | \s1 s2 -> 215 | append (initialize s1 identity) (initialize s2 ((+) s1)) 216 | |> Expect.equal (initialize (s1 + s2) identity) 217 | , fuzz (defaultSizeRange) "toString" <| 218 | \size -> 219 | let 220 | ls = 221 | List.range 0 size 222 | in 223 | Array.toString (fromList ls) 224 | |> Expect.equal ("Array " ++ Basics.toString ls) 225 | ] 226 | 227 | 228 | sliceTests : Test 229 | sliceTests = 230 | let 231 | smallSample = 232 | fromList (List.range 1 8) 233 | in 234 | describe "Slice" 235 | [ fuzz2 (intRange -50 -1) (intRange 100 33000) "both" <| 236 | \n size -> 237 | slice (abs n) n (initialize size identity) 238 | |> Expect.equal (initialize (size + n + n) (\idx -> idx - n)) 239 | , fuzz2 (intRange -50 -1) (intRange 100 33000) "left" <| 240 | \n size -> 241 | let 242 | arr = 243 | initialize size identity 244 | in 245 | slice (abs n) (length arr) arr 246 | |> Expect.equal (initialize (size + n) (\idx -> idx - n)) 247 | , fuzz2 (intRange -50 -1) (intRange 100 33000) "right" <| 248 | \n size -> 249 | slice 0 n (initialize size identity) 250 | |> Expect.equal (initialize (size + n) identity) 251 | , fuzz defaultSizeRange "slicing all but the last item" <| 252 | \size -> 253 | initialize size identity 254 | |> slice -1 size 255 | |> toList 256 | |> Expect.equal [ size - 1 ] 257 | , test "both small" <| 258 | \() -> 259 | toList (slice 2 5 smallSample) 260 | |> Expect.equal (List.range 3 5) 261 | , test "start small" <| 262 | \() -> 263 | toList (slice 2 (length smallSample) smallSample) 264 | |> Expect.equal (List.range 3 8) 265 | , test "negative" <| 266 | \() -> 267 | toList (slice -5 -2 smallSample) 268 | |> Expect.equal (List.range 4 6) 269 | , test "impossible" <| 270 | \() -> 271 | toList (slice -1 -2 smallSample) 272 | |> Expect.equal [] 273 | , test "crash" <| 274 | \() -> 275 | Array.repeat (33 * 32) 1 276 | |> Array.slice 0 1 277 | |> Expect.equal (Array.repeat 1 1) 278 | ] 279 | 280 | 281 | runtimeCrashTests : Test 282 | runtimeCrashTests = 283 | describe "Runtime crashes in core" 284 | [ test "magic slice" <| 285 | \() -> 286 | let 287 | n = 288 | 10 289 | in 290 | initialize (4 * n) identity 291 | |> slice n (4 * n) 292 | |> slice n (3 * n) 293 | |> slice n (2 * n) 294 | |> slice n n 295 | |> \a -> Expect.equal a a 296 | , test "magic slice 2" <| 297 | \() -> 298 | let 299 | ary = 300 | fromList <| List.range 0 32 301 | 302 | res = 303 | append (slice 1 32 ary) (slice (32 + 1) -1 ary) 304 | in 305 | Expect.equal res res 306 | , test "magic append" <| 307 | \() -> 308 | let 309 | res = 310 | append (initialize 1 (always 1)) 311 | (initialize (32 ^ 2 - 1 * 32 + 1) (\i -> i)) 312 | in 313 | Expect.equal res res 314 | ] 315 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Sample Elm Test", 4 | "repository": "https://github.com/Skinney/elm-array-exploration.git", 5 | "license": "BSD-3-Clause", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "native-modules": true, 11 | "exposed-modules": [], 12 | "dependencies": { 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-community/elm-test": "3.0.0 <= v < 4.0.0", 15 | "rtfeldman/node-test-runner": "3.0.0 <= v < 4.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } 19 | --------------------------------------------------------------------------------