Repository: marcglasberg/fast_immutable_collections

Issue:
Can Tuple2 be removed with Dart 3.0 now having Records?



Discussion: Wouldn't that be a breaking change for everyone using Tuple2, and not much to gain from it?
Yeah it would be a breaking change in any case. I'm anticipating that people are going to migrate from `Tuple2` to Records anyways. 
The main gain would be that for example `IList.splitAt` would then return a record and the caller could then benefit from the new record syntax, which brings destructuring into local variables etc.

Another reason agains having `Tuple2` in general is, that there is no `Tuple3` or `Tuple4`. Prior to Dart 3.0 we had to add https://pub.dev/packages/tuple to our projects which also provides a `Tuple2` and would thus cause a name conflict.
From my point of view we should either have support for the whole tuple "spectrum" or remove it completely, because otherwise the user will use a different tuple implementation anyways.
Makes sense not to return Tuple2 anymore.
Should I create a PR for this then?
Dart 3 is nearly mandatory, having a breaking change in revision would be enough and migration does not seems too difficult. It also open the doors to more syntax sugars and new methods.
Please send a PR.

Fix patch:
diff --git a/example/benchmark/benchmark_app/lib/screens/multi_benchmark_screen.dart b/example/benchmark/benchmark_app/lib/screens/multi_benchmark_screen.dart
index 49873215..962bc734 100644
--- a/example/benchmark/benchmark_app/lib/screens/multi_benchmark_screen.dart
+++ b/example/benchmark/benchmark_app/lib/screens/multi_benchmark_screen.dart
@@ -33,16 +33,10 @@ class MultiBenchmarkScreen extends StatelessWidget {
     );
   }
 
-  List<Widget> get _benchmarks {
-    switch (collectionType) {
-      case List:
-        return listBenchmarks;
-      case Set:
-        return setBenchmarks;
-      case Map:
-        return mapBenchmarks;
-      default:
-        throw UnimplementedError("No benchmarks for this collection type.");
-    }
-  }
+  List<Widget> get _benchmarks => switch (collectionType) {
+        const (List) => listBenchmarks,
+        const (Set) => setBenchmarks,
+        const (Map) => mapBenchmarks,
+        _ => throw UnimplementedError("No benchmarks for this collection type."),
+      };
 }
diff --git a/example/main.dart b/example/main.dart
index e122fcb1..08e95fe9 100644
--- a/example/main.dart
+++ b/example/main.dart
@@ -10,7 +10,7 @@ void main() {
   print('Original List: $iList');
 
   // Access elements in the list.
-  int firstElement = iList.first;
+  final int firstElement = iList.first;
   print('First Element: $firstElement');
 
   // Remove an element from the list.
@@ -18,7 +18,7 @@ void main() {
   print('List after removing 20: $iList');
 
   // Find an element in the list.
-  bool containsThirty = iList.contains(30);
+  final bool containsThirty = iList.contains(30);
   print('Does the list contain 30? $containsThirty');
 
   // Example usage of ISet.
@@ -34,12 +34,12 @@ void main() {
   print('Set after removing orange: $iSet');
 
   // Check if the set contains an element.
-  bool containsBanana = iSet.contains('banana');
+  final bool containsBanana = iSet.contains('banana');
   print('Does the set contain banana? $containsBanana');
 
   // Iterate over the elements in the set.
   print('Iterating over the set:');
-  for (String fruit in iSet) {
+  for (final String fruit in iSet) {
     print(fruit);
   }
 }
diff --git a/example/simple/main.dart b/example/simple/main.dart
index 1f27d63f..7c65a992 100644
--- a/example/simple/main.dart
+++ b/example/simple/main.dart
@@ -1,11 +1,11 @@
 import "package:fast_immutable_collections/fast_immutable_collections.dart";
 
 void main() {
-  var ilist1 = IList<dynamic>();
+  final ilist1 = IList<dynamic>();
 
   print(ilist1); // []
 
-  var ilist2 = ilist1.add(1);
+  final ilist2 = ilist1.add(1);
 
   print(ilist1); // []
   print(ilist2); // [1]
diff --git a/lib/fast_immutable_collections.dart b/lib/fast_immutable_collections.dart
index 845aaef0..0bb0ccdd 100644
--- a/lib/fast_immutable_collections.dart
+++ b/lib/fast_immutable_collections.dart
@@ -20,7 +20,7 @@ export "src/base/zip_extensions.dart";
 // IList:
 export "src/ilist/from_ilist_mixin.dart";
 export "src/ilist/from_iterable_ilist_mixin.dart";
-export "src/ilist/ilist.dart" hide L, IListImpl, InternalsForTestingPurposesIList;
+export "src/ilist/ilist.dart" hide IListImpl, InternalsForTestingPurposesIList, L;
 export "src/ilist/ilist_of_2.dart";
 export "src/ilist/ilist_of_3.dart";
 export "src/ilist/ilist_of_4.dart";
@@ -31,7 +31,7 @@ export "src/ilist/unmodifiable_list_from_ilist.dart";
 
 // IMap:
 export "src/imap/entry.dart";
-export "src/imap/imap.dart" hide M, IMapImpl, InternalsForTestingPurposesIMap;
+export "src/imap/imap.dart" hide IMapImpl, InternalsForTestingPurposesIMap, M;
 export "src/imap/map_extension.dart";
 export "src/imap/modifiable_map_from_imap.dart";
 export "src/imap/unmodifiable_map_from_imap.dart";
@@ -42,7 +42,7 @@ export "src/imap_of_sets/imap_of_sets.dart";
 // ISet:
 export "src/iset/from_iset_mixin.dart";
 export "src/iset/from_iterable_iset_mixin.dart";
-export "src/iset/iset.dart" hide S, ISetImpl, InternalsForTestingPurposesISet;
+export "src/iset/iset.dart" hide ISetImpl, InternalsForTestingPurposesISet, S;
 export "src/iset/modifiable_set_from_iset.dart";
 export "src/iset/set_extension.dart";
 export "src/iset/unmodifiable_set_from_iset.dart";
diff --git a/lib/src/base/zip_extensions.dart b/lib/src/base/zip_extensions.dart
index ceb98188..409e9804 100644
--- a/lib/src/base/zip_extensions.dart
+++ b/lib/src/base/zip_extensions.dart
@@ -1,8 +1,7 @@
-import 'package:fast_immutable_collections/fast_immutable_collections.dart';
-
-extension FICZipExtension<U, V> on Iterable<Tuple2<U, V>> {
-  /// Iterable Tuple2 as Iterable
-  Tuple2<Iterable<U>, Iterable<V>> unzip() => Tuple2(
-      Iterable<U>.generate(length, (idx) => elementAt(idx).first),
-      Iterable<V>.generate(length, (idx) => elementAt(idx).second));
+extension FICZipExtension<U, V> on Iterable<(U, V)> {
+  /// Iterable Record as Iterable
+  (Iterable<U>, Iterable<V>) unzip() => (
+        Iterable<U>.generate(length, (idx) => elementAt(idx).$1),
+        Iterable<V>.generate(length, (idx) => elementAt(idx).$2)
+      );
 }
diff --git a/lib/src/ilist/ilist.dart b/lib/src/ilist/ilist.dart
index 4152044e..d98d7980 100644
--- a/lib/src/ilist/ilist.dart
+++ b/lib/src/ilist/ilist.dart
@@ -1080,7 +1080,7 @@ abstract class IList<T> // ignore: must_be_immutable
   }
 
   /// Split the List at specified index
-  Tuple2<Iterable<T>, Iterable<T>> splitAt(int index) => Tuple2(take(index), skip(index));
+  (Iterable<T>, Iterable<T>) splitAt(int index) => (take(index), skip(index));
 
   /// Moves all items that satisfy the provided [test] to the end of the list.
   /// Keeps the relative order of the moved items.
@@ -1692,26 +1692,26 @@ abstract class IList<T> // ignore: must_be_immutable
   int count(Predicate<T> p) => where(p).length;
 
   /// Split list based on predicate p. (takeWhile p, dropWhile p)
-  Tuple2<Iterable<T>, Iterable<T>> span(Predicate<T> p) {
+  (Iterable<T>, Iterable<T>) span(Predicate<T> p) {
     final i = indexWhere((e) => !p(e));
     final idx = i < 0 ? length : i;
-    return Tuple2(getRange(0, idx), getRange(idx, length));
+    return (getRange(0, idx), getRange(idx, length));
   }
 
   /// Aggregate each element with corresponding index
-  Iterable<Tuple2<int, T>> zipWithIndex() =>
-      Iterable.generate(length, (index) => Tuple2(index, _l[index])).toIList(config);
+  Iterable<(int, T)> zipWithIndex() =>
+      Iterable.generate(length, (index) => (index, _l[index])).toIList(config);
 
   /// Aggregate two sources trimming by the shortest source
-  Iterable<Tuple2<T, T>> zip(Iterable<T> otherIterable) {
+  Iterable<(T, T)> zip(Iterable<T> otherIterable) {
     final other = otherIterable.toList();
     final minLength = min(length, other.length);
-    return Iterable.generate(minLength, (index) => Tuple2(_l[index], other[index])).toIList(config);
+    return Iterable.generate(minLength, (index) => (_l[index], other[index])).toIList(config);
   }
 
   /// Aggregate two sources based on the longest source.
   /// Missing elements can be completed by passing a [currentFill] and [otherFill] methods or will be at null by default
-  Iterable<Tuple2<T?, U?>> zipAll<U>(
+  Iterable<(T?, U?)> zipAll<U>(
     Iterable<U> otherIterable, {
     T Function(int index)? currentFill,
     U Function(int index)? otherFill,
@@ -1728,7 +1728,7 @@ abstract class IList<T> // ignore: must_be_immutable
 
     return Iterable.generate(
         maxLength,
-        (index) => Tuple2<T?, U?>(
+        (index) => (
               getOrFill(current, index, currentFill) as T?,
               getOrFill(other, index, otherFill) as U?,
             )).toIList(config);
diff --git a/lib/src/ilist/ilist_of_2.dart b/lib/src/ilist/ilist_of_2.dart
index 13dbeb90..bf55ad20 100644
--- a/lib/src/ilist/ilist_of_2.dart
+++ b/lib/src/ilist/ilist_of_2.dart
@@ -28,69 +28,3 @@ class IListOf2<T> {
   @override
   int get hashCode => hashObj2(first, last);
 }
-
-/// Dedicated Indexation Zip
-// TODO required non-function type alias since dart 2.13
-// typedef IndexedZip<T> = Zip<int, T>;
-
-abstract class Tuple {
-  //
-  final int arity;
-
-  const Tuple(this.arity);
-}
-
-/// Resulting composition of multiple sources like indexes or other Iterables.
-/// Adapted from Tuple package
-class Tuple2<T1, T2> extends Tuple {
-  //
-  /// Returns the first item of the tuple
-  final T1 first;
-
-  /// Returns the second item of the tuple
-  final T2 second;
-
-  /// Creates a new tuple value with the specified items.
-  const Tuple2(this.first, this.second) : super(2);
-
-  /// Operator access
-  Object? operator [](int i) {
-    if (i == 0)
-      return first;
-    else if (i == 1)
-      return second;
-    else
-      throw IndexError.withLength(i, 2, indexable: this);
-  }
-
-  /// Create a new tuple value with the specified list [items].
-  factory Tuple2.fromList(List items) {
-    if (items.length != 2) {
-      throw ArgumentError('items must have length 2');
-    }
-
-    return Tuple2<T1, T2>(items[0] as T1, items[1] as T2);
-  }
-
-  /// Returns a tuple with the first item set to the specified value.
-  Tuple2<T1, T2> withItem1(T1 v) => Tuple2<T1, T2>(v, second);
-
-  /// Returns a tuple with the second item set to the specified value.
-  Tuple2<T1, T2> withItem2(T2 v) => Tuple2<T1, T2>(first, v);
-
-  /// Creates a [List] containing the items of this [Tuple2].
-  ///
-  /// The elements are in item order. The list is variable-length
-  /// if [growable] is true.
-  List toList({bool growable = false}) => List.from([first, second], growable: growable);
-
-  @override
-  String toString() => '($first, $second)';
-
-  @override
-  bool operator ==(Object other) =>
-      other is Tuple2 && other.first == first && other.second == second;
-
-  @override
-  int get hashCode => hash2(first.hashCode, second.hashCode);
-}


Test patch:
diff --git a/test/ilist/ilist_test.dart b/test/ilist/ilist_test.dart
index 057839f3..5899b360 100644
--- a/test/ilist/ilist_test.dart
+++ b/test/ilist/ilist_test.dart
@@ -1505,16 +1505,22 @@ void main() {
     final ilist = [0, 2, 4, 6, 7, 8, 9, 2].lock;
 
     final evenSpan = ilist.span((e) => e % 2 == 0);
-    expect(evenSpan.first.toIList(), [0, 2, 4, 6]);
-    expect(evenSpan.second.toIList(), [7, 8, 9, 2]);
+    expect(evenSpan.$1.toIList(), [0, 2, 4, 6]);
+    expect(evenSpan.$2.toIList(), [7, 8, 9, 2]);
   });
 
   test("Zip with Index", () {
     //
     final ilist1 = ['red', 'green', 'blue', 'alpha'].lock;
     final indexZipped = ilist1.zipWithIndex();
-    expect(indexZipped,
-        IList([Tuple2(0, 'red'), Tuple2(1, 'green'), Tuple2(2, 'blue'), Tuple2(3, 'alpha')]));
+    expect(
+        indexZipped,
+        IList([
+          (0, 'red'),
+          (1, 'green'),
+          (2, 'blue'),
+          (3, 'alpha'),
+        ]));
   });
 
   test("Zip with another source of same or different length ignoring the longer Iterable", () {
@@ -1522,32 +1528,32 @@ void main() {
     final countries = ['France', 'Germany', 'Brazil', 'Japan'].lock;
     final capitals = ['Paris', 'Berlin', 'Brasilia', 'Tokyo'].lock;
 
-    final Iterable<Tuple2> zipped = countries.zip(capitals);
+    final Iterable<(String, String)> zipped = countries.zip(capitals);
     expect(
         zipped,
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
-          Tuple2('Brazil', 'Brasilia'),
-          Tuple2('Japan', 'Tokyo')
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
+          ('Brazil', 'Brasilia'),
+          ('Japan', 'Tokyo')
         ]));
 
     // Ignore Brazil Japan
-    final Iterable<Tuple2> subIn = countries.take(2).toIList().zip(capitals);
+    final Iterable<(String, String)> subIn = countries.take(2).toIList().zip(capitals);
     expect(
         subIn,
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
         ]));
 
     // Ignore Brazil Japan
-    final Iterable<Tuple2> subOut = countries.zip(capitals.take(2));
+    final Iterable<(String, String)> subOut = countries.zip(capitals.take(2));
     expect(
         subOut,
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
         ]));
   });
 
@@ -1559,19 +1565,19 @@ void main() {
     expect(
         countries.zipAll(capitals),
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
-          Tuple2('Brazil', 'Brasilia'),
-          Tuple2('Japan', 'Tokyo')
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
+          ('Brazil', 'Brasilia'),
+          ('Japan', 'Tokyo')
         ]));
 
     expect(
         countries.zipAll(capitals.take(2)),
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
-          Tuple2('Brazil', null),
-          Tuple2('Japan', null)
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
+          ('Brazil', null),
+          ('Japan', null),
         ]));
 
     expect(
@@ -1580,10 +1586,10 @@ void main() {
               currentFill: (idx) => 'Country $idx',
             ),
         IList([
-          Tuple2('France', 'Paris'),
-          Tuple2('Germany', 'Berlin'),
-          Tuple2('Country 2', 'Brasilia'),
-          Tuple2('Country 3', 'Tokyo')
+          ('France', 'Paris'),
+          ('Germany', 'Berlin'),
+          ('Country 2', 'Brasilia'),
+          ('Country 3', 'Tokyo')
         ]));
 
     expect(
@@ -1592,15 +1598,15 @@ void main() {
           otherFill: (idx) => (idx + 1) * 100,
         ),
         IList([
-          Tuple2('France', 100),
-          Tuple2('Germany', 200),
-          Tuple2('Brazil', 300),
-          Tuple2('Japan', 400)
+          ('France', 100),
+          ('Germany', 200),
+          ('Brazil', 300),
+          ('Japan', 400),
         ]));
 
     final unzipped = countries.zipAll(capitals).unzip();
-    expect(unzipped.first.toIList(), countries);
-    expect(unzipped.second.toIList(), capitals);
+    expect(unzipped.$1.toIList(), countries);
+    expect(unzipped.$2.toIList(), capitals);
   });
 
   test("Iterate apply ops n times to create a list", () {
@@ -1615,8 +1621,8 @@ void main() {
     final base = [1, 2, 3, 4, 5, 6, 7, 8, 9].lock;
     final split = base.splitAt(4);
 
-    expect(split.first.toIList(), base.sublist(0, 4));
-    expect(split.second.toIList(), base.sublist(4));
+    expect(split.$1.toIList(), base.sublist(0, 4));
+    expect(split.$2.toIList(), base.sublist(4));
   });
 
   test("Count on predicates", () {