Skip to content

Commit

Permalink
Add some missing Dict and Set functions
Browse files Browse the repository at this point in the history
Also remove some unnecessary Hash and Eq restrictions
  • Loading branch information
rtfeldman committed Jun 27, 2023
1 parent 82a2f3e commit ed9d9b1
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 5 deletions.
43 changes: 39 additions & 4 deletions crates/compiler/builtins/roc/Dict.roc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface Dict
clear,
capacity,
len,
isEmpty,
get,
contains,
insert,
Expand All @@ -21,6 +22,8 @@ interface Dict
insertAll,
keepShared,
removeAll,
map,
joinMap,
]
imports [
Bool.{ Bool, Eq },
Expand Down Expand Up @@ -139,12 +142,12 @@ empty = \{} ->
## Returns the max number of elements the dictionary can hold before requiring a rehash.
## ```
## foodDict =
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
## Dict.empty {}
## |> Dict.insert "apple" "fruit"
##
## capacityOfDict = Dict.capacity foodDict
## ```
capacity : Dict k v -> Nat | k has Hash & Eq
capacity : Dict * * -> Nat
capacity = \@Dict { dataIndices } ->
cap = List.len dataIndices

Expand Down Expand Up @@ -192,10 +195,20 @@ fromList = \data ->
## |> Dict.len
## |> Bool.isEq 3
## ```
len : Dict k v -> Nat | k has Hash & Eq
len : Dict * * -> Nat
len = \@Dict { size } ->
size

## Check if the dictinoary is empty.
## ```
## Dict.isEmpty (Dict.empty {} |> Dict.insert "key" 42)
##
## Dict.isEmpty (Dict.empty {})
## ```
isEmpty : Dict * * -> Bool
isEmpty = \@Dict { size } ->
size == 0

## Clears all elements from a dictionary keeping around the allocation if it isn't huge.
## ```
## songs =
Expand Down Expand Up @@ -225,6 +238,28 @@ clear = \@Dict { metadata, dataIndices, data } ->
size: 0,
}

## Convert each value in the dictionary to something new, by calling a conversion
## function on each of them which receives both the key and the old value. Then return a
## new dictionary containing the same keys and the converted values.
map : Dict k a, (k, a -> b) -> Dict k b | k has Hash & Eq, b has Hash & Eq
map = \dict, transform ->
init = withCapacity (capacity dict)

walk dict init \answer, k, v ->
insert answer k (transform k v)

## Like [Dict.map], except the transformation function wraps the return value
## in a dictionary. At the end, all the dictionaries get joined together
## (using [Dict.insertAll]) into one dictionary.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Dict a b, (a, b -> Dict x y) -> Dict x y | a has Hash & Eq, x has Hash & Eq
joinMap = \dict, transform ->
init = withCapacity (capacity dict) # Might be a pessimization

walk dict init \answer, k, v ->
insertAll answer (transform k v)

## Iterate through the keys and values in the dictionary and call the provided
## function with signature `state, k, v -> state` for each value, with an
## initial `state` value provided for the first call.
Expand Down
3 changes: 3 additions & 0 deletions crates/compiler/builtins/roc/List.roc
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ interface List
## * Even when copying is faster, other list operations may still be slightly slower with persistent data structures. For example, even if it were a persistent data structure, [List.map], [List.walk], and [List.keepIf] would all need to traverse every element in the list and build up the result from scratch. These operations are all
## * Roc's compiler optimizes many list operations into in-place mutations behind the scenes, depending on how the list is being used. For example, [List.map], [List.keepIf], and [List.set] can all be optimized to perform in-place mutations.
## * If possible, it is usually best for performance to use large lists in a way where the optimizer can turn them into in-place mutations. If this is not possible, a persistent data structure might be faster - but this is a rare enough scenario that it would not be good for the average Roc program's performance if this were the way [List] worked by default. Instead, you can look outside Roc's standard modules for an implementation of a persistent data structure - likely built using [List] under the hood!

# separator so List.isEmpty doesn't absorb the above into its doc comment

## Check if the list is empty.
## ```
## List.isEmpty [1, 2, 3]
Expand Down
57 changes: 56 additions & 1 deletion crates/compiler/builtins/roc/Set.roc
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@ interface Set
walkUntil,
insert,
len,
isEmpty,
capacity,
remove,
contains,
toList,
fromList,
union,
intersection,
difference,
map,
joinMap,
]
imports [
List,
Expand Down Expand Up @@ -59,6 +63,13 @@ hashSet = \hasher, @Set inner -> Hash.hash hasher inner
empty : {} -> Set k | k has Hash & Eq
empty = \{} -> @Set (Dict.empty {})

## Return a dictionary with space allocated for a number of entries. This
## may provide a performance optimization if you know how many entries will be
## inserted.
withCapacity : Nat -> Set k | k has Hash & Eq
withCapacity = \cap ->
@Set (Dict.withCapacity cap)

## Creates a new `Set` with a single value.
## ```
## singleItemSet = Set.single "Apple"
Expand Down Expand Up @@ -115,10 +126,32 @@ expect
##
## expect countValues == 3
## ```
len : Set k -> Nat | k has Hash & Eq
len : Set * -> Nat
len = \@Set dict ->
Dict.len dict

## Returns the max number of elements the set can hold before requiring a rehash.
## ```
## foodSet =
## Set.empty {}
## |> Set.insert "apple"
##
## capacityOfSet = Set.capacity foodSet
## ```
capacity : Set * -> Nat
capacity = \@Set dict ->
Dict.capacity dict

## Check if the set is empty.
## ```
## Set.isEmpty (Set.empty {} |> Set.insert 42)
##
## Set.isEmpty (Set.empty {})
## ```
isEmpty : Set * -> Bool
isEmpty = \@Set dict ->
Dict.isEmpty dict

# Inserting a duplicate key has no effect on length.
expect
actual =
Expand Down Expand Up @@ -261,6 +294,28 @@ walk : Set k, state, (state, k -> state) -> state | k has Hash & Eq
walk = \@Set dict, state, step ->
Dict.walk dict state (\s, k, _ -> step s k)

## Convert each value in the set to something new, by calling a conversion
## function on each of them which receives the old value. Then return a
## new set containing the converted values.
map : Set a, (a -> b) -> Set b | a has Hash & Eq, b has Hash & Eq
map = \set, transform ->
init = withCapacity (capacity set)

walk set init \answer, k ->
insert answer (transform k)

## Like [Set.map], except the transformation function wraps the return value
## in a set. At the end, all the sets get joined together
## (using [Set.union]) into one set.
##
## You may know a similar function named `concatMap` in other languages.
joinMap : Set a, (a -> Set b) -> Set b | a has Hash & Eq, b has Hash & Eq
joinMap = \set, transform ->
init = withCapacity (capacity set) # Might be a pessimization

walk set init \answer, k ->
union answer (transform k)

## Iterate through the values of a given `Set` and build a value, can stop
## iterating part way through the collection.
## ```
Expand Down
6 changes: 6 additions & 0 deletions crates/compiler/module/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1471,6 +1471,9 @@ define_builtins! {

22 DICT_LIST_GET_UNSAFE: "listGetUnsafe"
23 DICT_PSEUDO_SEED: "pseudoSeed"
24 DICT_IS_EMPTY: "isEmpty"
25 DICT_MAP: "map"
26 DICT_JOINMAP: "joinMap"
}
9 SET: "Set" => {
0 SET_SET: "Set" exposed_type=true // the Set.Set type alias
Expand All @@ -1490,6 +1493,9 @@ define_builtins! {
14 SET_CONTAINS: "contains"
15 SET_TO_DICT: "toDict"
16 SET_CAPACITY: "capacity"
17 SET_IS_EMPTY: "isEmpty"
18 SET_MAP: "map"
19 SET_JOIN_MAP: "joinMap"
}
10 BOX: "Box" => {
0 BOX_BOX_TYPE: "Box" exposed_apply_type=true // the Box.Box opaque type
Expand Down

0 comments on commit ed9d9b1

Please sign in to comment.