simc / dartx

Superpowers for Dart. Collection of useful static extension methods.
https://pub.dev/packages/dartx
Apache License 2.0
1.08k stars 88 forks source link

Offering my switchCase Extension #80

Open Skquark opened 4 years ago

Skquark commented 4 years ago

I just adapted a little utility I've been using to work as extension, and I think it'd fit in well with your library. It does the equivalent of switch(string) { case "txt1": etc, only cleaner and easier inside a widget structure, or wherever you'd use a standard switch case break default statement.. Here it is:

extension switchString on String {
  /// Performs a switch-case statement on String using a map for cases & conditions, optional default value 
  /// ```dart
  /// child: roomType.switchCase({
  ///   "public": Text("It's public"),
  ///   "private": Text("It's private"),
  ///   "custom": () { ... },
  /// }, Text("Default type")),
  /// ```
  TValue switchCase<String, TValue>(Map<String, TValue> cases, [TValue defaultValue]) {
    if (cases.containsKey(this)) return cases[this];
    return defaultValue;
  }
}

extension switchInt on int {
  /// Performs a switch-case statement on int using a map for cases & conditions, optional default value 
  /// ```dart
  /// child: typeNumber.switchCase({
  ///   1: Text("It's one"),
  ///   2: () { ... },
  ///   3: Text("It's three"),
  /// }, Text("Other number")),
  /// ```
  TValue switchCase<int, TValue>(Map<int, TValue> cases, [TValue defaultValue]) {
    if (cases.containsKey(this)) return cases[this];
    return defaultValue;
  }
}

Simple and fairly elegant code with the optional default: parameter, I could give you the original generic TOptionType version as well (I didn't write it), but it's nicer as extension on String and int and maybe another type too. Feel free to add it in, or if you prefer, I can add it in myself with a PR with doc updates. I'm just loving the flexibility of Dart now, so many new toys to play with...

passsy commented 4 years ago

I found myself using this a few times. Although I think pattern matching or at least a switch case with return type will land in Dart in the next 2 years, this can work as a neat temporary solution.

I'd like to hear your opinion on a slightly different API. When I use this pattern I use Functions as my cases. The trailing braces () in your API don't look nice. I'm also not 100% satisfied with the default argument. I wish it would be a named argument but as default is a reserved word, that's kind of impossible.

Original

// Original Proposal
_counter.switchCase({
      1: Text("It's one"),
      2: Text("It's two"),
      3: Text("It's three"),
    }) ??
    Text("Other number"),

// Original Proposal with default
_counter.switchCase(
  {
    1: Text("It's one"),
    2: Text("It's two"),
    3: Text("It's three"),
  },
  Text("Other number"),
),

// Original Proposal with functions
_counter.switchCase(
  {
    1: () => Text("It's one"),
    2: () => Text("It's two"),
    3: () => Text("It's three"),
  },
  () => Text("Other number"),
)(),

Proposal 2

// Proposal 2 - forced functions
_counter.switchCase2({
      1: () => Text("It's one"),
      2: () => Text("It's two"),
      3: () => Text("It's three"),
    }) ??
    Text("Other number"),

// Proposal 2 with default
_counter.switchCase2(
  {
    1: () => Text("It's one"),
    2: () => Text("It's two"),
    3: () => Text("It's three"),
  },
  fallback: () => Text("Other number"),
),

Proposal 3

// Proposal 3 - match or error
_counter.switchCase3({
  0: () => Text("zero"),
  1: () => Text("It's one"),
  2: () => Text("It's two"),
  3: () => Text("It's three"),
}).match(),

// Proposal 3 - no default, but null (dartfmt 😖)
_counter.switchCase3({
      1: () => Text("It's one"),
      2: () => Text("It's two"),
      3: () => Text("It's three"),
    }).matchOrNull() ??
    Text("Other number"),

// Proposal 3 with explicit default
_counter.switchCase3({
  1: () => Text("It's one"),
  2: () => Text("It's two"),
  3: () => Text("It's three"),
}).defaultTo(() => Text("Other number")),

Implementation

Not that interesting, and very likely to change.

extension switchInt on int {
  T switchCase<int, T>(Map<int, T> cases, [T defaultValue]) {
    if (cases.containsKey(this)) return cases[this];
    return defaultValue;
  }

  T switchCase2<int, T>(Map<int, T Function()> cases, {T Function() fallback}) {
    if (cases.containsKey(this)) return cases[this]();
    if (fallback == null) return null;
    return fallback();
  }

  SwitchCase<T> switchCase3<int, T>(Map<int, T Function()> cases) {
    if (cases.containsKey(this)) return SwitchCase.match(cases[this]());
    return SwitchCase.noMatch();
  }
}

class SwitchCase<T> {
  SwitchCase.match(this.value) : matched = true;
  SwitchCase.noMatch()
      : matched = false,
        value = null;

  final T value;
  final bool matched;

  T match() {
    if (matched) return value;
    throw "no default argument";
  }

  T matchOrNull() {
    if (matched) return value;
    return null;
  }

  T defaultTo(T Function() fallback) {
    if (!matched) return fallback();
    return value;
  }
}

Does someone have more ideas?

Skquark commented 4 years ago

Hmm, I like some of your implementations, but not sure if those proposals really improve it's ease of use over the original. The goal would be as little typing as possible, without extra fields needed, and adding the additional syntax for the optional defaultValue seems to complicate it visually. I agree about there being another way to do the switch case more integrated into the Dart language in the way that the if's are like isTrue ? true : false with an inline method and some creative syntax (I've tried to visualize the best format of that for years), but until then this is a nice simple shortcut method. You could include more than one implementation method so users can pick which switch they prefer for their case. If I had to choose, I liked Proposal 2 with default Maybe there's another simple option too, I'm thinking about it but your call what goes in your package..