Repository: gleam-lang/stdlib
Pull request #823: Various changes to the list module
Author: login: giacomocavalieri
Created: 2025-04-07T17:17:22Z | Merged: 2025-04-18T17:58:54Z

I went over the list module and found some functions that weren't implemented with the usual `_loop` pattern and changed those. In doing so I've also made `combinations` tail recursive, it was not previously

Ah and I've changed some bit array segments that were implicitly truncated to their truncated value that's actually used!


Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b1e4ddf..38931fe9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## Unreleased
+
+- The performance of various functions in the `list` module has been improved.
+
 ## v0.59.0 - 2025-04-07
 
 - The `debug` function in the `io` module has been deprecated in favour of
diff --git a/src/gleam/list.gleam b/src/gleam/list.gleam
index 52752931..472311a4 100644
--- a/src/gleam/list.gleam
+++ b/src/gleam/list.gleam
@@ -26,7 +26,6 @@ import gleam/dict.{type Dict}
 import gleam/float
 import gleam/int
 import gleam/order.{type Order}
-import gleam/pair
 
 /// Counts the number of elements in a given list.
 ///
@@ -88,12 +87,18 @@ fn length_loop(list: List(a), count: Int) -> Int {
 /// ```
 ///
 pub fn count(list: List(a), where predicate: fn(a) -> Bool) -> Int {
-  fold(list, 0, fn(acc, value) {
-    case predicate(value) {
-      True -> acc + 1
-      False -> acc
-    }
-  })
+  count_loop(list, predicate, 0)
+}
+
+fn count_loop(list: List(a), predicate: fn(a) -> Bool, acc: Int) -> Int {
+  case list {
+    [] -> acc
+    [first, ..rest] ->
+      case predicate(first) {
+        True -> count_loop(rest, predicate, acc + 1)
+        False -> count_loop(rest, predicate, acc)
+      }
+  }
 }
 
 /// Creates a new list from a given list containing the same elements but in the
@@ -127,7 +132,10 @@ pub fn reverse(list: List(a)) -> List(a) {
   reverse_and_prepend(list, [])
 }
 
-// Reverses a list and prepends it to another list
+/// Reverses a list and prepends it to another list.
+/// This function runs in linear time, proportional to the lenght of the list
+/// to prepend.
+///
 fn reverse_and_prepend(list prefix: List(a), to suffix: List(a)) -> List(a) {
   case prefix {
     [] -> suffix
@@ -287,14 +295,23 @@ pub fn rest(list: List(a)) -> Result(List(a), Nil) {
 /// ```
 ///
 pub fn group(list: List(v), by key: fn(v) -> k) -> Dict(k, List(v)) {
-  fold(list, dict.new(), update_group(key))
+  group_loop(list, key, dict.new())
 }
 
-fn update_group(f: fn(a) -> k) -> fn(Dict(k, List(a)), a) -> Dict(k, List(a)) {
-  fn(groups, elem) {
-    case dict.get(groups, f(elem)) {
-      Ok(existing) -> dict.insert(groups, f(elem), [elem, ..existing])
-      Error(_) -> dict.insert(groups, f(elem), [elem])
+fn group_loop(
+  list: List(v),
+  to_key: fn(v) -> k,
+  groups: Dict(k, List(v)),
+) -> Dict(k, List(v)) {
+  case list {
+    [] -> groups
+    [first, ..rest] -> {
+      let key = to_key(first)
+      let groups = case dict.get(groups, key) {
+        Error(_) -> dict.insert(groups, key, [first])
+        Ok(existing) -> dict.insert(groups, key, [first, ..existing])
+      }
+      group_loop(rest, to_key, groups)
     }
   }
 }
@@ -438,12 +455,22 @@ pub fn map_fold(
   from initial: acc,
   with fun: fn(acc, a) -> #(acc, b),
 ) -> #(acc, List(b)) {
-  fold(over: list, from: #(initial, []), with: fn(acc, item) {
-    let #(current_acc, items) = acc
-    let #(next_acc, next_item) = fun(current_acc, item)
-    #(next_acc, [next_item, ..items])
-  })
-  |> pair.map_second(reverse)
+  map_fold_loop(list, fun, initial, [])
+}
+
+fn map_fold_loop(
+  list: List(a),
+  fun: fn(acc, a) -> #(acc, b),
+  acc: acc,
+  list_acc: List(b),
+) -> #(acc, List(b)) {
+  case list {
+    [] -> #(acc, reverse(list_acc))
+    [first, ..rest] -> {
+      let #(acc, first) = fun(acc, first)
+      map_fold_loop(rest, fun, acc, [first, ..list_acc])
+    }
+  }
 }
 
 /// Returns a new list containing only the elements of the first list after the
@@ -672,14 +699,6 @@ pub fn prepend(to list: List(a), this item: a) -> List(a) {
   [item, ..list]
 }
 
-fn flatten_loop(lists: List(List(a)), acc: List(a)) -> List(a) {
-  case lists {
-    [] -> reverse(acc)
-    [list, ..further_lists] ->
-      flatten_loop(further_lists, reverse_and_prepend(list: list, to: acc))
-  }
-}
-
 /// This is the same as `concat`: it joins a list of lists into a single
 /// list.
 ///
@@ -696,6 +715,14 @@ pub fn flatten(lists: List(List(a))) -> List(a) {
   flatten_loop(lists, [])
 }
 
+fn flatten_loop(lists: List(List(a)), acc: List(a)) -> List(a) {
+  case lists {
+    [] -> reverse(acc)
+    [list, ..further_lists] ->
+      flatten_loop(further_lists, reverse_and_prepend(list, to: acc))
+  }
+}
+
 /// Maps the list with the given function into a list of lists, and then flattens it.
 ///
 /// ## Examples
@@ -706,8 +733,7 @@ pub fn flatten(lists: List(List(a))) -> List(a) {
 /// ```
 ///
 pub fn flat_map(over list: List(a), with fun: fn(a) -> List(b)) -> List(b) {
-  map(list, fun)
-  |> flatten
+  flatten(map(list, fun))
 }
 
 /// Reduces a list of elements into a single value by calling a given function
@@ -2089,7 +2115,11 @@ fn scan_loop(
 /// ```
 ///
 pub fn last(list: List(a)) -> Result(a, Nil) {
-  reduce(list, fn(_, elem) { elem })
+  case list {
+    [] -> Error(Nil)
+    [last] -> Ok(last)
+    [_, ..rest] -> last(rest)
+  }
 }
 
 /// Return unique combinations of elements in the list.
@@ -2107,24 +2137,18 @@ pub fn last(list: List(a)) -> Result(a, Nil) {
 /// ```
 ///
 pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
-  case n {
-    0 -> [[]]
-    _ ->
-      case items {
-        [] -> []
-        [first, ..rest] -> {
-          let first_combinations =
-            map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
-            |> reverse
-          fold(first_combinations, combinations(rest, n), fn(acc, c) {
-            [c, ..acc]
-          })
-        }
-      }
+  case n, items {
+    0, _ -> [[]]
+    _, [] -> []
+    _, [first, ..rest] ->
+      combinations(rest, n - 1)
+      |> map(fn(combination) { [first, ..combination] })
+      |> reverse
+      |> fold(combinations(rest, n), fn(acc, c) { [c, ..acc] })
   }
 }
 
-/// Return unique pair combinations of elements in the list
+/// Return unique pair combinations of elements in the list.
 ///
 /// ## Examples
 ///
@@ -2134,16 +2158,16 @@ pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
 /// ```
 ///
 pub fn combination_pairs(items: List(a)) -> List(#(a, a)) {
-  combination_pairs_loop(items)
-  |> flatten
+  combination_pairs_loop(items, [])
 }
 
-fn combination_pairs_loop(items: List(a)) -> List(List(#(a, a))) {
+fn combination_pairs_loop(items: List(a), acc: List(#(a, a))) -> List(#(a, a)) {
   case items {
-    [] -> []
+    [] -> reverse(acc)
     [first, ..rest] -> {
       let first_combinations = map(rest, with: fn(other) { #(first, other) })
-      [first_combinations, ..combination_pairs_loop(rest)]
+      let acc = reverse_and_prepend(first_combinations, acc)
+      combination_pairs_loop(rest, acc)
     }
   }
 }
@@ -2166,7 +2190,7 @@ pub fn interleave(list: List(List(a))) -> List(a) {
 ///
 /// Notice: This function is not tail recursive,
 /// and thus may exceed stack size if called,
-/// with large lists (on target JavaScript).
+/// with large lists (on the JavaScript target).
 ///
 /// ## Examples
 ///
@@ -2175,25 +2199,34 @@ pub fn interleave(list: List(List(a))) -> List(a) {
 /// // -> [[1, 101], [2, 102], [3, 103]]
 /// ```
 ///
-pub fn transpose(list_of_list: List(List(a))) -> List(List(a)) {
-  let take_first = fn(list) {
-    case list {
-      [] -> []
-      [first] -> [first]
-      [first, ..] -> [first]
+pub fn transpose(list_of_lists: List(List(a))) -> List(List(a)) {
+  transpose_loop(list_of_lists, [])
+}
+
+fn transpose_loop(rows: List(List(a)), columns: List(List(a))) -> List(List(a)) {
+  case rows {
+    [] -> reverse(columns)
+    _ -> {
+      let #(column, rest) = take_firsts(rows, [], [])
+      case column {
+        [_, ..] -> transpose_loop(rest, [column, ..columns])
+        [] -> transpose_loop(rest, columns)
+      }
     }
   }
+}
 
-  case list_of_list {
-    [] -> []
-    [[], ..rest] -> transpose(rest)
-    rows -> {
-      let firsts =
-        rows
-        |> map(take_first)
-        |> flatten
-      let rest = transpose(map(rows, drop(_, 1)))
-      [firsts, ..rest]
+fn take_firsts(
+  rows: List(List(a)),
+  column: List(a),
+  remaining_rows: List(List(a)),
+) -> #(List(a), List(List(a))) {
+  case rows {
+    [] -> #(reverse(column), reverse(remaining_rows))
+    [[], ..rest] -> take_firsts(rest, column, remaining_rows)
+    [[first, ..remaining_row], ..rest_rows] -> {
+      let remaining_rows = [remaining_row, ..remaining_rows]
+      take_firsts(rest_rows, [first, ..column], remaining_rows)
     }
   }
 }
@@ -2250,12 +2283,21 @@ pub fn max(
   over list: List(a),
   with compare: fn(a, a) -> Order,
 ) -> Result(a, Nil) {
-  reduce(over: list, with: fn(acc, other) {
-    case compare(acc, other) {
-      order.Gt -> acc
-      order.Lt | order.Eq -> other
-    }
-  })
+  case list {
+    [] -> Error(Nil)
+    [first, ..rest] -> Ok(max_loop(rest, compare, first))
+  }
+}
+
+fn max_loop(list, compare, max) {
+  case list {
+    [] -> max
+    [first, ..rest] ->
+      case compare(first, max) {
+        order.Gt -> max_loop(rest, compare, first)
+        order.Lt | order.Eq -> max_loop(rest, compare, max)
+      }
+  }
 }
 
 /// Take a random sample of k elements from a list using reservoir sampling via
diff --git a/test/gleam/bit_array_test.gleam b/test/gleam/bit_array_test.gleam
index 1d0c6224..6294c98c 100644
--- a/test/gleam/bit_array_test.gleam
+++ b/test/gleam/bit_array_test.gleam
@@ -193,11 +193,11 @@ pub fn to_string_test() {
   |> bit_array.to_string
   |> should.equal(Ok("ø"))
 
-  <<65_535>>
+  <<255>>
   |> bit_array.to_string
   |> should.equal(Error(Nil))
 
-  <<"ø":utf8, 50:4>>
+  <<"ø":utf8, 2:4>>
   |> bit_array.to_string
   |> should.equal(Error(Nil))
 
@@ -224,7 +224,7 @@ pub fn is_utf8_test() {
   |> bit_array.is_utf8
   |> should.be_true
 
-  <<65_535>>
+  <<255>>
   |> bit_array.is_utf8
   |> should.be_false
 }
@@ -452,7 +452,7 @@ pub fn compare_test() {
   bit_array.compare(<<4:5, 3:3, 0:0>>, <<4:5, 3:3, 0:0>>)
   |> should.equal(order.Eq)
 
-  bit_array.compare(<<4:2, 3:4, 0:0>>, <<4:2, 3:3, 0:0>>)
+  bit_array.compare(<<0:2, 3:4, 0:0>>, <<0:2, 3:3, 0:0>>)
   |> should.equal(order.Gt)
 
   // first is: <<33, 1:size(1)>>


Reviews:
user: login: inoas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:41:36Z
user: login: inoas
body: What happens for 100k or 1mio for new.max?

+nitpickery:
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:44:30Z
user: login: inoas
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-08T17:48:23Z
user: login: CloserToTheCenter
state: COMMENTED
author_association: NONE
submitted_at: 2025-04-08T18:15:03Z
user: login: giacomocavalieri
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T18:22:29Z
user: login: giacomocavalieri
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T18:22:57Z
user: login: giacomocavalieri
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-08T19:10:42Z
user: login: lpil
body: Thank you!
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-18T17:58:32Z

Review comments:
diff_hunk: @@ -2107,24 +2147,19 @@ pub fn last(list: List(a)) -> Result(a, Nil) {
 /// ```
 ///
 pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
-  case n {
-    0 -> [[]]
-    _ ->
-      case items {
-        [] -> []
-        [first, ..rest] -> {
-          let first_combinations =
-            map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
-            |> reverse
-          fold(first_combinations, combinations(rest, n), fn(acc, c) {
-            [c, ..acc]
-          })
-        }
-      }
+  case n, items {
+    0, _ -> [[]]
+    _, [] -> []
+    _, [first, ..rest] -> {
+      let first_combinations =
+        map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
+        |> reverse
+      fold(first_combinations, combinations(rest, n), fn(acc, c) { [c, ..acc] })
path: src/gleam/list.gleam
user: login: inoas
body: this could probably just pipe?
created_at: 2025-04-08T17:41:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2134,16 +2169,16 @@ pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
 /// ```
 ///
 pub fn combination_pairs(items: List(a)) -> List(#(a, a)) {
-  combination_pairs_loop(items)
-  |> flatten
+  combination_pairs_loop(items, [])
 }
 
-fn combination_pairs_loop(items: List(a)) -> List(List(#(a, a))) {
+fn combination_pairs_loop(items: List(a), acc: List(#(a, a))) -> List(#(a, a)) {
   case items {
-    [] -> []
+    [] -> reverse(acc)
     [first, ..rest] -> {
       let first_combinations = map(rest, with: fn(other) { #(first, other) })
-      [first_combinations, ..combination_pairs_loop(rest)]
+      let acc = reverse_and_prepend(first_combinations, acc)
+      combination_pairs_loop(rest, acc)
path: src/gleam/list.gleam
user: login: inoas
body: this could probably just pipe and then we can get rid of the curleys?
created_at: 2025-04-08T17:42:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -706,8 +733,17 @@ pub fn flatten(lists: List(List(a))) -> List(a) {
 /// ```
 ///
 pub fn flat_map(over list: List(a), with fun: fn(a) -> List(b)) -> List(b) {
-  map(list, fun)
-  |> flatten
+  flat_map_loop(list, fun, [])
+}
+
+fn flat_map_loop(list: List(a), fun: fn(a) -> List(b), acc: List(b)) -> List(b) {
+  case list {
+    [] -> reverse(acc)
+    [first, ..rest] -> {
+      let list = fun(first)
+      flat_map_loop(rest, fun, reverse_and_prepend(list, to: acc))
path: src/gleam/list.gleam
user: login: inoas
body: would this read better?
```
first |> fun |> reverse_and_prepend(to: acc) |> flat_map_loop(rest, fun, _)
```
created_at: 2025-04-08T17:44:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2107,24 +2147,19 @@ pub fn last(list: List(a)) -> Result(a, Nil) {
 /// ```
 ///
 pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
-  case n {
-    0 -> [[]]
-    _ ->
-      case items {
-        [] -> []
-        [first, ..rest] -> {
-          let first_combinations =
-            map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
-            |> reverse
-          fold(first_combinations, combinations(rest, n), fn(acc, c) {
-            [c, ..acc]
-          })
-        }
-      }
+  case n, items {
+    0, _ -> [[]]
+    _, [] -> []
+    _, [first, ..rest] -> {
+      let first_combinations =
+        map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
+        |> reverse
+      fold(first_combinations, combinations(rest, n), fn(acc, c) { [c, ..acc] })
path: src/gleam/list.gleam
user: login: inoas
body: aka:
```gleam
_, [first, ..rest] ->
  combinations(rest, n - 1)
  |> map(with: fn(com) { [first, ..com] })
  |> reverse
  |> fold(combinations(rest, n), fn(acc, c) { [c, ..acc] })
```
?
created_at: 2025-04-08T17:48:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2250,12 +2285,21 @@ pub fn max(
   over list: List(a),
   with compare: fn(a, a) -> Order,
 ) -> Result(a, Nil) {
-  reduce(over: list, with: fn(acc, other) {
-    case compare(acc, other) {
-      order.Gt -> acc
-      order.Lt | order.Eq -> other
-    }
-  })
+  case list {
+    [] -> Error(Nil)
+    [first, ..rest] -> Ok(max_loop(rest, compare, first))
+  }
+}
+
+fn max_loop(list, compare, max) {
+  case list {
+    [] -> max
+    [first, ..rest] ->
+      case compare(first, max) {
+        order.Gt -> max_loop(rest, compare, first)
+        order.Lt | order.Eq -> max_loop(rest, compare, max)
path: src/gleam/list.gleam
user: login: CloserToTheCenter
body: Undocumented behavior: how ties are resolved.

This defaults to the first maximum encountered. This is specified in neither docs nor tests, but some programs will depend on it.
created_at: 2025-04-08T18:15:03Z
author_association: NONE
diff_hunk: @@ -2134,16 +2169,16 @@ pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
 /// ```
 ///
 pub fn combination_pairs(items: List(a)) -> List(#(a, a)) {
-  combination_pairs_loop(items)
-  |> flatten
+  combination_pairs_loop(items, [])
 }
 
-fn combination_pairs_loop(items: List(a)) -> List(List(#(a, a))) {
+fn combination_pairs_loop(items: List(a), acc: List(#(a, a))) -> List(#(a, a)) {
   case items {
-    [] -> []
+    [] -> reverse(acc)
     [first, ..rest] -> {
       let first_combinations = map(rest, with: fn(other) { #(first, other) })
-      [first_combinations, ..combination_pairs_loop(rest)]
+      let acc = reverse_and_prepend(first_combinations, acc)
+      combination_pairs_loop(rest, acc)
path: src/gleam/list.gleam
user: login: giacomocavalieri
body: I feel like making this a pipe would not necessarily make it easier to read
created_at: 2025-04-08T18:22:28Z
author_association: MEMBER
diff_hunk: @@ -706,8 +733,17 @@ pub fn flatten(lists: List(List(a))) -> List(a) {
 /// ```
 ///
 pub fn flat_map(over list: List(a), with fun: fn(a) -> List(b)) -> List(b) {
-  map(list, fun)
-  |> flatten
+  flat_map_loop(list, fun, [])
+}
+
+fn flat_map_loop(list: List(a), fun: fn(a) -> List(b), acc: List(b)) -> List(b) {
+  case list {
+    [] -> reverse(acc)
+    [first, ..rest] -> {
+      let list = fun(first)
+      flat_map_loop(rest, fun, reverse_and_prepend(list, to: acc))
path: src/gleam/list.gleam
user: login: giacomocavalieri
body: Nah I think this is fine already
created_at: 2025-04-08T18:22:57Z
author_association: MEMBER
diff_hunk: @@ -2107,24 +2147,19 @@ pub fn last(list: List(a)) -> Result(a, Nil) {
 /// ```
 ///
 pub fn combinations(items: List(a), by n: Int) -> List(List(a)) {
-  case n {
-    0 -> [[]]
-    _ ->
-      case items {
-        [] -> []
-        [first, ..rest] -> {
-          let first_combinations =
-            map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
-            |> reverse
-          fold(first_combinations, combinations(rest, n), fn(acc, c) {
-            [c, ..acc]
-          })
-        }
-      }
+  case n, items {
+    0, _ -> [[]]
+    _, [] -> []
+    _, [first, ..rest] -> {
+      let first_combinations =
+        map(combinations(rest, n - 1), with: fn(com) { [first, ..com] })
+        |> reverse
+      fold(first_combinations, combinations(rest, n), fn(acc, c) { [c, ..acc] })
path: src/gleam/list.gleam
user: login: giacomocavalieri
body: Nice catch!
created_at: 2025-04-08T19:10:42Z
author_association: MEMBER

PR comments:
user: login: lpil
created_at: 2025-04-08T09:13:38Z
body: Could you share your benchmarks please 🙏 Can't evaluate performance motivated changes without them.
author_association: MEMBER
user: login: giacomocavalieri
created_at: 2025-04-08T12:48:57Z
body: Sure!
- `list.count`
  - On the Erlang target we see an average 30% improvement for lists ranging from 10 to 10K items
    ```text
    Input               Function                       IPS
    100                 old.count(_, is_even)  1780018.9954
    100                 new.count(_, is_even)  2627295.0623 +30%
    1K                  old.count(_, is_even)   200183.5025
    1K                  new.count(_, is_even)   314633.3074 +36%
    10K                 old.count(_, is_even)    20369.6493
    10K                 new.count(_, is_even)    30554.5072 +33%
    ```
  - On the JavaScript target this makes no noticeable difference
- `list.group`
  - On the Erlang target it's a 9% improvement on average:
    ```text
    Input               Function                       IPS
    100                 old.group(_, mod5)     166806.2302
    100                 new.group(_, mod5)     185181.1164 +10%
    1K                  old.group(_, mod5)      16939.5837
    1K                  new.group(_, mod5)      18538.5855 +9%
    10K                 old.group(_, mod5)       1668.1442
    10K                 new.group(_, mod5)       1860.1116 +10%
    ```
  - Same goes for the JavaScript target:
    ```text
    Input               Function                       IPS
    100                 old.group(_, mod5)      10819.7221
    100                 new.group(_, mod5)      11863.1991 +8%
    1K                  old.group(_, mod5)       1067.1118
    1K                  new.group(_, mod5)       1171.8842 +9%
    10K                 old.group(_, mod5)        106.8180
    10K                 new.group(_, mod5)        117.9820 +9%
    ```
- `list.map_fold`
  - On the Erlang target it performs a little worse on a small list, however it has a much bigger improvement as the list grows, not really sure why that small drop on 100 items!
    ```txt
    Input               Function                       IPS
    100                 old.map_fold(_, 0, id)  1180019.7534
    100                 new.map_fold(_, 0, id)  1034750.9199 -14%
    1K                  old.map_fold(_, 0, id)    86292.0824
    1K                  new.map_fold(_, 0, id)   114499.5901 +25%
    10K                 old.map_fold(_, 0, id)     8769.9480
    10K                 new.map_fold(_, 0, id)    12507.9839 +30%
    ```
  - On the JavaScript target a nice improvement across the board
    ```txt
    Input               Function                       IPS
    100                 old.map_fold(_, 0, id)   801101.7768
    100                 new.map_fold(_, 0, id)   997124.8861 +20%
    1K                  old.map_fold(_, 0, id)    86383.0092
    1K                  new.map_fold(_, 0, id)   115212.4870 +25%
    10K                 old.map_fold(_, 0, id)     8394.7783
    10K                 new.map_fold(_, 0, id)     9885.7841 +15%
    ```
- `list.flat_map` no noticeable difference on both targets
- `list.last`
  - Big improvements on Erlang:
    ```txt
    Input               Function                       IPS
    100                 old.last              2263983.8634
    100                 new.last              2878676.0305 +21%
    1K                  old.last               133592.7853
    1K                  new.last               382436.9183 +65%
    10K                 old.last                11661.8486
    10K                 new.last                31847.0297 +63%
    ```
  - And some improvements for JavaScript as well
    ```txt
    Input               Function                       IPS
    100                 old.last              2163553.2030
    100                 new.last              2978601.9351 +27%
    1K                  old.last               314094.5715
    1K                  new.last               489229.2581 +36%
    10K                 old.last                32737.3927
    10K                 new.last                45945.1156 +29%
    ```
- `list.combination_pair` is now tail recursive, it was not previously
- `list.max`
  - Improvements on the Erlang target:
    ```txt
    Input               Function                          IPS
    100                 old.max(_, int.compare)  2032145.6313
    100                 new.max(_, int.compare)  2498915.9989 +19%
    1K                  old.max(_, int.compare)   219478.0872
    1K                  new.max(_, int.compare)   293443.3055 +25%
    10K                 old.max(_, int.compare)    21824.8833
    10K                 new.max(_, int.compare)    23550.1938 +7%
    ```
  - And it improves for JS as well:
    ```txt
    Input               Function                          IPS
    100                 old.max(_, int.compare)  1088848.1258
    100                 new.max(_, int.compare)  1077516.4628 -1%
    1K                  old.max(_, int.compare)   143517.8054
    1K                  new.max(_, int.compare)   198081.4548 +27%
    10K                 old.max(_, int.compare)    14711.3318
    10K                 new.max(_, int.compare)    20333.3453 +27%
    100K                old.max(_, int.compare)     1471.8014
    100K                new.max(_, int.compare)     2036.0563 +27%
    ```
- `list.transpose` is now tail recursive, it was not previously
  - On the Erlang target:
    ```txt
    Input               Function                       IPS
    100                 old.transpose            4896.5162
    100                 new.transpose           19127.8524 +74%
    1K                  old.transpose              31.9103
    1K                  new.transpose              80.8961 +60%
    10K                 old.transpose               0.0000 // neither finished in time
    10K                 new.transpose               0.0000 
    ```
  - On the JavaScript target:
    ```txt
    Input               Function                       IPS
    100                 old.transpose            1845.6147
    100                 new.transpose            4998.9909 +63%
    1K                  old.transpose               9.4205
    1K                  new.transpose              17.1786 +45%
    10K                 old.transpose                ----- // crashed out of memory
    10K                 new.transpose               0.0000 // didn't finish in time but at least didn't crash

    ``` 
    
Hope this is thorough enough!
author_association: MEMBER
user: login: lpil
created_at: 2025-04-15T16:19:35Z
body: Thank you! Could you revert the flat map change please 🙏 If there's no performance improvement we don't want to increase code size 
author_association: MEMBER
user: login: giacomocavalieri
created_at: 2025-04-15T16:49:26Z
body: Done! And I've updated the Changelog
author_association: MEMBER