Class: Concurrent::Maybe — Concurrent Ruby

# Class: Concurrent::Maybe

Inherits:
  Synchronization::Object
  - Object
  - Synchronization::Object
  - Concurrent::Maybe
  show all

Includes:
  Comparable

Defined in:
  lib/concurrent/maybe.rb

## Overview

A `Maybe` encapsulates an optional value. A `Maybe` either contains a value of (represented as `Just`), or it is empty (represented as `Nothing`). Using `Maybe` is a good way to deal with errors or exceptional cases without resorting to drastic measures such as exceptions.

`Maybe` is a replacement for the use of `nil` with better type checking.

For compatibility with Concern::Obligation the predicate and accessor methods are aliased as `fulfilled?`, `rejected?`, `value`, and `reason`.

## Motivation

A common pattern in languages with pattern matching, such as Erlang and Haskell, is to return either a value or an error from a function Consider this Erlang code:

    case file:consult("data.dat") of
      {ok, Terms} -> do_something_useful(Terms);
      {error, Reason} -> lager:error(Reason)
    end.

In this example the standard library function `file:consult` returns a tuple with two elements: an atom (similar to a ruby symbol) and a variable containing ancillary data. On success it returns the atom `ok` and the data from the file. On failure it returns `error` and a string with an explanation of the problem. With this pattern there is no ambiguity regarding success or failure. If the file is empty the return value cannot be misinterpreted as an error. And when an error occurs the return value provides useful information.

In Ruby we tend to return `nil` when an error occurs or else we raise an exception. Both of these idioms are problematic. Returning `nil` is ambiguous because `nil` may also be a valid value. It also lacks information pertaining to the nature of the error. Raising an exception is both expensive and usurps the normal flow of control. All of these problems can be solved with the use of a `Maybe`.

A `Maybe` is unambiguous with regard to whether or not it contains a value. When `Just` it contains a value, when `Nothing` it does not. When `Just` the value it contains may be `nil`, which is perfectly valid. When `Nothing` the reason for the lack of a value is contained as well. The previous Erlang example can be duplicated in Ruby in a principled way by having functions return `Maybe` objects:

    result = MyFileUtils.consult("data.dat") # returns a Maybe
    if result.just?
      do_something_useful(result.value)      # or result.just
    else
      logger.error(result.reason)            # or result.nothing
    end

Examples:

Returning a Maybe from a Function

    module MyFileUtils
      def self.consult(path)
        file = File.open(path, 'r')
        Concurrent::Maybe.just(file.read)
      rescue => ex
        return Concurrent::Maybe.nothing(ex)
      ensure
        file.close if file
      end
    end

    maybe = MyFileUtils.consult('bogus.file')
    maybe.just?    #=> false
    maybe.nothing? #=> true
    maybe.reason   #=> #<Errno::ENOENT: No such file or directory @ rb_sysopen - bogus.file>

    maybe = MyFileUtils.consult('README.md')
    maybe.just?    #=> true
    maybe.nothing? #=> false
    maybe.value    #=> "# Concurrent Ruby\n[![Gem Version..."

Using Maybe with a Block

    result = Concurrent::Maybe.from do
      Client.find(10) # Client is an ActiveRecord model
    end

    # -- if the record was found
    result.just? #=> true
    result.value #=> #<Client id: 10, first_name: "Ryan">

    # -- if the record was not found
    result.just?  #=> false
    result.reason #=> ActiveRecord::RecordNotFound

Using Maybe with the Null Object Pattern

    # In a Rails controller...
    result = ClientService.new(10).find    # returns a Maybe
    render json: result.or(NullClient.new)

See Also:

- Haskell Data.Maybe
- PureScript Data.Maybe

## Constant Summary collapse

NONE =

Indicates that the given attribute has not been set. When `Just` the #nothing getter will return `NONE`. When `Nothing` the #just getter will return `NONE`.

      ::Object.new.freeze

## Instance Attribute Summary collapse

- #just ⇒ undocumented (also: #value) readonly

  The value of a `Maybe` when `Just`.

- #nothing ⇒ undocumented (also: #reason) readonly

  The reason for the `Maybe` when `Nothing`.

## Class Method Summary collapse

- .from(*args) {|args| ... } ⇒ Maybe

  Create a new `Maybe` using the given block.

- .just(value) ⇒ Maybe

  Create a new `Just` with the given value.

- .nothing(error = '') ⇒ Maybe

  Create a new `Nothing` with the given (optional) reason.

## Instance Method Summary collapse

- #<=>(other) ⇒ Integer

  Comparison operator.

- #just? ⇒ Boolean (also: #fulfilled?)

  Is this `Maybe` a `Just` (successfully fulfilled with a value)?.

- #nothing? ⇒ Boolean (also: #rejected?)

  Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?.

- #or(other) ⇒ Object

  Return either the value of self or the given default value.

## Instance Attribute Details

### #just ⇒ `undocumented` (readonly) Also known as: value

The value of a `Maybe` when `Just`. Will be `NONE` when `Nothing`.

    114
    115
    116
    # File 'lib/concurrent/maybe.rb', line 114

    def just
      @just
    end

### #nothing ⇒ `undocumented` (readonly) Also known as: reason

The reason for the `Maybe` when `Nothing`. Will be `NONE` when `Just`.

    117
    118
    119
    # File 'lib/concurrent/maybe.rb', line 117

    def nothing
      @nothing
    end

## Class Method Details

### .from(*args) {|args| ... } ⇒ `Maybe`

Create a new `Maybe` using the given block.

Runs the given block passing all function arguments to the block as block arguments. If the block runs to completion without raising an exception a new `Just` is created with the value set to the return value of the block. If the block raises an exception a new `Nothing` is created with the reason being set to the raised exception.

Parameters:

- args (`Array<Object>`) —

  Zero or more arguments to pass to the block.

Yields:

- The block from which to create a new `Maybe`.

Yield Parameters:

- args (`Array<Object>`) —

  Zero or more block arguments passed as arguments to the function.

Returns:

- (`Maybe`) —

  The newly created object.

Raises:

- (`ArgumentError`) —

  when no block given.

    137
    138
    139
    140
    141
    142
    143
    144
    145
    # File 'lib/concurrent/maybe.rb', line 137

    def self.from(*args)
      raise ArgumentError.new('no block given') unless block_given?
      begin
        value = yield(*args)
        return new(value, NONE)
      rescue => ex
        return new(NONE, ex)
      end
    end

### .just(value) ⇒ `Maybe`

Create a new `Just` with the given value.

Parameters:

- value (`Object`) —

  The value to set for the new `Maybe` object.

Returns:

- (`Maybe`) —

  The newly created object.

    152
    153
    154
    # File 'lib/concurrent/maybe.rb', line 152

    def self.just(value)
      return new(value, NONE)
    end

### .nothing(error = '') ⇒ `Maybe`

Create a new `Nothing` with the given (optional) reason.

Parameters:

- error (`Exception`) (defaults to: `''`) —

  The reason to set for the new `Maybe` object. When given a string a new `StandardError` will be created with the argument as the message. When no argument is given a new `StandardError` with an empty message will be created.

Returns:

- (`Maybe`) —

  The newly created object.

    164
    165
    166
    167
    168
    169
    170
    171
    # File 'lib/concurrent/maybe.rb', line 164

    def self.nothing(error = '')
      if error.is_a?(Exception)
        nothing = error
      else
        nothing = StandardError.new(error.to_s)
      end
      return new(NONE, nothing)
    end

## Instance Method Details

### #<=>(other) ⇒ `Integer`

Comparison operator.

Returns:

- (`Integer`) —

  0 if self and other are both `Nothing`; -1 if self is `Nothing` and other is `Just`; 1 if self is `Just` and other is nothing; `self.just <=> other.just` if both self and other are `Just`.

    199
    200
    201
    202
    203
    204
    205
    # File 'lib/concurrent/maybe.rb', line 199

    def <=>(other)
      if nothing?
        other.nothing? ? 0 : -1
      else
        other.nothing? ? 1 : just <=> other.just
      end
    end

### #just? ⇒ `Boolean` Also known as: fulfilled?

Is this `Maybe` a `Just` (successfully fulfilled with a value)?

Returns:

- (`Boolean`) —

  True if `Just` or false if `Nothing`.

    176
    177
    178
    # File 'lib/concurrent/maybe.rb', line 176

    def just?
      ! nothing?
    end

### #nothing? ⇒ `Boolean` Also known as: rejected?

Is this `Maybe` a `nothing` (rejected with an exception upon fulfillment)?

Returns:

- (`Boolean`) —

  True if `Nothing` or false if `Just`.

    184
    185
    186
    # File 'lib/concurrent/maybe.rb', line 184

    def nothing?
      @nothing != NONE
    end

### #or(other) ⇒ `Object`

Return either the value of self or the given default value.

Returns:

- (`Object`) —

  The value of self when `Just`; else the given default.

    210
    211
    212
    # File 'lib/concurrent/maybe.rb', line 210

    def or(other)
      just? ? just : other
    end