Skip to content

Commit 551f5ad

Browse files
feat(stdlib): Add List.filterMap, List.filterMapi, List.findMap (#2201)
Co-authored-by: Oscar Spencer <oscar.spen@gmail.com>
1 parent 4919ba3 commit 551f5ad

File tree

3 files changed

+210
-8
lines changed

3 files changed

+210
-8
lines changed

compiler/test/stdlib/list.test.gr

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,19 @@ assert filteri((x, i) => i + x > 2, list) == [2, 3]
8888
assert filteri((x, i) => x == 3, list) == [3]
8989
assert filteri((x, i) => x == 3, []) == []
9090

91+
// List.filterMap
92+
93+
assert List.filterMap(x => Some(x), list) == list
94+
assert List.filterMap(x => if (x == 3) Some(4) else None, list) == [4]
95+
assert List.filterMap(x => None, list) == []
96+
97+
// List.filterMapi
98+
assert List.filterMapi((x, i) => Some(x), list) == list
99+
assert List.filterMapi((x, i) => if (x == 3) Some(4) else None, list) == [4]
100+
assert List.filterMapi((x, i) => None, list) == []
101+
assert List.filterMapi((x, i) => if (i != 0) Some(i) else None, list) == [1, 2]
102+
assert List.filterMapi((x, i) => if (i != 0) Some(x) else None, list) == [2, 3]
103+
91104
// List.reject
92105

93106
assert reject(x => x > 0, list) == []
@@ -207,6 +220,13 @@ assert findIndex(x => x == 1, list) == Some(0)
207220
assert findIndex(x => x == 2, list) == Some(1)
208221
assert findIndex(x => false, list) == None
209222

223+
// List.findMap
224+
let duplicateList = [(1, 'a'), (2, 'b'), (1, 'c')]
225+
assert List.findMap(((k, v)) => if (k == 1) Some(v) else None, duplicateList) ==
226+
Some('a')
227+
assert List.findMap(x => if (x == 2) Some(x) else None, list) == Some(2)
228+
assert List.findMap(x => None, list) == None
229+
210230
// List.product
211231

212232
let listA = [1, 2]

stdlib/list.gr

Lines changed: 79 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,56 @@ provide let mapi = (fn, list) => {
217217
iter(fn, list, 0)
218218
}
219219

220+
/**
221+
* Produces a new list initialized with the results of a mapper function
222+
* called on each element of the input list.
223+
* The mapper function can return `None` to exclude the element from the new list.
224+
*
225+
* @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
226+
* @param list: The list to iterate
227+
* @returns The new list with filtered mapped values
228+
*
229+
* @example List.filterMap(x => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
230+
*
231+
* @since v0.7.0
232+
*/
233+
provide let rec filterMap = (fn, list) => {
234+
match (list) {
235+
[] => [],
236+
[first, ...rest] => match (fn(first)) {
237+
Some(v) => [v, ...filterMap(fn, rest)],
238+
None => filterMap(fn, rest),
239+
},
240+
}
241+
}
242+
243+
/**
244+
* Produces a new list initialized with the results of a mapper function
245+
* called on each element of the input list and its index.
246+
* The mapper function can return `None` to exclude the element from the new list.
247+
*
248+
* @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
249+
* @param list: The list to iterate
250+
* @returns The new list with filtered mapped values
251+
*
252+
* @example List.filterMapi((x, i) => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
253+
* @example List.filterMapi((x, i) => if (i == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["1"]
254+
*
255+
* @since v0.7.0
256+
*/
257+
provide let filterMapi = (fn, list) => {
258+
let rec iter = (fn, list, index) => {
259+
match (list) {
260+
[] => [],
261+
[first, ...rest] => match (fn(first, index)) {
262+
Some(v) => [v, ...iter(fn, rest, index + 1)],
263+
None => iter(fn, rest, index + 1),
264+
},
265+
}
266+
}
267+
iter(fn, list, 0)
268+
}
269+
220270
/**
221271
* Produces a new list by calling a function on each element
222272
* of the input list. Each iteration produces an intermediate
@@ -242,7 +292,7 @@ provide let rec flatMap = (fn, list) => {
242292
*
243293
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
244294
* @param list: The list to check
245-
* @returns `true` if all elements satify the condition or `false` otherwise
295+
* @returns `true` if all elements satisfy the condition or `false` otherwise
246296
*
247297
* @since v0.1.0
248298
*/
@@ -260,7 +310,7 @@ provide let rec every = (fn, list) => {
260310
*
261311
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
262312
* @param list: The list to iterate
263-
* @returns `true` if one or more elements satify the condition or `false` otherwise
313+
* @returns `true` if one or more elements satisfy the condition or `false` otherwise
264314
*
265315
* @since v0.1.0
266316
*/
@@ -769,7 +819,7 @@ provide let rec takeWhile = (fn, list) => {
769819
}
770820

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

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

861+
/**
862+
* Finds the first element in a list that satisfies the given condition and
863+
* returns the result of applying a mapper function to it.
864+
*
865+
* @param fn: The function to call on each element, where the returned value indicates if the element satisfies the condition
866+
* @param list: The list to search
867+
* @returns `Some(mapped)` containing the first value found with the given mapping or `None` otherwise
868+
*
869+
* @example
870+
* let jsonObject = [(1, 'a'), (2, 'b'), (1, 'c')]
871+
* let getItem = (key, obj) => List.findMap(((k, v)) => if (k == key) Some(v) else None, obj)
872+
* assert getItem(1, jsonObject) == Some('a')
873+
*
874+
* @since v0.7.0
875+
*/
876+
provide let rec findMap = (fn, list) => {
877+
match (list) {
878+
[] => None,
879+
[first, ...rest] => match (fn(first)) {
880+
None => findMap(fn, rest),
881+
Some(v) => Some(v),
882+
},
883+
}
884+
}
885+
811886
/**
812887
* Combines two lists into a Cartesian product of tuples containing
813888
* all ordered pairs `(a, b)`.

stdlib/list.md

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,78 @@ Returns:
343343
|----|-----------|
344344
|`List<b>`|The new list with mapped values|
345345

346+
### List.**filterMap**
347+
348+
<details disabled>
349+
<summary tabindex="-1">Added in <code>next</code></summary>
350+
No other changes yet.
351+
</details>
352+
353+
```grain
354+
filterMap : (fn: (a => Option<b>), list: List<a>) => List<b>
355+
```
356+
357+
Produces a new list initialized with the results of a mapper function
358+
called on each element of the input list.
359+
The mapper function can return `None` to exclude the element from the new list.
360+
361+
Parameters:
362+
363+
|param|type|description|
364+
|-----|----|-----------|
365+
|`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|
366+
|`list`|`List<a>`|The list to iterate|
367+
368+
Returns:
369+
370+
|type|description|
371+
|----|-----------|
372+
|`List<b>`|The new list with filtered mapped values|
373+
374+
Examples:
375+
376+
```grain
377+
List.filterMap(x => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
378+
```
379+
380+
### List.**filterMapi**
381+
382+
<details disabled>
383+
<summary tabindex="-1">Added in <code>next</code></summary>
384+
No other changes yet.
385+
</details>
386+
387+
```grain
388+
filterMapi : (fn: ((a, Number) => Option<b>), list: List<a>) => List<b>
389+
```
390+
391+
Produces a new list initialized with the results of a mapper function
392+
called on each element of the input list and its index.
393+
The mapper function can return `None` to exclude the element from the new list.
394+
395+
Parameters:
396+
397+
|param|type|description|
398+
|-----|----|-----------|
399+
|`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|
400+
|`list`|`List<a>`|The list to iterate|
401+
402+
Returns:
403+
404+
|type|description|
405+
|----|-----------|
406+
|`List<b>`|The new list with filtered mapped values|
407+
408+
Examples:
409+
410+
```grain
411+
List.filterMapi((x, i) => if (x % 2 == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["2", "4"]
412+
```
413+
414+
```grain
415+
List.filterMapi((x, i) => if (i == 0) Some(toString(x)) else None, [1, 2, 3, 4]) == ["1"]
416+
```
417+
346418
### List.**flatMap**
347419

348420
<details disabled>
@@ -397,7 +469,7 @@ Returns:
397469

398470
|type|description|
399471
|----|-----------|
400-
|`Bool`|`true` if all elements satify the condition or `false` otherwise|
472+
|`Bool`|`true` if all elements satisfy the condition or `false` otherwise|
401473

402474
### List.**some**
403475

@@ -424,7 +496,7 @@ Returns:
424496

425497
|type|description|
426498
|----|-----------|
427-
|`Bool`|`true` if one or more elements satify the condition or `false` otherwise|
499+
|`Bool`|`true` if one or more elements satisfy the condition or `false` otherwise|
428500

429501
### List.**forEach**
430502

@@ -1124,7 +1196,7 @@ Returns:
11241196
find : (fn: (a => Bool), list: List<a>) => Option<a>
11251197
```
11261198

1127-
Finds the first element in a list that satifies the given condition.
1199+
Finds the first element in a list that satisfies the given condition.
11281200

11291201
Parameters:
11301202

@@ -1158,7 +1230,7 @@ Returns:
11581230
findIndex : (fn: (a => Bool), list: List<a>) => Option<Number>
11591231
```
11601232

1161-
Finds the first index in a list where the element satifies the given condition.
1233+
Finds the first index in a list where the element satisfies the given condition.
11621234

11631235
Parameters:
11641236

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

1248+
### List.**findMap**
1249+
1250+
<details disabled>
1251+
<summary tabindex="-1">Added in <code>next</code></summary>
1252+
No other changes yet.
1253+
</details>
1254+
1255+
```grain
1256+
findMap : (fn: (a => Option<b>), list: List<a>) => Option<b>
1257+
```
1258+
1259+
Finds the first element in a list that satisfies the given condition and
1260+
returns the result of applying a mapper function to it.
1261+
1262+
Parameters:
1263+
1264+
|param|type|description|
1265+
|-----|----|-----------|
1266+
|`fn`|`a => Option<b>`|The function to call on each element, where the returned value indicates if the element satisfies the condition|
1267+
|`list`|`List<a>`|The list to search|
1268+
1269+
Returns:
1270+
1271+
|type|description|
1272+
|----|-----------|
1273+
|`Option<b>`|`Some(mapped)` containing the first value found with the given mapping or `None` otherwise|
1274+
1275+
Examples:
1276+
1277+
```grain
1278+
let jsonObject = [(1, 'a'), (2, 'b'), (1, 'c')]
1279+
let getItem = (key, obj) => List.findMap(((k, v)) => if (k == key) Some(v) else None, obj)
1280+
assert getItem(1, jsonObject) == Some('a')
1281+
```
1282+
11761283
### List.**product**
11771284

11781285
<details disabled>

0 commit comments

Comments
 (0)