Skip to content

Commit

Permalink
feat(stdlib): Add List.filterMap, List.filterMapi, List.findMap (
Browse files Browse the repository at this point in the history
…#2201)

Co-authored-by: Oscar Spencer <[email protected]>
  • Loading branch information
spotandjake and ospencer authored Jan 2, 2025
1 parent 4919ba3 commit 551f5ad
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 8 deletions.
20 changes: 20 additions & 0 deletions compiler/test/stdlib/list.test.gr
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,19 @@ assert filteri((x, i) => i + x > 2, list) == [2, 3]
assert filteri((x, i) => x == 3, list) == [3]
assert filteri((x, i) => x == 3, []) == []

// List.filterMap

assert List.filterMap(x => Some(x), list) == list
assert List.filterMap(x => if (x == 3) Some(4) else None, list) == [4]
assert List.filterMap(x => None, list) == []

// List.filterMapi
assert List.filterMapi((x, i) => Some(x), list) == list
assert List.filterMapi((x, i) => if (x == 3) Some(4) else None, list) == [4]
assert List.filterMapi((x, i) => None, list) == []
assert List.filterMapi((x, i) => if (i != 0) Some(i) else None, list) == [1, 2]
assert List.filterMapi((x, i) => if (i != 0) Some(x) else None, list) == [2, 3]

// List.reject

assert reject(x => x > 0, list) == []
Expand Down Expand Up @@ -207,6 +220,13 @@ assert findIndex(x => x == 1, list) == Some(0)
assert findIndex(x => x == 2, list) == Some(1)
assert findIndex(x => false, list) == None

// List.findMap
let duplicateList = [(1, 'a'), (2, 'b'), (1, 'c')]
assert List.findMap(((k, v)) => if (k == 1) Some(v) else None, duplicateList) ==
Some('a')
assert List.findMap(x => if (x == 2) Some(x) else None, list) == Some(2)
assert List.findMap(x => None, list) == None

// List.product

let listA = [1, 2]
Expand Down
83 changes: 79 additions & 4 deletions stdlib/list.gr
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,56 @@ provide let mapi = (fn, list) => {
iter(fn, list, 0)
}

/**
* Produces a new list initialized with the results of a mapper function
* called on each element of the input list.
* The mapper function can return `None` to exclude the element from the new list.
*
* @param fn: The mapper function to call on each element, where the value returned will be used to initialize the element in the new list
* @param list: The list to iterate
* @returns The new list with filtered mapped values
*
* @example List.filterMap(x => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
*
* @since v0.7.0
*/
provide let rec filterMap = (fn, list) => {
match (list) {
[] => [],
[first, ...rest] => match (fn(first)) {
Some(v) => [v, ...filterMap(fn, rest)],
None => filterMap(fn, rest),
},
}
}

/**
* Produces a new list initialized with the results of a mapper function
* called on each element of the input list and its index.
* The mapper function can return `None` to exclude the element from the new list.
*
* @param fn: The mapper function to call on each element, where the value returned will be used to initialize the element in the new list
* @param list: The list to iterate
* @returns The new list with filtered mapped values
*
* @example List.filterMapi((x, i) => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
* @example List.filterMapi((x, i) => if (i == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["1"]
*
* @since v0.7.0
*/
provide let filterMapi = (fn, list) => {
let rec iter = (fn, list, index) => {
match (list) {
[] => [],
[first, ...rest] => match (fn(first, index)) {
Some(v) => [v, ...iter(fn, rest, index + 1)],
None => iter(fn, rest, index + 1),
},
}
}
iter(fn, list, 0)
}

/**
* Produces a new list by calling a function on each element
* of the input list. Each iteration produces an intermediate
Expand All @@ -242,7 +292,7 @@ provide let rec flatMap = (fn, list) => {
*
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
* @param list: The list to check
* @returns `true` if all elements satify the condition or `false` otherwise
* @returns `true` if all elements satisfy the condition or `false` otherwise
*
* @since v0.1.0
*/
Expand All @@ -260,7 +310,7 @@ provide let rec every = (fn, list) => {
*
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
* @param list: The list to iterate
* @returns `true` if one or more elements satify the condition or `false` otherwise
* @returns `true` if one or more elements satisfy the condition or `false` otherwise
*
* @since v0.1.0
*/
Expand Down Expand Up @@ -769,7 +819,7 @@ provide let rec takeWhile = (fn, list) => {
}

/**
* Finds the first element in a list that satifies the given condition.
* Finds the first element in a list that satisfies the given condition.
*
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
* @param list: The list to search
Expand All @@ -787,7 +837,7 @@ provide let rec find = (fn, list) => {
}

/**
* Finds the first index in a list where the element satifies the given condition.
* Finds the first index in a list where the element satisfies the given condition.
*
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
* @param list: The list to search
Expand All @@ -808,6 +858,31 @@ provide let findIndex = (fn, list) => {
findItemIndex(list, 0)
}

/**
* Finds the first element in a list that satisfies the given condition and
* returns the result of applying a mapper function to it.
*
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
* @param list: The list to search
* @returns `Some(mapped)` containing the first value found with the given mapping or `None` otherwise
*
* @example
* let jsonObject = [(1, 'a'), (2, 'b'), (1, 'c')]
* let getItem = (key, obj) => List.findMap(((k, v)) => if (k == key) Some(v) else None, obj)
* assert getItem(1, jsonObject) == Some('a')
*
* @since v0.7.0
*/
provide let rec findMap = (fn, list) => {
match (list) {
[] => None,
[first, ...rest] => match (fn(first)) {
None => findMap(fn, rest),
Some(v) => Some(v),
},
}
}

/**
* Combines two lists into a Cartesian product of tuples containing
* all ordered pairs `(a, b)`.
Expand Down
115 changes: 111 additions & 4 deletions stdlib/list.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,78 @@ Returns:
|----|-----------|
|`List<b>`|The new list with mapped values|

### List.**filterMap**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
filterMap : (fn: (a => Option<b>), list: List<a>) => List<b>
```

Produces a new list initialized with the results of a mapper function
called on each element of the input list.
The mapper function can return `None` to exclude the element from the new list.

Parameters:

|param|type|description|
|-----|----|-----------|
|`fn`|`a => Option<b>`|The mapper function to call on each element, where the value returned will be used to initialize the element in the new list|
|`list`|`List<a>`|The list to iterate|

Returns:

|type|description|
|----|-----------|
|`List<b>`|The new list with filtered mapped values|

Examples:

```grain
List.filterMap(x => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
```

### List.**filterMapi**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
filterMapi : (fn: ((a, Number) => Option<b>), list: List<a>) => List<b>
```

Produces a new list initialized with the results of a mapper function
called on each element of the input list and its index.
The mapper function can return `None` to exclude the element from the new list.

Parameters:

|param|type|description|
|-----|----|-----------|
|`fn`|`(a, Number) => Option<b>`|The mapper function to call on each element, where the value returned will be used to initialize the element in the new list|
|`list`|`List<a>`|The list to iterate|

Returns:

|type|description|
|----|-----------|
|`List<b>`|The new list with filtered mapped values|

Examples:

```grain
List.filterMapi((x, i) => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
```

```grain
List.filterMapi((x, i) => if (i == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["1"]
```

### List.**flatMap**

<details disabled>
Expand Down Expand Up @@ -397,7 +469,7 @@ Returns:

|type|description|
|----|-----------|
|`Bool`|`true` if all elements satify the condition or `false` otherwise|
|`Bool`|`true` if all elements satisfy the condition or `false` otherwise|

### List.**some**

Expand All @@ -424,7 +496,7 @@ Returns:

|type|description|
|----|-----------|
|`Bool`|`true` if one or more elements satify the condition or `false` otherwise|
|`Bool`|`true` if one or more elements satisfy the condition or `false` otherwise|

### List.**forEach**

Expand Down Expand Up @@ -1124,7 +1196,7 @@ Returns:
find : (fn: (a => Bool), list: List<a>) => Option<a>
```

Finds the first element in a list that satifies the given condition.
Finds the first element in a list that satisfies the given condition.

Parameters:

Expand Down Expand Up @@ -1158,7 +1230,7 @@ Returns:
findIndex : (fn: (a => Bool), list: List<a>) => Option<Number>
```

Finds the first index in a list where the element satifies the given condition.
Finds the first index in a list where the element satisfies the given condition.

Parameters:

Expand All @@ -1173,6 +1245,41 @@ Returns:
|----|-----------|
|`Option<Number>`|`Some(index)` containing the index of the first element found or `None` otherwise|

### List.**findMap**

<details disabled>
<summary tabindex="-1">Added in <code>next</code></summary>
No other changes yet.
</details>

```grain
findMap : (fn: (a => Option<b>), list: List<a>) => Option<b>
```

Finds the first element in a list that satisfies the given condition and
returns the result of applying a mapper function to it.

Parameters:

|param|type|description|
|-----|----|-----------|
|`fn`|`a => Option<b>`|The function to call on each element, where the returned value indicates if the element satisfies the condition|
|`list`|`List<a>`|The list to search|

Returns:

|type|description|
|----|-----------|
|`Option<b>`|`Some(mapped)` containing the first value found with the given mapping or `None` otherwise|

Examples:

```grain
let jsonObject = [(1, 'a'), (2, 'b'), (1, 'c')]
let getItem = (key, obj) => List.findMap(((k, v)) => if (k == key) Some(v) else None, obj)
assert getItem(1, jsonObject) == Some('a')
```

### List.**product**

<details disabled>
Expand Down

0 comments on commit 551f5ad

Please sign in to comment.