result_monad 3.0.0 copy "result_monad: ^3.0.0" to clipboard
result_monad: ^3.0.0 copied to clipboard

A Dart implementation of the Result Monad which allows for more expressive result generation and processing without using exceptions.

example/example.md

Examples #

The project ships with examples which you can find here.

Simple Usage #

The simplest usage begins with defining the function return to be a Result Monad. The monad takes two types, the value type for when it succeeds and an error type for if it fails. The value type is always whatever the natural return of the function would be. The error type can be literally anything. For example:

import 'package:result_monad/result_monad.dart';

Result<double, String> invert(double value) {
  if (value == 0) {
    return Error('Cannot invert zero');
  }

  return Ok(1.0 / value);
}

void main() {
  // Prints 'Inverse is: 0.5'
  invert(2).match(
      onSuccess: (value) => print("Inverse is: $value"),
      onError: (error) => print(error));

  // Prints 'Cannot invert zero'
  invert(0).match(
      onSuccess: (value) => print("Inverse is: $value"),
      onError: (error) => print(error));
}

Chaining Calls and Error Propagation #

The power of monads comes in the ability to do a call sequence where the results of one permutation is passed to the next in the call chain, assuming it was successful. This allows a developer to not worry about continually checking error conditions but instead focus on what the next steps are if something succeeded. The Result Monad library takes care of stopping evaluation at the point of error. For example:

import 'package:result_monad/result_monad.dart';

Result<num, dynamic> safeSqrt(num value) {
  if (value < 0) {
    return Error('Value cannot be negative', StackTrace.current);
  }

  return Ok(sqrt(value));
}

Result doCalculation(num value) {
  return Ok(value)
      .transform((v) => pow(v, 3))
      .andThen<num, String>((v) =>
  v >= 0
      ? Ok(sqrt(v))
      : Error('Value cannot be negative', StackTrace.current))
      .andThen(safeSqrt)
      .transform((v) => v / 2);
}

void main() {
  // Works because a non-negative number
  print('f(500)=${doCalculation(500)}\n');

  //Doesn't work because a negative number
  print('f(-500)=${doCalculation(-500)}\n');
}

In this example we have a doCalculation method that does a series of transformations of a passed in value. First it cubes the number, then it tries to take the square root a couple of times. Finally, it divides it in half. The Dart sqrt function returns a NaN value rather than throwing an exception if the number is negative. For this code here we check if the value is negative first and use that to determine whether we are returning an Ok or Error object. The Error object has an optional second argument of a stack trace. This example shows how to add the current stack track to the Error object. This can help with debugging. This example shows the same code implemented twice, once as an anonymous function and once as method that is passed into the andThen stage. Because some steps can get very large it is sometimes helpful to have them written as functions passed in rather than as a big anonymous function blob.

Another thing you may have noticed was that we use transform in some places and andThen in others. These are similar functions but perform differently. A transform function is passed a value. It is expected to return a raw value as well. The andThen method is also passed a value. It however is expected to return a Result object wrapping a value or an error. It is therefore best to use the transform function when you are expecting it to always be able to evaluate based on your available logic. It is better to use andThen if you are going to be looking at the value to determine if you should be returing a result or an error. In all cases if the code throws an exception then that will be wrapped in an Error object with a stack trace.

Looking at our above example, the code is called with the argument 500 it executes all three stages and returns an Ok object with the final v/2 transformation. The code called with the argument -500 however is going to get tripped in the andThen where the first sqrt evaluation occurs. Looking at the output of this program you can see the successfully completed computation and the one that ended in an error and the stack trace to see where it failed. For this simple example it may be pretty obvious where it failed however for more complicated examples the stack trace can be helpful.

f(500)=ok(5590.169943749474)

f(-500)=error(Value cannot be negative)
Stack trace:
#0      doCalculation.<anonymous closure> (file:///home/user1/dev/dart-result-monad/example/simple_chaining_example.dart:10:58)
#1      Result.andThen (package:result_monad/src/result_monad_base.dart:77:28)
#2      doCalculation (file:///home/user1/dev/dart-result-monad/example/simple_chaining_example.dart:8:8)
#3      main (file:///home/user1/dev/dart-result-monad/example/simple_chaining_example.dart:19:20)
#4      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#5      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12

Using Error Objects #

For more complex cases you may have a reusable error object type that can be passed around. In the above example that could be as simple as:

import 'dart:math' as math;

import 'package:result_monad/result_monad.dart';

enum MathError {
  divideByZero,
  undefinedResult,
}

Result<double, MathError> invert(double value) {
  if (value == 0) {
    return Error(MathError.divideByZero);
  }

  return Ok(1.0 / value);
}

Result<double, MathError> sqrt(double x) {
  if (x < 0) {
    return Error(MathError.undefinedResult, StackTrace.current);
  }

  return Ok(math.sqrt(x));
}

void main() {
  // Prints 'Inverse is: 0.5'
  invert(2).match(
    onSuccess: (value) => print("Inverse is: $value"),
    onError: (error, stackTrace) => print(error),
  );

  // Prints 'Cannot invert zero'
  invert(0).match(
    onSuccess: (value) => print("Inverse is: $value"),
    onError: (error, stackTrace) => print(error),
  );

  sqrt(4).match(
    onSuccess: (value) => print("Sqrt is: $value"),
    onError: (error, stackTrace) => print(error),
  );

  sqrt(-1)
      .withError(
          (error, stackTrace) => print('Stack trace for error: $stackTrace'))
      .match(
    onSuccess: (value) => print("Sqrt is: $value"),
    onError: (error, _) => print('Error calculating sqrt(-1): $error'),
  );
}

Putting It All Together #

For cases where you use the runCatching wrapper or if an exception is thrown during the evaluation of a transform or andThen method, et cetera, then the Error Result Monad will contain the Exception and the stack track from the point the exception was thrown. For example this code with a parsing error:

print(Ok('hello').transform((v) => int.parse(v)));

Would output:

error(FormatException: Invalid radix-10 number (at character 1)
hello
^
)
Stack trace:
#0      int._handleFormatError (dart:core-patch/integers_patch.dart:150:5)
#1      int._parseRadix (dart:core-patch/integers_patch.dart:179:14)
#2      int._parse (dart:core-patch/integers_patch.dart:121:12)
#3      int.parse (dart:core-patch/integers_patch.dart:81:12)
#4      main.<anonymous closure> (file:///home/user1/dev/dart-result-monad/example/simple_chaining_example.dart:24:42)
#5      Result.transform (package:result_monad/src/result_monad_base.dart:101:31)
#6      main (file:///home/user1/dev/dart-result-monad/example/simple_chaining_example.dart:24:21)
#7      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:314:19)
#8      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:193:12)

In the above previous cases we aren't directly querying if a Monad held a result or an error but instead using the match method to have two different code paths depending on whether we are dealing with a succeeded or failed result. The below example shows how to use all of the above in a more real world example. In it we also show using methods fold, withError, and withResult to accomplish to interact with and tranform Result Monads generally at the end of a chain. Seethe API documentation for more details.

import 'dart:io';

import 'package:result_monad/result_monad.dart';

enum ErrorEnum { environment, fileAccess }

void main(List<String> arguments) {
  final stringToWrite = 'Data written to the temp file ${DateTime.now()}';
  final tmpFileResult = getTempFile()
      .withResult((file) => print('Temp file: ${file.path}'))
      .withError(
          (error, trace) => print('Error getting temp file: $error\n$trace'));

  // Probably would check if failure and stop here normally but want to show
  // that even starting with an error Result Monad flows correctly.
  final writtenSuccessfully = tmpFileResult
      .withResult((file) => file.writeAsStringSync(stringToWrite))
      .transform((file) => file.readAsStringSync())
      .fold(
      onSuccess: (text) => text == stringToWrite,
      onError: (_, __) => false);

  print('Successfully wrote to temp file? $writtenSuccessfully');
}

Result<File, ErrorEnum> getTempFile({String prefix = '', String suffix = '.tmp'}) {
  final tmpName = '$prefix${DateTime
      .now()
      .millisecondsSinceEpoch}$suffix';
  return getTempFolder()
      .transform((tempFolder) => '$tempFolder${Platform.pathSeparator}$tmpName')
      .transform((tmpPath) => File(tmpPath))
      .mapError((error) => error is ErrorEnum ? error : ErrorEnum.fileAccess);
}

Result<String, ErrorEnum> getTempFolder() {
  String folderName = '';
  if (Platform.isMacOS || Platform.isWindows) {
    final varName = Platform.isMacOS ? 'TMPDIR' : 'TEMP';
    final tempDirPathFromEnv = Platform.environment[varName];
    if (tempDirPathFromEnv != null) {
      folderName = tempDirPathFromEnv;
    } else {
      return Error(ErrorEnum.environment);
    }
  } else if (Platform.isLinux) {
    folderName = '/tmp';
  }

  if (folderName.isEmpty) {
    return Error(ErrorEnum.environment);
  }

  final Result<bool, dynamic> canWriteResult = runCatching(() {
    if (!Directory(folderName).existsSync()) {
      return Ok(false);
    }

    final testFilePath =
        '$folderName${Platform.pathSeparator}${DateTime
        .now()
        .millisecondsSinceEpoch}.tmp';
    final tmpFile = File(testFilePath);
    tmpFile.writeAsStringSync('test');
    tmpFile.deleteSync();

    return Ok(true);
  });

  return canWriteResult
      .andThen<String, ErrorEnum>(
          (canWrite) => canWrite ? Ok(folderName) : Error(ErrorEnum.fileAccess))
      .mapError((_) => ErrorEnum.fileAccess);
}
7
likes
0
points
2.04k
downloads

Publisher

verified publishermyportal.social

Weekly Downloads

A Dart implementation of the Result Monad which allows for more expressive result generation and processing without using exceptions.

Repository (GitLab)
View/report issues

License

unknown (license)

More

Packages that depend on result_monad