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 +4 -4
- data/.gitignore +4 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/README.md +149 -6
- data/examples/.keep +0 -0
- data/examples/cookie_hash.rb +32 -0
- data/examples/dna.rb +43 -0
- data/examples/list_operations.rb +33 -0
- data/examples/run_length_encoder.rb +70 -0
- data/lib/match.rb +14 -9
- data/lib/rubo_claus.rb +37 -5
- data/lib/rubo_claus/version.rb +1 -1
- data/rubo_claus.gemspec +12 -9
- data/test/test_match.rb +69 -0
- data/test/test_rubo_claus.rb +58 -104
- metadata +27 -4
- data/.ruby-version +0 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bec5af93a59d08f56169f041625c4e7c67fbdd4e
|
4
|
+
data.tar.gz: c185d7213d32531ff43d5bd2f9ff9c7c0c1a6c74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6b7d5d705d13430d5e29056c964b6c0b9fdb2c33e69a89fe2a8cbd0491ef0bb748811c03b35b8102587aa8242fdc04e635aeb1daa3885238f1455a6698f85a48
|
7
|
+
data.tar.gz: b3729d8789de8c9ad048f60786a7d56c3874d9085ce6e37a10ea35427d4fb9622800b52d7f4ad5189a0ef60770dffb5a3b5022c1ec7c0db3b181a74d7657bcdc
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -1,11 +1,154 @@
|
|
1
1
|
# RuboClaus
|
2
2
|
|
3
|
-
|
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
|
-
|
5
|
+
#### Note
|
6
6
|
|
7
|
-
|
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
|
-
|
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)
|
data/examples/.keep
ADDED
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
|
data/examples/dna.rb
ADDED
@@ -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
|
data/lib/match.rb
CHANGED
@@ -1,29 +1,30 @@
|
|
1
1
|
module Match
|
2
|
-
def
|
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
|
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
|
data/lib/rubo_claus.rb
CHANGED
@@ -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
|
-
|
19
|
-
|
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
|
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
|
35
|
-
|
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)
|
data/lib/rubo_claus/version.rb
CHANGED
data/rubo_claus.gemspec
CHANGED
@@ -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
|
7
|
-
s.version
|
8
|
-
s.date
|
9
|
-
s.summary
|
10
|
-
s.description
|
11
|
-
s.authors
|
12
|
-
s.email
|
13
|
-
s.files
|
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
|
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
|
data/test/test_match.rb
ADDED
@@ -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
|
data/test/test_rubo_claus.rb
CHANGED
@@ -11,51 +11,39 @@ end
|
|
11
11
|
class MyClass
|
12
12
|
include RuboClaus
|
13
13
|
|
14
|
-
define_function :
|
14
|
+
define_function :return_thing do
|
15
15
|
clauses(
|
16
|
-
clause([
|
17
|
-
clause([
|
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 :
|
21
|
+
define_function :return_two_things do
|
24
22
|
clauses(
|
25
|
-
clause([
|
26
|
-
clause([
|
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 :
|
28
|
+
define_function :welcome_persons_named_tim do
|
31
29
|
clauses(
|
32
|
-
clause([
|
33
|
-
|
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 :
|
35
|
+
define_function :fib do
|
39
36
|
clauses(
|
40
|
-
clause([
|
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 :
|
43
|
+
define_function :get_head_and_tail_shallow do
|
45
44
|
clauses(
|
46
|
-
clause([
|
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
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
83
|
+
class MyClassTest < Minitest::Test
|
84
|
+
def test_define_function
|
110
85
|
k = MyClass.new
|
111
86
|
|
112
|
-
|
113
|
-
|
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
|
91
|
+
def test_using_clauses_and_variadic_arity
|
122
92
|
k = MyClass.new
|
123
93
|
|
124
|
-
assert_equal
|
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.
|
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
|
102
|
+
def test_using_catchall
|
144
103
|
k = MyClass.new
|
145
104
|
|
146
|
-
assert_equal
|
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
|
109
|
+
def test_using_recursion
|
154
110
|
k = MyClass.new
|
155
111
|
|
156
|
-
assert_equal
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
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
|
-
|
164
|
-
|
118
|
+
assert_equal 3, k.count([1, 2, 3])
|
119
|
+
assert_equal 1, k.count([1])
|
165
120
|
|
166
|
-
assert_equal
|
121
|
+
assert_equal 3, k.count_with_private([1, 2, 3])
|
167
122
|
|
168
123
|
assert_raises RuboClaus::NoPatternMatchError do
|
169
|
-
k.
|
124
|
+
k.count_with_private([1, 2, 3], 3)
|
170
125
|
end
|
171
126
|
end
|
172
127
|
|
173
|
-
def
|
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
|
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
|
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
|
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.
|
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
|
-
- ".
|
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: '
|
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
|
data/.ruby-version
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
2.3.1
|