The Option Pattern

Jay Bienvenu

NetShapers

October 2018

Disclaimers

This description of the Option pattern (or what I am calling the Option pattern) is my own. Descriptions of this pattern may differ in literature.

Code shown herein is intentionally simplified to facilitate this presentation. Real-world code may have additional complexity not shown in this presentation.

The Objective

 Isolate operations, especially unstable ones,
into small objects backed by robust testing.

Getting Started

  • Wrap a data value in a class with one method, get().
  • Create a parallel class that represents
    null or no value.
  • Create a factory that takes a value and returns the appropriate Option class.
interface Option 
{
  public function get();
}

class ScalarOption implements Option {
  private $_value;
  public function __construct($value) { $this->_value = $value; }
  public function get() { return $this->_value; }
}

class NullOption implements Option {
  public function get() { throw new \Exception('No value.'); }
}

abstract class OptionFactory {
  public static function fromValue($value) {
    return is_null($value) ? new NullOption() : new ScalarOption($value);
  }
}

Add get() functionality

  • Return a fallback  value.
  • Run a callable/closure.
  • Throw our own exception.
interface Option
{
  public function get();
  public function getOrElse($fallback);
  public function getOrCall($callable);
  public function getOrThrow($exception);  
}

class ScalarOption implements Option {
  // ...
  public function get() { return $this->_value; }
  public function getOrElse() { return $this->_value; }
  public function getOrCall($callable) { return $this->_value; }
  public function getOrThrow() { return $this->_value; }
}

class NullOption implements Option {
  // ...
  public function get() { throw new \Exception('No value.'); }
  public function getOrElse($fallback) { return $fallback; }
  public function getOrCall($callable) { return $callable(); }
  public function getOrThrow($exception) { throw $exception; }
}
// Before:
if (!is_null($value)) {
  return $value;
} else {
  return do_something_else();
}

// After:
return $value_option->getOrCall(do_something_else);

Add some useful methods

interface Option
{
  // ...
  public function hasValue(); 
}

class ScalarOption implements Option {
  // ...
  public function hasValue() { return true; }
}

class NullOption implements Option {
  // ...
  public function hasValue() { return false; }
}

Replace null option with another option

interface Option
{
  public function orElse(Option $else);
}

class ScalarOption implements Option {
  public function orElse(Option $else) { return $this; }
}

class NullOption implements Option {
  public function orElse(Option $else) { return $else; }
}
// Before:
$x = $repo->findSomething();
if (is_null($x)) {
  $x = $repo->findSomethingElse();
  if (is_null($x)) {
    $x = $repo->createSomething();
  }
}
// After: (bad idea; we'll fix it shortly)
$x = $repo->findSomethingReturnOption()
->orElse($repo->findSomethingElseReturnOption())
->orElse($repo->createSomethingReturnOption())
->get();

Being Lazy

An Option that specifies a deferred action.

class LazyOption implements Option {

  private $_callback;
  private $_arguments;
  
  public function __construct($callback, array $arguments = []) {
    $this->_callback = $callback;
    $this->_arguments = $arguments;
  }

  private function option()
  {
      $option = call_user_func_array($this->callback, $this->arguments);
      if ($option instanceof Option) return $option;
      return new NullOption(); 
  }
    
  public function get() { return $this->option()->get(); }
  public function getOrCall($callable) { return $this->option()->getOrCall($callable); }
  public function getOrElse($fallback) { return $this->option()->getOrElse($fallback); }
  public function getOrThrow($exception) { return $this->option()->getOrThrow($exception); }
  public function hasValue() { return $this->option()->hasValue(); }
  public function orElse(Option $else) { return $this->option()->orElse($else); }
}

Using LazyOption

// Bad:
$x = $repo->findSomethingReturnOption()
->orElse($repo->findSomethingElseReturnOption())
->orElse($repo->createSomethingReturnOption())
->get();

// Better:
$x = $repo->findSomethingReturnOption()
->orElse(new LazyOption([$repo,'findSomethingElseReturnOption']))
->orElse(new LazyOption([$repo,'createSomethingReturnOption']))
->get();

Becoming Manipulative

  • Transform the value & put it in a new Option.
  • Apply the value to a callable.
interface Option
{
  // ...
  /**
   * Applies callable to non-empty value.
   * Returns return value of callable wrapped in Option().
   * If option is empty, then callable is not applied.
   */
  public function map($callable) : Option;
  /**
   * Applies callable to non-empty value; returns return value of callable directly.
   * Expects callable to return an Option.
   */
  public function flatMap($callable) : Option;
  /**
   * Applies callable to non-empty value, but does not return value.
   */
  public function forAll($callable) : Option;
}

class ScalarOption implements Option {
  // ...
  public function map($callable)
  {
      return OptionFactory::fromValue(call_user_func($callable, $this->value));
  }
  public function flatMap($callable)
  {
      return call_user_func($callable, $this->value); // must return Option
  }
  public function forAll($callable)
  {
      return call_user_func($callable, $this->value); return $this;
  }
}

class NullOption implements Option {
  // ...
  public function map($callable) { return $this; }
  public function flatMap($callable) { return $this; }
  public function forAll($callable) { return $this; }
}

Questions

The Option Pattern

By Jay Bienvenu

The Option Pattern

  • 833