From 6f5ce0e9311b57470af25cd0e811c0c7d89de1d9 Mon Sep 17 00:00:00 2001 From: Cameron Dutro Date: Mon, 10 Mar 2014 12:12:52 -0700 Subject: [PATCH] Adding scala-style matching --- lib/option.rb | 78 ++++++++++++++++++++++++++++++++++++++++++++- spec/option_spec.rb | 53 ++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) diff --git a/lib/option.rb b/lib/option.rb index 7848bf6..12a9643 100644 --- a/lib/option.rb +++ b/lib/option.rb @@ -1,8 +1,75 @@ +module OptionHelpers + class OptionMatcher + attr_reader :return_value + + def initialize(option) + @option = option + @return_value = nil + end + + def case(type) + if type.is_a?(OptionType) + if @option.present? + option_val = @option.get + if option_val.is_a?(type.type) + @return_value = yield(option_val) + end + end + elsif type == SomeClass + if @option.present? + @return_value = yield(@option.get) + end + elsif type.is_a?(NoneClass) + if !@option.present? + @return_value = yield(@option) + end + else + raise TypeError, "can't match an Option against a #{type.to_s}" + end + end + + def else + @return_value = yield(@option) + end + end + + class OptionType + attr_reader :type + + def initialize(type) + @type = type + end + + class << self + def for_class(klass) + case klass + when Class + option_type_cache[klass] ||= OptionType.new(klass) + else + raise TypeError, "Must be a Class" + end + end + + private + + def option_type_cache + @option_type_cache ||= {} + end + end + end +end + class OptionClass def or_nil end + class << self + def [](type) + OptionHelpers::OptionType.for_class(type) + end + end + def ==(that) case that when OptionClass then or_nil == that.or_nil @@ -10,6 +77,12 @@ def ==(that) end end + def match + matcher = OptionHelpers::OptionMatcher.new(self) + yield matcher + matcher.return_value + end + private def assert_option(result) @@ -102,6 +175,9 @@ def error(*argv) end class NoneClass < OptionClass + def [](type) + raise TypeError, "can't specify a type of NoneClass" + end def dup raise TypeError, "can't dup NoneClass" @@ -187,6 +263,6 @@ def Some(value) SomeClass.new(value) end -def Option(value) +def Option(value=nil) value.nil? ? None : Some(value) end diff --git a/spec/option_spec.rb b/spec/option_spec.rb index 11ef91a..35b7222 100644 --- a/spec/option_spec.rb +++ b/spec/option_spec.rb @@ -102,6 +102,10 @@ it "should assemble an Error from the arguments passed in" do lambda { None.error(StandardError, "this is a problem") }.must_raise StandardError, "this is a problem" end + + it "cannot be typed" do + lambda { None[String] }.must_raise TypeError, "can't specify a type of NoneClass" + end end describe SomeClass do @@ -212,6 +216,16 @@ value = !!(Some(value).error("error") rescue false) value.must_equal true end + + it "can be typed" do + opt = Some[String] + opt.class.must_equal OptionHelpers::OptionType + opt.type.must_equal String + end + + it "must be typed using a class, not a value" do + lambda { Some[5] }.must_raise TypeError, "Must be a Class" + end end describe OptionClass do @@ -227,4 +241,43 @@ it "should do equality checks against the boxed value" do Option(value).must_equal(value) end + + it "allows matching typed options" do + Option(5).match do |matcher| + matcher.case(Some[Fixnum]) { |val| val * 2 } + matcher.case(Some[String]) { |val| "bar" } + end.must_equal 10 + + Option("foo").match do |matcher| + matcher.case(Some[Fixnum]) { |val| val * 2 } + matcher.case(Some[String]) { |val| "bar" } + end.must_equal "bar" + end + + it "allows matching untyped options" do + Option(5).match do |matcher| + matcher.case(Some) { |val| val * 2 } + end.must_equal 10 + end + + it "allows matching on none" do + None.match do |matcher| + matcher.case(None) { "none" } + end.must_equal "none" + end + + it "allows matching with an else block" do + Option(5).match do |matcher| + matcher.case(Some[String]) { |val| "bar" } + matcher.else { |val| val.get * 2} + end.must_equal 10 + end + + it "does not allow matching against non-option types" do + lambda do + Option(5).match do |matcher| + matcher.case(Fixnum) { 1 } + end + end.must_raise TypeError, "can't match an Option against a Fixnum" + end end