ms-ati/rumonade

View on GitHub
lib/rumonade/option.rb

Summary

Maintainability
A
25 mins
Test Coverage
require 'singleton'
require 'rumonade/monad'

module Rumonade # :nodoc:
  # Represents optional values. Instances of Option are either an instance of Some or the object None.
  #
  # The most idiomatic way to use an Option instance is to treat it as a collection or monad
  # and use map, flat_map, select, or each:
  #
  #   name = Option(params[:name])
  #   upper = name.map(&:strip).select { |s| s.length != 0 }.map(&:upcase)
  #   puts upper.get_or_else("")
  #
  # Note that this is equivalent to
  #
  #   # TODO: IMPLEMENT FOR COMPREHENSIONS
  #   # see http://stackoverflow.com/questions/1052476/can-someone-explain-scalas-yield
  #   val upper = for {
  #     name    <- Option(params[:name])
  #     trimmed <- Some(name.strip)
  #     upper   <- Some(trimmed.upcase) if trimmed.length != 0
  #   } yield upper
  #   puts upper.get_or_else("")
  #
  # Because of how for comprehension works, if None is returned from params#[], the entire expression results in None
  # This allows for sophisticated chaining of Option values without having to check for the existence of a value.
  #
  # A less-idiomatic way to use Option values is via direct comparison:
  #
  #   name_opt = params[:name]
  #   case name_opt
  #     when Some
  #       puts name_opt.get.strip.upcase
  #     when None
  #       puts "No name value"
  #   end
  #
  # @abstract
  class Option
    class << self
      # @return [Option] Returns an +Option+ containing the given value
      def unit(value)
        Rumonade.Some(value)
      end

      # @return [Option] Returns the empty +Option+
      def empty
        None
      end
    end

    def initialize
      raise(TypeError, "class Option is abstract; cannot be instantiated") if self.class == Option
    end
    private :initialize

    # Returns None if None, or the result of executing the given block or lambda on the contents if Some
    def bind(lam = nil, &blk)
      empty? ? self : (lam || blk).call(value)
    end

    include Monad

    # @return [Boolean] Returns +true+ if +None+, +false+ if +Some+
    def empty?
      raise(NotImplementedError)
    end

    # Returns contents if Some, or raises NoSuchElementError if None
    def get
      if !empty? then value else raise NoSuchElementError end
    end

    # Returns contents if Some, or given value or result of given block or lambda if None
    def get_or_else(val_or_lam = nil, &blk)
      v_or_f = val_or_lam || blk
      if !empty? then value else (v_or_f.respond_to?(:call) ? v_or_f.call : v_or_f) end
    end

    # Returns contents if Some, or +nil+ if None
    def or_nil
      get_or_else(nil)
    end
  end

  # Represents an Option containing a value
  class Some < Option
    def initialize(value)
      @value = value
    end

    attr_reader :value # :nodoc:

    # @return (see Option#empty?)
    def empty?
      false
    end

    def ==(other)
      other.is_a?(Some) && other.value == value
    end

    def to_s
      "Some(#{value.nil? ? 'nil' : value.to_s})"
    end
  end

  # Represents an Option which is empty, accessed via the constant None
  class NoneClass < Option
    include Singleton

    # @return (see Option#empty?)
    def empty?
      true
    end

    def ==(other)
      other.equal?(self.class.instance)
    end

    def to_s
      "None"
    end
  end

  # Returns an Option wrapping the given value: Some if non-nil, None if nil
  def Option(value)
    value.nil? ? None : Some(value)
  end

  # @return [Some] Returns a +Some+ wrapping the given value, for convenience
  def Some(value)
    Some.new(value)
  end

  # The single global instance of NoneClass, representing the empty Option
  None = NoneClass.instance # :doc:

  module_function :Option, :Some
  public :Option, :Some
end