rubo_claus 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e4ddf83f73f07a31517416ef5bea818d7d574561
4
- data.tar.gz: 9940e9766b9af282e20d30f3d5f98102b2501a48
3
+ metadata.gz: bec5af93a59d08f56169f041625c4e7c67fbdd4e
4
+ data.tar.gz: c185d7213d32531ff43d5bd2f9ff9c7c0c1a6c74
5
5
  SHA512:
6
- metadata.gz: c7f8f1c381054d7de8a4bc3dcd212daba86dd996e658315f20bd599ff741df5aad5530fead18846f58b40fda772bd82e6f2971206512ec22173ac789623d972a
7
- data.tar.gz: 84bc8b619df55125bca97b0843d2e0bdb54ffe9dad803cce6c01fce7df958012047d5db61d58f4951c1437fe84c864940085304f35530a74973b689a8260bbb2
6
+ metadata.gz: 6b7d5d705d13430d5e29056c964b6c0b9fdb2c33e69a89fe2a8cbd0491ef0bb748811c03b35b8102587aa8242fdc04e635aeb1daa3885238f1455a6698f85a48
7
+ data.tar.gz: b3729d8789de8c9ad048f60786a7d56c3874d9085ce6e37a10ea35427d4fb9622800b52d7f4ad5189a0ef60770dffb5a3b5022c1ec7c0db3b181a74d7657bcdc
@@ -0,0 +1,4 @@
1
+ .ruby-version
2
+ *.gem
3
+
4
+ Gemfile.lock
@@ -0,0 +1,6 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ - 2.2.5
6
+ - 2.1.10
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify gem's dependencies in .gemspec
4
+ gemspec
data/README.md CHANGED
@@ -1,11 +1,154 @@
1
1
  # RuboClaus
2
2
 
3
- ## Ruby method pattern matching ala Erlang/Elixir
3
+ RuboClaus is an open source project that gives the Ruby developer a DSL to implement functions with multiple clauses and varying numbers of arguments on a pattern matching paradigm, inspired by functional programming in Elixir and Erlang.
4
4
 
5
- __NOTE__
5
+ #### Note
6
6
 
7
- This is purely a thought experiment at this point. Don't freak out,
8
- no one even said this was a good idea or will be usable yet. This is
9
- purely exploratory at this point.
7
+ _RuboClaus is still in very early stage of development and thought process. We are still treating this as a proof-of-concept and are still exploring the topic. As such, we don't suggest you use this in any kind of production environment, without first looking at the library code and feeling comfortable with how it works. And, if you would like to continue this thought experiment and provide feedback/suggestions/changes, we would love to hear it._
10
8
 
11
- _You have been warned_
9
+ ### Rationale
10
+
11
+ The beauty of multiple function clauses with pattern matching is fewer conditionals and fewer lines of unnecessary defensive logic. Focus on the happy path. Control types as they come in, and handle for edge cases with catch all clauses. It does not work great for simple methods, like this:
12
+
13
+ Ruby:
14
+
15
+ ```ruby
16
+ def add(first, second)
17
+ return "Please use numbers" unless [first, second].all? { |obj| obj.is_a? Fixnum }
18
+ first + second
19
+ end
20
+ ```
21
+
22
+ Ruby With RuboClaus:
23
+
24
+ ```ruby
25
+ define_function :add do
26
+ clauses(
27
+ clause([Fixnum, Fixnum], proc { |first, second| first + second }),
28
+ catch_all(proc { "Please use numbers" }
29
+ )
30
+ end
31
+ ```
32
+
33
+ It is cumbersome for problems like `add`--in which case we don't recommend using it. But as soon as we add complexity that depends on parameter arity or type, we can see how RuboClaus makes our code more extendible and maintainable. For example:
34
+
35
+ Ruby:
36
+
37
+ ```ruby
38
+ def handle_response(status, has_body, is_chunked)
39
+ if status == 200 && has_body && is_chunked
40
+ # ...
41
+ else
42
+ if status == 200 && has_body && !is_chunked
43
+ # ...
44
+ else
45
+ if status == 200 && !has_body
46
+ # ...
47
+ else
48
+ # ...
49
+ end
50
+ end
51
+ end
52
+ end
53
+ ```
54
+
55
+ Ruby with RuboClaus:
56
+
57
+ ```ruby
58
+ define_function :handle_response do
59
+ clauses(
60
+ clause([200, true, true], proc { |status, has_body, is_chunked| ... }),
61
+ clause([200, true, false], proc { |status, has_body, is_chunked| ... }),
62
+ clause([200, false], proc { |status, has_body| ... }),
63
+ catch_all(proc { return_error })
64
+ )
65
+ end
66
+ ```
67
+ [Credit](https://www.reddit.com/r/elixir/comments/34jyto/what_are_the_benefits_of_pattern_matching_as/cqve33n)
68
+
69
+ To learn more about this style of programming read about [function overloading](https://en.wikipedia.org/wiki/Function_overloading) and [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching).
70
+
71
+ ## Usage
72
+
73
+ Below are the public API methods and their associated arguments.
74
+
75
+ * `define_function`
76
+ * Symbol - name of the method to define
77
+ * Block - a single block with a `clauses` method call
78
+ * `clauses`
79
+ * N number of `clause` method calls and/or a single `catch_all` method call
80
+ * `clause` | `p_clause`
81
+ * Array - list of arguments to pattern match against
82
+ * Keywords:
83
+ * `:any` - among your arguments, `:any` represents that any data type will be accepted in its position.
84
+ * `:tail` - given an array argument with defined "head" elements and `:tail` as the last element (such as `[String, String, :tail]`), this will destructure the head elements and make the tail an array of the non-head elements.
85
+ * Proc - method body to execute when this method is matched and executed
86
+ * Note on `p_clause` - only visible to other clauses in the function, and will return `NoPatternMatchError` if invoked with matching parameters external to the function. Ideally used when calling the function recursively with different arity than the public api to the method.
87
+ * `catch_all`
88
+ * Proc - method body that will be executed if the arguments do not match any of the `clause` patterns defined
89
+
90
+ ### Clause pattern arguments
91
+
92
+ The first argument to the `clause` method is an array of pattern match options. This array can vary in length, and values depending on your pattern match case.
93
+
94
+ You can match against specific values:
95
+
96
+ ```ruby
97
+ clause(["foo"], proc {...})
98
+ clause([42], proc {...})
99
+ clause(["Hello", :darth_vader], proc {...})
100
+ ```
101
+
102
+ You can match against specifc argument types:
103
+
104
+ ```ruby
105
+ clause([String], proc {...})
106
+ clause([Fixnum], proc {...})
107
+ clause([String, Symbol], proc {...})
108
+ ```
109
+
110
+ You can match against specific values and types:
111
+
112
+ ```ruby
113
+ clause(["Hello", String], proc {...})
114
+ clause([42, Fixnum], proc {...})
115
+ clause([String, :darth_vader], proc {...})
116
+ ```
117
+
118
+ You also can match against any value or type if you don't have a specific requirement for an argument by using the `:any` symbol.
119
+
120
+ ```ruby
121
+ clause(["Hello", :any], proc {...})
122
+ clause([:any], proc {...})
123
+ clause([42, :any], proc {...})
124
+ ```
125
+
126
+ You also can destructure an array with `:tail`.
127
+
128
+ ```ruby
129
+ clause(["Hello", [Fixnum, :tail]], proc { |string, number, tail_array| ... })
130
+ clause([Hash, [Fixnum, Fixnum :tail]], proc { |hash, number1, number2, tail_array| ... })
131
+ ```
132
+
133
+
134
+
135
+ ### Examples
136
+
137
+ Please see the [examples directory](https://github.com/mojotech/rubo_claus/tree/master/examples) for various example use cases. Most examples include direct comparisons of the Ruby code to a similar implementation in Elixir.
138
+
139
+ ## Development
140
+
141
+ Don't introduce unneeded external dependencies.
142
+
143
+ Nothing else special to note for development. Just add tests associated to any code changes and make sure they pass.
144
+
145
+ ## TODO
146
+
147
+ - [ ] Rename public API methods? `define_function` is awkward since Ruby uses the term `method` instead of `function`
148
+ - [ ] Add Benchmarks to see performance implications
149
+ - [ ] Support private clauses to enforce a single entry point to a defined function
150
+
151
+ ---
152
+
153
+ [![Build Status](https://travis-ci.org/mojotech/rubo_claus.svg?branch=master)](https://travis-ci.org/mojotech/rubo_claus)
154
+ [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://github.com/dwyl/esta/issues)
File without changes
@@ -0,0 +1,32 @@
1
+ # From https://github.com/jnunemaker/httparty/blob/master/lib/httparty/cookie_hash.rb
2
+
3
+ class HTTParty::CookieHash < Hash #:nodoc:
4
+ include RuboClaus
5
+
6
+ CLIENT_COOKIES = %w(path expires domain path secure httponly)
7
+
8
+ define_function :add_cookies do
9
+ clauses(
10
+ clause([String], proc { |s| ... }),
11
+ clause([Hash], proc { |h| merge!(h) }),
12
+ catch_all(proc { raise "add_cookies only takes a Hash or a String" })
13
+ )
14
+ end
15
+
16
+ ## Ruby originally implemented in jnunemaker/httparty
17
+ #
18
+ # def add_cookies(value)
19
+ # case value
20
+ # when Hash
21
+ # merge!(value)
22
+ # when String
23
+ # ...
24
+ # else
25
+ # raise "add_cookies only takes a Hash or a String"
26
+ # end
27
+ # end
28
+
29
+ def to_cookie_string
30
+ reject { |k, v| CLIENT_COOKIES.include?(k.to_s.downcase) }.collect { |k, v| "#{k}=#{v}" }.join("; ")
31
+ end
32
+ end
@@ -0,0 +1,43 @@
1
+ require 'rubo_claus'
2
+
3
+ class DNA
4
+ include RuboClaus
5
+
6
+ def to_rna(dna='')
7
+ dna.split(//).map { |char| transform(char) }.join('')
8
+ end
9
+
10
+ define_function :transform do
11
+ clauses(
12
+ clause(['G'], proc { 'C' }),
13
+ clause(['C'], proc { 'G' }),
14
+ clause(['T'], proc { 'A' }),
15
+ clause(['A'], proc { 'U' })
16
+ )
17
+ end
18
+ end
19
+
20
+ ###
21
+ ### ELIXIR VERSION
22
+ ###
23
+ #
24
+ # defmodule DNA do
25
+ # @doc """
26
+ # Transcribes a character list representing DNA nucleotides to RNA
27
+ #
28
+ # ## Examples
29
+ #
30
+ # iex> DNA.to_rna('ACTG')
31
+ # 'UGAC'
32
+ # """
33
+ # @spec to_rna([char]) :: [char]
34
+ # def to_rna(dna) do
35
+ # Enum.map dna, &transform/1
36
+ # end
37
+ #
38
+ # @spec transform(char) :: char
39
+ # defp transform(?G), do: ?C
40
+ # defp transform(?C), do: ?G
41
+ # defp transform(?T), do: ?A
42
+ # defp transform(?A), do: ?U
43
+ # end
@@ -0,0 +1,33 @@
1
+ require 'rubo_claus'
2
+
3
+ class ListOps
4
+ include RuboClaus
5
+
6
+ define_function :count do
7
+ clauses(
8
+ clause([Array], proc { |array| count(array, 0) }),
9
+ clause([[], Fixnum], proc { |_array, sum| sum }),
10
+ clause([[:any, :tail], Fixnum], proc { |_head, tail, sum| count(tail, sum + 1) })
11
+ )
12
+ end
13
+
14
+ # The Ruby way to implement count recursively
15
+ def plain_ruby_count(array, sum=0)
16
+ return sum if array == []
17
+ a, *b = array
18
+ plain_ruby_count(b, sum + 1)
19
+ end
20
+ end
21
+
22
+ ###
23
+ ### ELIXIR VERSION
24
+ ###
25
+ # defmodule ListOps do
26
+ # @moduledoc """
27
+ # Implements some common list operations by hand
28
+ # """
29
+ # @spec count(list) :: non_neg_integer
30
+ # def count(l), do: l |> count(0)
31
+ # def count([], sum), do: sum
32
+ # def count([_|t], sum), do: count(t, sum + 1)
33
+ # end
@@ -0,0 +1,70 @@
1
+ require 'rubo_claus'
2
+
3
+ class RunLengthEncoder
4
+ include RuboClaus
5
+
6
+ def encode(str)
7
+ encoder str.split(//), ''
8
+ end
9
+
10
+ define_function :encoder do
11
+ clauses(
12
+ clause([[], String], proc { |_arr, encoded| encoded }),
13
+ clause([Array, String], proc do |arr, encoded|
14
+ encoder(arr, encoded, 1)
15
+ end),
16
+ clause([Array, String, Fixnum], proc do |arr, encoded, count|
17
+ h, *t = arr
18
+ if h == t[0]
19
+ encoder(t, encoded, count + 1)
20
+ else
21
+ encoder(t, encoded + "#{count}#{h}")
22
+ end
23
+ end)
24
+ )
25
+ end
26
+ end
27
+
28
+ ###
29
+ ### ELIXIR VERSION
30
+ ###
31
+ #
32
+ # defmodule RunLengthEncoder do
33
+ # @doc """
34
+ # Generates a string where consecutive elements are represented as a data value and count.
35
+ # "HORSE" => "1H1O1R1S1E"
36
+ # For this example, assume all input are strings, that are all uppercase letters.
37
+ # It should also be able to reconstruct the data into its original form.
38
+ # "1H1O1R1S1E" => "HORSE"
39
+ # """
40
+ # @spec encode(String.t) :: String.t
41
+ # def encode(string) do
42
+ # encoder String.codepoints(string), ""
43
+ # end
44
+ #
45
+ # @spec encoder([String.t], String.t) :: String.t
46
+ # defp encoder([], encoded), do: encoded
47
+ # defp encoder([h | t], encoded), do: encoder([h | t], encoded, 1)
48
+ #
49
+ # @spec encoder([String.t], String.t, integer()) :: String.t
50
+ # defp encoder([h | t], encoded, count) do
51
+ # cond do
52
+ # Enum.at(t, 0) == h -> encoder(t, encoded, count + 1)
53
+ # true -> encoder(t, encoded <> "#{count}#{h}")
54
+ # end
55
+ # end
56
+ #
57
+ # @spec decode(String.t) :: String.t
58
+ # def decode(string) do
59
+ # Regex.scan(~r/\\d+[A-Z]/, string) |> List.flatten |> decoder
60
+ # end
61
+ #
62
+ # @spec decoder([String.t]) :: String.t
63
+ # defp decoder(chars) do
64
+ # Enum.reduce chars, "", &(&2 <> format_string(String.split_at(&1, -1)))
65
+ # end
66
+ #
67
+ # @spec format_string(String.t) :: String.t
68
+ # defp format_string({1, str}), do: str
69
+ # defp format_string({cnt, str}), do: String.duplicate(str, String.to_integer(cnt))
70
+ # end
@@ -1,29 +1,30 @@
1
1
  module Match
2
- def deep_match?(lhs, rhs)
2
+ def match?(lhs, rhs)
3
+ return true if lhs.is_a?(RuboClaus::CatchAll)
4
+ return false if lhs.args.length != rhs.length
5
+ any_match?(lhs.args, rhs)
6
+ end
7
+
8
+ private def deep_match?(lhs, rhs)
3
9
  return match_array(lhs, rhs) if [lhs, rhs].all? { |side| side.is_a? Array }
4
10
  return match_hash(lhs, rhs) if [lhs, rhs].all? { |side| side.is_a? Hash }
5
11
  false
6
12
  end
7
13
 
8
- def single_match?(lhs, rhs)
14
+ private def single_match?(lhs, rhs)
9
15
  return true if lhs == :any
10
16
  return true if lhs == rhs
11
17
  return true if lhs == rhs.class
12
18
  false
13
19
  end
14
20
 
15
- def match?(lhs, rhs)
16
- return true if lhs.is_a?(RuboClaus::CatchAll)
17
- return false if lhs.args.length != rhs.length
18
- any_match?(lhs.args, rhs)
19
- end
20
-
21
- def any_match?(lhs, rhs)
21
+ private def any_match?(lhs, rhs)
22
22
  return true if single_match?(lhs, rhs)
23
23
  deep_match?(lhs, rhs)
24
24
  end
25
25
 
26
26
  private def match_array(lhs, rhs)
27
+ return head_tail_destructuring_match(lhs, rhs) if lhs.include?(:tail)
27
28
  return false if lhs.length != rhs.length
28
29
  lhs.zip(rhs) { |array| return false unless any_match?(*array) } || true
29
30
  end
@@ -46,4 +47,8 @@ module Match
46
47
  private def values_match?(lhs, rhs)
47
48
  any_match?(lhs.values, rhs.values)
48
49
  end
50
+
51
+ private def head_tail_destructuring_match(lhs, rhs)
52
+ any_match?(lhs[0..-2], rhs[0..(lhs[0..-2].size - 1)])
53
+ end
49
54
  end
@@ -3,6 +3,7 @@ require 'match'
3
3
  module RuboClaus
4
4
  include Match
5
5
  Clause = Struct.new(:args, :function)
6
+ PrivateClause = Struct.new(:args, :function)
6
7
  CatchAll = Struct.new(:proc)
7
8
 
8
9
  class NoPatternMatchError < NoMethodError; end
@@ -10,13 +11,23 @@ module RuboClaus
10
11
  module ClassMethods
11
12
  def define_function(symbol, &block)
12
13
  @function_name = symbol
14
+ @from_proc = false
13
15
  block.call
14
16
  end
15
17
 
16
18
  def clauses(*klauses)
17
19
  define_method(@function_name) do |*runtime_args|
18
- matching_function = find_matching_function(klauses, runtime_args)
19
- matching_function ? matching_function.call(*runtime_args) : raise(NoPatternMatchError)
20
+ case matching_clause = find_matching_clause(klauses, runtime_args)
21
+ when Clause
22
+ execute(head_tail_handle(matching_clause.args, runtime_args), matching_clause.function)
23
+ when PrivateClause
24
+ raise NoPatternMatchError, "no pattern defined for: #{runtime_args}" unless @from_proc
25
+ execute(head_tail_handle(matching_clause.args, runtime_args), matching_clause.function)
26
+ when CatchAll
27
+ execute(runtime_args, matching_clause.proc)
28
+ else
29
+ raise NoPatternMatchError, "no pattern defined for: #{runtime_args}"
30
+ end
20
31
  end
21
32
  end
22
33
 
@@ -24,15 +35,36 @@ module RuboClaus
24
35
  Clause.new(args, function)
25
36
  end
26
37
 
38
+ def p_clause(args, function)
39
+ PrivateClause.new(args, function)
40
+ end
41
+
27
42
  def catch_all(_proc)
28
43
  CatchAll.new(_proc)
29
44
  end
30
45
  end
31
46
 
32
- private def find_matching_function(klauses, runtime_args)
47
+ private def execute(args, proc)
48
+ @from_proc = true
49
+ method = instance_exec *args, &proc
50
+ @from_proc = false
51
+ method
52
+ end
53
+
54
+ private def find_matching_clause(klauses, runtime_args)
33
55
  clause = klauses.find { |pattern| match?(pattern, runtime_args) }
34
- return clause.function if clause.is_a?(Clause)
35
- return clause.proc if clause.is_a?(CatchAll)
56
+ return clause if [Clause, PrivateClause, CatchAll].include?(clause.class)
57
+ end
58
+
59
+ private def head_tail_handle(lhs, rhs)
60
+ lhs.each_with_index.flat_map do |arg, index|
61
+ if arg.is_a?(Array) && arg.include?(:tail)
62
+ number_of_heads = lhs[index][0..-2].size
63
+ rhs[index][0..(number_of_heads - 1)] + [rhs[index][number_of_heads..-1]]
64
+ else
65
+ [rhs[index]]
66
+ end
67
+ end
36
68
  end
37
69
 
38
70
  def self.included(klass)
@@ -1,3 +1,3 @@
1
1
  module RuboClaus
2
- VERSION = '0.1.0'
2
+ VERSION = '0.2.0'
3
3
  end
@@ -3,17 +3,20 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'rubo_claus/version'
4
4
 
5
5
  Gem::Specification.new do |s|
6
- s.name = 'rubo_claus'
7
- s.version = RuboClaus::VERSION
8
- s.date = '2016-08-09'
9
- s.summary = 'Ruby Method Pattern Matcher'
10
- s.description = 'Define ruby methods with pattern matching much like Erlang/Elixir'
11
- s.authors = ['Omid Bachari', 'Craig P Jolicoeur']
12
- s.email = ['omid@mojotech.com', 'craig@mojotech.com']
13
- s.files = `git ls-files`.split($/)
6
+ s.name = 'rubo_claus'
7
+ s.version = RuboClaus::VERSION
8
+ s.date = '2016-08-09'
9
+ s.summary = 'Ruby Method Pattern Matcher'
10
+ s.description = 'Define ruby methods with pattern matching much like Erlang/Elixir'
11
+ s.authors = ['Omid Bachari', 'Craig P Jolicoeur']
12
+ s.email = ['omid@mojotech.com', 'craig@mojotech.com']
13
+ s.files = `git ls-files`.split($/)
14
14
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
15
15
  s.require_paths = ["lib"]
16
- s.license = 'MIT'
16
+ s.license = 'MIT'
17
+ s.homepage = 'http://mojotech.github.io/rubo_claus/'
17
18
 
18
19
  s.add_development_dependency 'rake', '~> 0'
20
+ s.add_development_dependency 'minitest', '>= 5.9'
21
+ s.required_ruby_version = '>= 2.1'
19
22
  end
@@ -0,0 +1,69 @@
1
+ require 'minitest/autorun'
2
+ require 'match'
3
+
4
+ class MyClassTest < Minitest::Test
5
+ include Match
6
+ Clause = Struct.new(:args, :function)
7
+
8
+ def test_types
9
+ lhs = Clause.new([String, Fixnum, Array, Symbol, Float, Hash, Proc], '')
10
+ rhs = ["", 1, [], :hi, 1.1, {}, proc {}]
11
+ assert match?(lhs, rhs)
12
+ end
13
+
14
+ def test_any_matching
15
+ lhs = Clause.new([:any, 1, :any], '')
16
+ rhs = ["dog", 1, 1.22]
17
+
18
+ assert match?(lhs, rhs)
19
+ end
20
+
21
+ def test_literal_matching
22
+ lhs = Clause.new([1, 2, 3], '')
23
+ rhs = [1, 2, 3]
24
+ rhs_fail = [2, 2, 4]
25
+
26
+ assert match?(lhs, rhs)
27
+ assert !match?(lhs, rhs_fail)
28
+ end
29
+
30
+ # array
31
+ def test_array_matching
32
+ lhs = Clause.new([1, [:any, Hash, 2], 3], '')
33
+ rhs = [1, ["Dog", {people: ['Tom', 'Mary']}, 2], 3]
34
+
35
+ assert match?(lhs, rhs)
36
+ end
37
+
38
+ def test_array_matching_2
39
+ lhs = Clause.new([1, [[:any, [1]], Hash, 2], 3], '')
40
+ rhs = [1, [[2, [1]], {people: ['Tom', 'Mary']}, 2], 3]
41
+ rhs_fail = [1, [[2, [77]], {people: ['Tom', 'Mary']}, 2], 3]
42
+
43
+ assert match?(lhs, rhs)
44
+ assert !match?(lhs, rhs_fail)
45
+ end
46
+
47
+ def test_hash_matching
48
+ lhs = Clause.new([{name: :any, friends: Array, home_phone: String}], '')
49
+ rhs = [{name: "Jose", friends: ['Tim', 'Mary'], home_phone: '234-234-2343'}]
50
+ rhs_fail = [{username: "Jose", friends: ['Tim', 'Mary'], home_phone: '234-234-2343'}]
51
+
52
+ assert match?(lhs, rhs)
53
+ assert !match?(lhs, rhs_fail)
54
+ end
55
+
56
+ def test_hash_destructuring
57
+ lhs = Clause.new([{name: :any, friends: Array, home_phone: String}], '')
58
+ rhs = [{name: "Jose", friends: ['Tim', 'Mary'], home_phone: '234-234-2343', fax_number: '11-1111-111'}]
59
+
60
+ assert match?(lhs, rhs)
61
+ end
62
+
63
+ def test_compound_data_matching
64
+ lhs = Clause.new([{name: :any, friends: Array, home_phone: String, homes: [:any, Array, {address: [String, "111 Gomer Pile"]}]}], '')
65
+ rhs = [{name: "Jose", friends: ['Tim', 'Mary'], home_phone: '234-234-2343', homes: ["Big one", [1, 2 ,3], {address: ["Home", "111 Gomer Pile"]}], fax_number: '11-1111-111'}]
66
+
67
+ assert match?(lhs, rhs)
68
+ end
69
+ end
@@ -11,51 +11,39 @@ end
11
11
  class MyClass
12
12
  include RuboClaus
13
13
 
14
- define_function :add_one do
14
+ define_function :return_thing do
15
15
  clauses(
16
- clause([0], proc { |n| "Dont add one to zero please!" }),
17
- clause([NilClass], proc { |n| 42 }),
18
- clause([:any], proc { |n| n + 1 }),
19
- catch_all(proc { raise NoMethodError, "no clause defined} "})
16
+ clause([Array], proc { |n| n.inspect }),
17
+ clause([:any], proc { |n| n })
20
18
  )
21
19
  end
22
20
 
23
- define_function :greeting do
21
+ define_function :return_two_things do
24
22
  clauses(
25
- clause([String], proc { |str| "Hello, #{str}!" }),
26
- clause([String, String], proc { |greet, str| "#{greet}, #{str}!"})
23
+ clause([:any, :any], proc { |n, n1| [n, n1] }),
24
+ clause([:any], proc { "Please use two things, not one." })
27
25
  )
28
26
  end
29
27
 
30
- define_function :any_test do
28
+ define_function :welcome_persons_named_tim do
31
29
  clauses(
32
- clause([1, :any, 3], proc { |n1, any, n3| "#{n1} ANY 2" }),
33
- clause([:any, 2, 3], proc { |any, n2, n3| "ANY 2 3" }),
34
- clause([:any, 2, :any], proc { |ne1, n2, ne2| "ANY 2 ANY" })
30
+ clause(["Tim"], proc { "Please welcome Tim."}),
31
+ catch_all(proc { "You are not Tim." })
35
32
  )
36
33
  end
37
34
 
38
- define_function :print_string_in_array do
35
+ define_function :fib do
39
36
  clauses(
40
- clause([1, [:any, [String]], Fixnum], proc { |n, n1, n2| "#{n1[1][0]}" })
37
+ clause([0], proc { 0 }),
38
+ clause([1], proc { 1 }),
39
+ clause([Fixnum], proc { |num| fib(num-1) + fib(num-2) })
41
40
  )
42
41
  end
43
42
 
44
- define_function :integer_in_nested_array do
43
+ define_function :get_head_and_tail_shallow do
45
44
  clauses(
46
- clause([Fixnum, 2, [[[[[Fixnum]]]]]], proc { |n, n1, n2| n2[0][0][0][0][0] })
47
- )
48
- end
49
-
50
- define_function :hash_keys do
51
- clauses(
52
- clause([Hash], proc { |hash| hash.keys })
53
- )
54
- end
55
-
56
- define_function :friend_hash do
57
- clauses(
58
- clause([{friend: String, foe: :any}], proc { |n| "I know #{n[:friend]}" })
45
+ clause([[String, :any, :tail], :any], proc { |n, n1, n2_tail, n3| {first: n, second: n1, third: n2_tail, fourth: n3} }),
46
+ clause([[:any, :tail], :any], proc { |n, n1_tail, n2| {first: n, second: n1_tail, third: n2} })
59
47
  )
60
48
  end
61
49
 
@@ -65,12 +53,6 @@ class MyClass
65
53
  )
66
54
  end
67
55
 
68
- define_function :get_people do
69
- clauses(
70
- clause([:any, [1, 2, String, String], {people: String}], proc { |n, n1, n2| n2[:people] })
71
- )
72
- end
73
-
74
56
  define_function :night_emergency_phone do
75
57
  param_shape = {username: String, friends: Array, phone: {fax: :any, mobile: String, emergency: {day: String, night: String}}}
76
58
 
@@ -80,97 +62,70 @@ class MyClass
80
62
  end)
81
63
  )
82
64
  end
83
- end
84
-
85
- class MyClassTest < Minitest::Test
86
- def test_simple_clauses
87
- k = MyClass.new
88
- assert_equal "Dont add one to zero please!", k.add_one(0)
89
- assert_equal 3, k.add_one(2)
90
- assert_equal 42, k.add_one(nil)
91
- assert_raises NoMethodError do
92
- k.add_one(1,2,3, [:b])
93
- end
94
- end
95
65
 
96
- def test_variadic_arity
97
- k = MyClass.new
98
- assert_equal "Hello, Newman!", k.greeting("Newman")
99
- assert_equal "Bye Bye, Dolly!", k.greeting("Bye Bye", "Dolly")
66
+ define_function :count do
67
+ clauses(
68
+ clause([Array], proc { |array| count(array, 0) }),
69
+ clause([[], Fixnum], proc { |_array, sum| sum }),
70
+ clause([[:any, :tail], Fixnum], proc { |_head, tail, sum| count(tail, sum + 1) })
71
+ )
100
72
  end
101
73
 
102
- def test_mixed_location_any_terms
103
- k = MyClass.new
104
- assert_equal "1 ANY 2", k.any_test(1,999,3)
105
- assert_equal "ANY 2 3", k.any_test(999,2,3)
106
- assert_equal "ANY 2 ANY", k.any_test(999,2,987)
74
+ define_function :count_with_private do
75
+ clauses(
76
+ clause([Array], proc { |array| count_with_private(array, 0) }),
77
+ p_clause([[], Fixnum], proc { |_array, sum| sum }),
78
+ p_clause([[:any, :tail], Fixnum], proc { |_head, tail, sum| count_with_private(tail, sum + 1) })
79
+ )
107
80
  end
81
+ end
108
82
 
109
- def test_no_catch_all_clause_defined
83
+ class MyClassTest < Minitest::Test
84
+ def test_define_function
110
85
  k = MyClass.new
111
86
 
112
- assert_raises RuboClaus::NoPatternMatchError do
113
- k.greeting(1,2,3,)
114
- end
115
-
116
- assert_raises RuboClaus::NoPatternMatchError do
117
- k.greeting(["Goat"])
118
- end
87
+ assert_equal '[1]', k.return_thing([1])
88
+ assert_equal 1, k.return_thing(1)
119
89
  end
120
90
 
121
- def test_nested_array_with_shape
91
+ def test_using_clauses_and_variadic_arity
122
92
  k = MyClass.new
123
93
 
124
- assert_equal "Inner string", k.print_string_in_array(1, [1, ["Inner string"]], 3)
94
+ assert_equal [1, 2], k.return_two_things(1, 2)
95
+ assert_equal "Please use two things, not one.", k.return_two_things(1)
125
96
 
126
97
  assert_raises RuboClaus::NoPatternMatchError do
127
- k.print_string_in_array(3, [1, "Inner string"], 3)
128
- end
129
-
130
- assert_raises RuboClaus::NoPatternMatchError do
131
- k.print_string_in_array(3, [1, "Inner string"], "S")
132
- end
133
-
134
- assert_raises RuboClaus::NoPatternMatchError do
135
- k.print_string_in_array(3, [1, []], "S")
136
- end
137
-
138
- assert_raises RuboClaus::NoPatternMatchError do
139
- k.print_string_in_array(3, [[]], "S")
98
+ k.return_two_things(1, 2, 3)
140
99
  end
141
100
  end
142
101
 
143
- def test_nested_array
102
+ def test_using_catchall
144
103
  k = MyClass.new
145
104
 
146
- assert_equal 3, k.integer_in_nested_array(1, 2, [[[[[3]]]]])
147
-
148
- assert_raises RuboClaus::NoPatternMatchError do
149
- k.integer_in_nested_array(1, 2, [[[[["Dog"]]]]])
150
- end
105
+ assert_equal "Please welcome Tim.", k.welcome_persons_named_tim("Tim")
106
+ assert_equal "You are not Tim.", k.welcome_persons_named_tim("Don")
151
107
  end
152
108
 
153
- def test_hash
109
+ def test_using_recursion
154
110
  k = MyClass.new
155
111
 
156
- assert_equal [:dog], k.hash_keys({dog: :bone})
157
-
158
- assert_raises RuboClaus::NoPatternMatchError do
159
- k.hash_keys([{}])
160
- end
161
- end
112
+ assert_equal 0, k.fib(0)
113
+ assert_equal 1, k.fib(1)
114
+ assert_equal 1, k.fib(2)
115
+ assert_equal 2, k.fib(3)
116
+ assert_equal 3, k.fib(4)
162
117
 
163
- def test_hash_with_keys
164
- k = MyClass.new
118
+ assert_equal 3, k.count([1, 2, 3])
119
+ assert_equal 1, k.count([1])
165
120
 
166
- assert_equal "I know John", k.friend_hash({friend: "John", foe: 3})
121
+ assert_equal 3, k.count_with_private([1, 2, 3])
167
122
 
168
123
  assert_raises RuboClaus::NoPatternMatchError do
169
- k.friend_hash({})
124
+ k.count_with_private([1, 2, 3], 3)
170
125
  end
171
126
  end
172
127
 
173
- def test_hash_with_keys_destructured
128
+ def test_shallow_hash_destructuring
174
129
  k = MyClass.new
175
130
 
176
131
  assert_equal "I know everything about John", k.friend_hash_des({friend: "John", foe: 3})
@@ -180,20 +135,19 @@ class MyClassTest < Minitest::Test
180
135
  end
181
136
  end
182
137
 
183
- def test_compound_hash_array
138
+ def test_shallow_head_tail_destructuring
184
139
  k = MyClass.new
140
+ output = {first: 1, second: [2, 3, 4], third: 5}
141
+ second_output = {first: "1", second: 2, third: [3, 4], fourth: 5}
185
142
 
186
- assert_equal "John", k.get_people("Douglas", [1, 2, "Anything", "Another any string"], {people: "John"})
187
-
188
- assert_raises RuboClaus::NoPatternMatchError do
189
- k.get_people("Douglas", [], {people: "John"})
190
- end
143
+ assert_equal output, k.get_head_and_tail_shallow([1, 2, 3, 4], 5)
144
+ assert_equal second_output, k.get_head_and_tail_shallow(["1", 2, 3, 4], 5)
191
145
  end
192
146
 
193
- def test_three_dimensional_compound_data_structure
147
+ def test_compound_data_destructuring
194
148
  k = MyClass.new
195
149
 
196
- param = {username: "Sally Moe", friends: [], phone: {fax: "NA", mobile: "123-345-1232", emergency: {day: "123-123-1234", night: "999-999-9999"}}}
150
+ param = {username: "Sally Moe", friends: [], phone: {fax: "NA", mobile: "123-345-1232", emergency: {day: "123-123-1234", night: "999-999-9999", weekend: '123-123-1233'}}}
197
151
  assert_equal "999-999-9999", k.night_emergency_phone(param)
198
152
 
199
153
  assert_raises RuboClaus::NoPatternMatchError do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubo_claus
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Omid Bachari
@@ -25,6 +25,20 @@ dependencies:
25
25
  - - "~>"
26
26
  - !ruby/object:Gem::Version
27
27
  version: '0'
28
+ - !ruby/object:Gem::Dependency
29
+ name: minitest
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: '5.9'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: '5.9'
28
42
  description: Define ruby methods with pattern matching much like Erlang/Elixir
29
43
  email:
30
44
  - omid@mojotech.com
@@ -33,15 +47,23 @@ executables: []
33
47
  extensions: []
34
48
  extra_rdoc_files: []
35
49
  files:
36
- - ".ruby-version"
50
+ - ".gitignore"
51
+ - ".travis.yml"
52
+ - Gemfile
37
53
  - README.md
38
54
  - Rakefile
55
+ - examples/.keep
56
+ - examples/cookie_hash.rb
57
+ - examples/dna.rb
58
+ - examples/list_operations.rb
59
+ - examples/run_length_encoder.rb
39
60
  - lib/match.rb
40
61
  - lib/rubo_claus.rb
41
62
  - lib/rubo_claus/version.rb
42
63
  - rubo_claus.gemspec
64
+ - test/test_match.rb
43
65
  - test/test_rubo_claus.rb
44
- homepage:
66
+ homepage: http://mojotech.github.io/rubo_claus/
45
67
  licenses:
46
68
  - MIT
47
69
  metadata: {}
@@ -53,7 +75,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
53
75
  requirements:
54
76
  - - ">="
55
77
  - !ruby/object:Gem::Version
56
- version: '0'
78
+ version: '2.1'
57
79
  required_rubygems_version: !ruby/object:Gem::Requirement
58
80
  requirements:
59
81
  - - ">="
@@ -66,4 +88,5 @@ signing_key:
66
88
  specification_version: 4
67
89
  summary: Ruby Method Pattern Matcher
68
90
  test_files:
91
+ - test/test_match.rb
69
92
  - test/test_rubo_claus.rb
@@ -1 +0,0 @@
1
- 2.3.1