DEV Community

Mykhailo Krainik
Mykhailo Krainik

Posted on

How to Implement Rust’s Result Type in Dart Programming Language.

According to the Stack Overflow survey, the Rust programming language has incorporated the best practices from both functional and imperative programming paradigms, drawing inspiration from languages like OCaml and Haskell. Notably, Rust’s use of the Either type sets it apart from modern languages such as Java, Python, Ruby, and Dart, which do not commonly employ this construct.

While external libraries are available to achieve similar functionality, I suggest developing a simple alternative for the Result type.

We will make use of the powerful generic functionality in Dart’s class system to achieve this goal.

class Result<T, E> {
  T? value;
  E? error;

  Result.Ok(T value) {
    this.value = value;
  }

  Result.Err(E error) {
    this.error = error;
  }
}
Enter fullscreen mode Exit fullscreen mode

After implementing the Result class with the ability to handle errors, we can test it to ensure it functions as expected. The Result class consists of a generic type T and an error type E, and the Ok and Err methods are used to set the value and error types, respectively. With this class in place, we can handle errors more effectively and improve our code’s reliability.

This looks very straightforward, and in the next step, we will test this class.

To find the required element in the collection, we require a wrapper object along with a method called getRecordById. In this regard, we have implemented the RecordStore class. This class also includes the method that takes an integer searchId as input and returns a Result object. If the record is found in the collection, Result.Ok(record) is returned, else Result.Err(‘NotFound’) is returned.

class Record {
  int _id;

  get getId => _id;

  Record({required id}) : _id = id;

  String toString() => 'Record(id: $_id)';
}

class RecordStore {
  final _records;

  RecordStore({required records}) : _records = records;   

  Result<Record, String> getRecordById(int searchId) {
    for (final record in _records) {
        if (record.getId == searchId) return Result.Ok(record);
    }
    return Result.Err('NotFound');
  }
}
Enter fullscreen mode Exit fullscreen mode

We can now test the implementation by attempting to retrieve the value for ID 2.

void main() {
  final store = RecordStore(
      records: List<Record>.generate(10, (genNum) => Record(id: genNum)));
  final result = store.getRecordById(2);

  if (result.value != null) {
    print(result.value.toString());
  } else {
    print(result.error);
  }
}
Enter fullscreen mode Exit fullscreen mode

The output of the line print(result.value.toString()); was Record(id: 2). This is because the getRecordById method of the RecordStore class returns a Result type. If the record is found, we get Result.Ok(record), and if not, we return Result.Err(‘NotFound’). This approach is better than returning a nullable object like Record?, as it provides a more explicit way of handling errors

Suppose we modified getRecordById to return errors as an Error enum type instead of a String. The updated code would look like this:

enum Error {
  recordNotFound,
  notValidId,
}

class RecordStore {
  final _records;

  RecordStore({required records}) : _records = records;

  Result<Record, Error> getRecordById(int searchId) {
    if (searchId <= 0) return Result.Err(Error.notValidId);

    for (final record in _records) {
      if (record.getId == searchId) return Result.Ok(record);
    }
    return Result.Err(Error.recordNotFound);
  }
}
Enter fullscreen mode Exit fullscreen mode

In the previous implementation, we used the Error enum to represent different types of errors that can occur when searching for a record. With the updated implementation, the getRecordById method returns a Result type. If the record is not found, Result.Err(error) is returned to indicate that an error occurred. The specific error type returned can be either Error.notValidId or Error.recordNotFound, depending on the situation. When we attempt to call store.getRecordById(-2), we receive an Error.notValidId error if the ID is invalid or an Error.recordNotFound if the record is not found. This approach can be helpful as we can use an Error enum with a switch statement to ensure that all the elements of the enum are considered in the condition

I believe we can proceed with the implementation of the following code:

result.then(ok: (record) {
    print(record.toString());
  }, err: (error) {
    switch (error) {
      case Error.recordNotFound:
        print("Nothing to display at this time.");
        break;
      case Error.notValidId:
        print(
            "Invalid ID provided. Please enter a positive integer value for the record ID.");
    }
  });
Enter fullscreen mode Exit fullscreen mode

This code snippet prints the record details if it exists, and prints an appropriate error message if it does not. We use a switch statement to handle the possible error cases and provide relevant feedback to the user.

We can modify the Result class to enhance its functionality. The updated class would have the following structure:

class Result<T, E> {
  T? value;
  E? error;

  Result.Ok(T value) {
    this.value = value;
  }

  Result.Err(E error) {
    this.error = error;
  }

  dynamic then({required Function ok, required Function err}) {
    if (this.value != null) {
      return ok(value);
    }
    if (this.error != null) {
      return err(error);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

With these changes, we can create instances of the Result class that can hold both a value and an error. The then method can be used to handle both cases by taking in two functions: one to handle the successful case (ok) and another to handle the error case (err). If a value is present, the ok function will be called with the value as its argument. If an error is present, the err function will be called with the error as its argument.

Here are all the code samples provided for this implementation.

enum Error {
  recordNotFound,
  notValidId,
}

class Result<T, E> {
  T? value;
  E? error;

  Result.Ok(T value) {
    this.value = value;
  }

  Result.Err(E error) {
    this.error = error;
  }

  dynamic then({required Function ok, required Function err}) {
    if (this.value != null) {
      return ok(value);
    }
    if (this.error != null) {
      return err(error);
    }
  }
}

class Record {
  int _id;

  get getId => _id;

  Record({required id}) : _id = id;

  String toString() => 'Record(id: $_id)';
}

class RecordStore {
  final _records;

  RecordStore({required records}) : _records = records;

  Result<Record, Error> getRecordById(int searchId) {
    if (searchId <= 0) return Result.Err(Error.notValidId);

    for (final record in _records) {
      if (record.getId == searchId) return Result.Ok(record);
    }
    return Result.Err(Error.recordNotFound);
  }
}

void main() {
  final store = RecordStore(
      records: List<Record>.generate(10, (genNum) => Record(id: genNum)));
  final result = store.getRecordById(-4);

  result.then(ok: (record) {
    print(record.toString());
  }, err: (error) {
    switch (error) {
      case Error.recordNotFound:
        print("Nothing to display at this time.");
        break;
      case Error.notValidId:
        print(
            "Invalid ID provided. Please enter a positive integer value for the record ID.");
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

In conclusion, we have seen how Rust’s Result type can be implemented in Dart to handle errors and return values in a cleaner and more concise way. By creating a custom Result class, we were able to use it to represent the success and failure of operations in a type-safe manner. We also explored how using an Error enum to represent different types of errors can make code more robust and easier to maintain. With these tools in hand, Dart developers can create more reliable and efficient code that is less prone to errors and easier support.

Top comments (0)