monadist 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +16 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +41 -0
- data/Rakefile +12 -0
- data/examples/continuation/github_fetch_original.rb +48 -0
- data/examples/continuation/github_fetch_refactored_continuation.rb +124 -0
- data/examples/continuation/github_fetch_refactored_meanwhile.rb +49 -0
- data/examples/general.rb +27 -0
- data/examples/identity.rb +6 -0
- data/examples/list.rb +65 -0
- data/examples/maybe.rb +90 -0
- data/lib/monadist/continuation.rb +31 -0
- data/lib/monadist/identity.rb +25 -0
- data/lib/monadist/list.rb +29 -0
- data/lib/monadist/maybe.rb +27 -0
- data/lib/monadist/meanwhile.rb +15 -0
- data/lib/monadist/monad.rb +24 -0
- data/lib/monadist/shims.rb +21 -0
- data/lib/monadist/version.rb +3 -0
- data/lib/monadist.rb +7 -0
- data/monadist.gemspec +24 -0
- data/spec/monadist/identity_spec.rb +38 -0
- data/spec/monadist/list_spec.rb +54 -0
- data/spec/monadist/maybe_spec.rb +76 -0
- data/spec/monadist_spec.rb +7 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/support/shared_examples_for_monad.rb +27 -0
- metadata +132 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Zoltan Ormandi
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# Monadist
|
2
|
+
|
3
|
+
A practical and useful Ruby implementation of a couple of popular monads.
|
4
|
+
Method naming follows the Haskell convention so the gem can be used for trying to understand monads
|
5
|
+
(mostly described in tutorials using Haskell).
|
6
|
+
|
7
|
+
Monads implemented:
|
8
|
+
|
9
|
+
* Identity (for educational purposes)
|
10
|
+
* Maybe
|
11
|
+
* List
|
12
|
+
* Continuation
|
13
|
+
* Meanwhile
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
Add this line to your application's Gemfile:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem 'monadist'
|
21
|
+
```
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install monadist
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
For examples on how to use these monads, please check out the [examples](https://github.com/zormandi/monadist/tree/master/examples) directory.
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it ( https://github.com/[my-github-username]/monadist/fork )
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
41
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
|
4
|
+
desc "Run RSpec code examples (options: RSPEC_SEED=seed)"
|
5
|
+
RSpec::Core::RakeTask.new :spec do |task|
|
6
|
+
task.verbose = false
|
7
|
+
task.rspec_opts = "--format progress --order random"
|
8
|
+
task.rspec_opts << " --seed #{ENV['RSPEC_SEED']}" if ENV['RSPEC_SEED']
|
9
|
+
end
|
10
|
+
|
11
|
+
task :default => :spec
|
12
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'json'
|
3
|
+
require 'uri_template'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
def get_json(url, &success)
|
8
|
+
Thread.new do
|
9
|
+
puts "Reading from #{url}"
|
10
|
+
uri = URI url
|
11
|
+
json = Net::HTTP.start uri.host, uri.port, :use_ssl => uri.scheme == 'https' do |http|
|
12
|
+
http.request_get(uri.path).body
|
13
|
+
end
|
14
|
+
value = JSON.parse json
|
15
|
+
success.call value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
get_json('https://api.github.com/') do |urls|
|
22
|
+
org_url_template = URITemplate.new(urls['organization_url'])
|
23
|
+
org_url = org_url_template.expand(org: 'ruby')
|
24
|
+
|
25
|
+
get_json(org_url) do |org|
|
26
|
+
repos_url = org['repos_url']
|
27
|
+
|
28
|
+
get_json(repos_url) do |repos|
|
29
|
+
most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
|
30
|
+
repo_url = most_popular_repo['url']
|
31
|
+
|
32
|
+
get_json(repo_url) do |repo|
|
33
|
+
contributors_url = repo['contributors_url']
|
34
|
+
|
35
|
+
get_json(contributors_url) do |users|
|
36
|
+
most_prolific_user = users.max_by { |user| user['contributions'] }
|
37
|
+
user_url = most_prolific_user['url']
|
38
|
+
|
39
|
+
get_json(user_url) do |user|
|
40
|
+
puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
sleep(1) while Thread.list.count > 1
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'monadist'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri_template'
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def get_json(url, &success)
|
9
|
+
Thread.new do
|
10
|
+
puts "Reading from #{url}"
|
11
|
+
uri = URI url
|
12
|
+
json = Net::HTTP.start uri.host, uri.port, :use_ssl => uri.scheme == 'https' do |http|
|
13
|
+
http.request_get(uri.path).body
|
14
|
+
end
|
15
|
+
value = JSON.parse json
|
16
|
+
success.call value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
def get_github_api_urls
|
23
|
+
github_root_url = 'https://api.github.com/'
|
24
|
+
|
25
|
+
Monadist::Continuation.new { |success| get_json github_root_url, &success }
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
|
30
|
+
def get_org(urls, name)
|
31
|
+
org_url_template = URITemplate.new(urls['organization_url'])
|
32
|
+
org_url = org_url_template.expand(org: name)
|
33
|
+
|
34
|
+
Monadist::Continuation.new { |success| get_json org_url, &success }
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
|
39
|
+
def get_repos(org)
|
40
|
+
repos_url = org['repos_url']
|
41
|
+
|
42
|
+
Monadist::Continuation.new { |success| get_json repos_url, &success }
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
|
47
|
+
def get_most_popular_repo(repos)
|
48
|
+
most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
|
49
|
+
repo_url = most_popular_repo['url']
|
50
|
+
|
51
|
+
Monadist::Continuation.new { |success| get_json repo_url, &success }
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
|
56
|
+
def get_contributors(repo)
|
57
|
+
contributors_url = repo['contributors_url']
|
58
|
+
|
59
|
+
Monadist::Continuation.new { |success| get_json contributors_url, &success }
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
def get_most_prolific_user(contributors)
|
65
|
+
most_prolific_user = contributors.max_by { |user| user['contributions'] }
|
66
|
+
user_url = most_prolific_user['url']
|
67
|
+
|
68
|
+
Monadist::Continuation.new { |success| get_json user_url, &success }
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
|
73
|
+
Monadist::Continuation.new { |success| get_json 'https://api.github.com/', &success }.bind do |urls|
|
74
|
+
org_url_template = URITemplate.new(urls['organization_url'])
|
75
|
+
org_url = org_url_template.expand(org: 'ruby')
|
76
|
+
|
77
|
+
Monadist::Continuation.new { |success| get_json org_url, &success }.bind do |org|
|
78
|
+
repos_url = org['repos_url']
|
79
|
+
|
80
|
+
Monadist::Continuation.new { |success| get_json repos_url, &success }.bind do |repos|
|
81
|
+
most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
|
82
|
+
repo_url = most_popular_repo['url']
|
83
|
+
|
84
|
+
Monadist::Continuation.new { |success| get_json repo_url, &success }.bind do |repo|
|
85
|
+
contributors_url = repo['contributors_url']
|
86
|
+
|
87
|
+
Monadist::Continuation.new { |success| get_json contributors_url, &success }.bind do |users|
|
88
|
+
most_prolific_user = users.max_by { |user| user['contributions'] }
|
89
|
+
user_url = most_prolific_user['url']
|
90
|
+
|
91
|
+
Monadist::Continuation.new { |success| get_json user_url, &success }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end.run do |user|
|
97
|
+
puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
get_github_api_urls.bind do |urls|
|
102
|
+
get_org(urls, 'ruby').bind do |org|
|
103
|
+
get_repos(org).bind do |repos|
|
104
|
+
get_most_popular_repo(repos).bind do |repo|
|
105
|
+
get_contributors(repo).bind do |users|
|
106
|
+
get_most_prolific_user(users)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end.run do |user|
|
112
|
+
puts "The most influential Rubyist is #{user['name']} (#{user['login']})"
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
get_github_api_urls.
|
117
|
+
bind { |urls| get_org urls, 'ruby' }.
|
118
|
+
bind { |org| get_repos org }.
|
119
|
+
bind { |repos| get_most_popular_repo repos }.
|
120
|
+
bind { |repo| get_contributors repo }.
|
121
|
+
bind { |contributors| get_most_prolific_user contributors }.
|
122
|
+
run { |user| puts "The most influential Rubyist is #{user['name']} (#{user['login']})" }
|
123
|
+
|
124
|
+
sleep 0.1 while Thread.list.count > 1
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'monadist'
|
2
|
+
require 'net/http'
|
3
|
+
require 'json'
|
4
|
+
require 'uri_template'
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def get_json(url)
|
9
|
+
puts "Reading from #{url}"
|
10
|
+
uri = URI url
|
11
|
+
json = Net::HTTP.start uri.host, uri.port, :use_ssl => uri.scheme == 'https' do |http|
|
12
|
+
http.request_get(uri.path).body
|
13
|
+
end
|
14
|
+
JSON.parse json
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def get_org(urls, name)
|
20
|
+
org_url_template = URITemplate.new(urls['organization_url'])
|
21
|
+
get_json org_url_template.expand(org: name)
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
def get_most_popular_repo(repos)
|
27
|
+
most_popular_repo = repos.max_by { |repo| repo['watchers_count'] }
|
28
|
+
get_json most_popular_repo['url']
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
|
33
|
+
def get_most_prolific_user(contributors)
|
34
|
+
most_prolific_user = contributors.max_by { |user| user['contributions'] }
|
35
|
+
get_json most_prolific_user['url']
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
Monadist::Meanwhile.unit('https://api.github.com/').
|
41
|
+
fmap { |github_api_url| get_json github_api_url }.
|
42
|
+
fmap { |urls| get_org urls, 'ruby' }.
|
43
|
+
fmap { |org| get_json org['repos_url'] }.
|
44
|
+
fmap { |repos| get_most_popular_repo repos }.
|
45
|
+
fmap { |repo| get_json repo['contributors_url'] }.
|
46
|
+
fmap { |contributors| get_most_prolific_user contributors }.
|
47
|
+
run { |user| puts "The most influential Rubyist is #{user['name']} (#{user['login']})" }
|
48
|
+
|
49
|
+
sleep 0.1 while Thread.list.count > 1
|
data/examples/general.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'monadist'
|
2
|
+
require 'monadist/shims'
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
nothing = possibly_nil nil
|
6
|
+
maybe = possibly_nil '{"first_name":"Sam", "last_name":"Vimes"}'
|
7
|
+
list = list '{"first_name":"Granny", "last_name":"Weatherwax"}', '{"first_name":"Nanny", "last_name":"Ogg"}'
|
8
|
+
continuation = with '{"first_name":"Fred", "last_name":"Colon"}'
|
9
|
+
meanwhile = meanwhile_with '{"first_name":"Nobby", "last_name":"Nobbs"}'
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def full_name(json_monad)
|
14
|
+
json_monad.
|
15
|
+
fmap { |json| JSON.parse json }.
|
16
|
+
fmap { |person| "#{person['first_name']} #{person['last_name']}" }
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
p full_name(nothing).value
|
22
|
+
p full_name(maybe).value
|
23
|
+
p full_name(list).values
|
24
|
+
full_name(continuation).run { |full_name| p full_name }
|
25
|
+
|
26
|
+
full_name(meanwhile).run { |full_name| p full_name }
|
27
|
+
sleep 0.1 while Thread.list.count > 1
|
data/examples/list.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'monadist'
|
2
|
+
require 'monadist/shims'
|
3
|
+
|
4
|
+
Blog = Struct.new :categories
|
5
|
+
Category = Struct.new :posts
|
6
|
+
Post = Struct.new :comments
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
def words_in(blogs)
|
11
|
+
blogs.flat_map do |blog|
|
12
|
+
blog.categories.flat_map do |category|
|
13
|
+
category.posts.flat_map do |post|
|
14
|
+
post.comments.flat_map do |comment|
|
15
|
+
comment.split /\s+/
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
def words_in_list_bind(blogs)
|
25
|
+
Monadist::List.unit(blogs).
|
26
|
+
bind { |blog| Monadist::List.unit blog.categories }.
|
27
|
+
bind { |category| Monadist::List.unit category.posts }.
|
28
|
+
bind { |post| Monadist::List.unit post.comments }.
|
29
|
+
bind { |comment| Monadist::List.unit comment.split /\s+/ }.
|
30
|
+
values
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
def words_in_list_fmap(blogs)
|
36
|
+
Monadist::List.unit(blogs).
|
37
|
+
fmap { |blog| blog.categories }.
|
38
|
+
fmap { |category| category.posts }.
|
39
|
+
fmap { |post| post.comments }.
|
40
|
+
fmap { |comment| comment.split /\s+/ }.
|
41
|
+
values
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
def words_in_list_sugar(blogs)
|
47
|
+
list(blogs).categories.posts.comments.split(/\s+/).values
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
|
52
|
+
blogs = [
|
53
|
+
Blog.new([Category.new([Post.new(['I love cats', 'I love dogs']),
|
54
|
+
Post.new(['I love mice', 'I love pigs'])]),
|
55
|
+
Category.new([Post.new(['I hate cats', 'I hate dogs']),
|
56
|
+
Post.new(['I hate mice', 'I hate pigs'])])]),
|
57
|
+
|
58
|
+
Blog.new([Category.new([Post.new(['Red is better than blue'])]),
|
59
|
+
Category.new([Post.new(['Blue is better than red'])])])
|
60
|
+
]
|
61
|
+
|
62
|
+
p words_in(blogs)
|
63
|
+
p words_in_list_bind(blogs)
|
64
|
+
p words_in_list_fmap(blogs)
|
65
|
+
p words_in_list_sugar(blogs)
|
data/examples/maybe.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'monadist'
|
2
|
+
require 'monadist/shims'
|
3
|
+
require 'active_support/all'
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
def lead_conversion_rate_history(customer_metrics)
|
8
|
+
customer_metrics[:lead][:conversion][:rate][:history]
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def lead_conversion_rate_history_safe(customer_metrics)
|
14
|
+
unless customer_metrics.nil?
|
15
|
+
unless customer_metrics[:lead].nil?
|
16
|
+
unless customer_metrics[:lead][:conversion].nil?
|
17
|
+
unless customer_metrics[:lead][:conversion][:rate].nil?
|
18
|
+
unless customer_metrics[:lead][:conversion][:rate][:history].nil?
|
19
|
+
customer_metrics[:lead][:conversion][:rate][:history]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
|
29
|
+
def lead_conversion_rate_history_fetch(customer_metrics)
|
30
|
+
customer_metrics ||= {}
|
31
|
+
customer_metrics.
|
32
|
+
fetch(:lead, {}).
|
33
|
+
fetch(:conversion, {}).
|
34
|
+
fetch(:rate, {}).
|
35
|
+
fetch(:history, nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
|
40
|
+
def lead_conversion_rate_history_try(customer_metrics)
|
41
|
+
customer_metrics.
|
42
|
+
try { |metrics| metrics[:lead] }.
|
43
|
+
try { |lead_metrics| lead_metrics[:conversion] }.
|
44
|
+
try { |lead_conversion_metrics| lead_conversion_metrics[:rate] }.
|
45
|
+
try { |lead_conversion_rate_metrics| lead_conversion_rate_metrics[:history] }
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
def lead_conversion_rate_history_maybe_bind(customer_metrics)
|
51
|
+
Monadist::Maybe.unit(customer_metrics).
|
52
|
+
bind { |metrics| Monadist::Maybe.unit(metrics[:lead]) }.
|
53
|
+
bind { |lead_metrics| Monadist::Maybe.unit(lead_metrics[:conversion]) }.
|
54
|
+
bind { |lead_conversion_metrics| Monadist::Maybe.unit(lead_conversion_metrics[:rate]) }.
|
55
|
+
bind { |lead_conversion_rate_metrics| Monadist::Maybe.unit(lead_conversion_rate_metrics[:history]) }.
|
56
|
+
value
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
def lead_conversion_rate_history_maybe_fmap(customer_metrics)
|
62
|
+
Monadist::Maybe.unit(customer_metrics).
|
63
|
+
fmap { |metrics| metrics[:lead] }.
|
64
|
+
fmap { |lead_metrics| lead_metrics[:conversion] }.
|
65
|
+
fmap { |lead_conversion_metrics| lead_conversion_metrics[:rate] }.
|
66
|
+
fmap { |lead_conversion_rate_metrics| lead_conversion_rate_metrics[:history] }.
|
67
|
+
value
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
def lead_conversion_rate_history_maybe_sugar(customer_metrics)
|
73
|
+
possibly_nil(customer_metrics)[:lead][:conversion][:rate][:history].value
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
customer_metrics = {
|
79
|
+
lead: {
|
80
|
+
conversion: {}
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
# p lead_conversion_rate_history customer_metrics
|
85
|
+
p lead_conversion_rate_history_safe customer_metrics
|
86
|
+
p lead_conversion_rate_history_fetch customer_metrics
|
87
|
+
p lead_conversion_rate_history_try customer_metrics
|
88
|
+
p lead_conversion_rate_history_maybe_bind customer_metrics
|
89
|
+
p lead_conversion_rate_history_maybe_fmap customer_metrics
|
90
|
+
p lead_conversion_rate_history_maybe_sugar customer_metrics
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Monadist
|
2
|
+
class Continuation < Monad
|
3
|
+
|
4
|
+
def initialize(&block)
|
5
|
+
@block = block
|
6
|
+
end
|
7
|
+
|
8
|
+
|
9
|
+
|
10
|
+
def bind(&block)
|
11
|
+
self.class.new do |next_block|
|
12
|
+
run do |value|
|
13
|
+
block.call(value).run(&next_block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def run(&block)
|
21
|
+
@block.call(block || lambda { |value| value })
|
22
|
+
end
|
23
|
+
|
24
|
+
|
25
|
+
|
26
|
+
def self.unit(value)
|
27
|
+
new { |next_block| next_block.call value }
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Monadist
|
2
|
+
class Identity < Monad
|
3
|
+
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
def bind(&block)
|
15
|
+
block.call value
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def self.unit(value)
|
21
|
+
new value
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Monadist
|
2
|
+
class List < Monad
|
3
|
+
|
4
|
+
attr_reader :values
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(values)
|
9
|
+
@values = values
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
def bind(&block)
|
15
|
+
self.class.new values.map(&block).flat_map(&:values)
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def self.unit(value)
|
21
|
+
if value.is_a? Array
|
22
|
+
new value
|
23
|
+
else
|
24
|
+
new [value]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Monadist
|
2
|
+
class Maybe < Monad
|
3
|
+
|
4
|
+
attr_reader :value
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
|
14
|
+
def bind(&block)
|
15
|
+
return self if value.nil?
|
16
|
+
|
17
|
+
block.call value
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
|
22
|
+
def self.unit(value)
|
23
|
+
new value
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Monadist
|
2
|
+
class Monad
|
3
|
+
|
4
|
+
def join
|
5
|
+
bind do |value|
|
6
|
+
raise "Wrapped value not a monad of type #{self.class.name}" unless value.instance_of? self.class
|
7
|
+
value
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def fmap(&block)
|
14
|
+
bind { |value| self.class.unit block.call value }
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def method_missing(*args, &block)
|
20
|
+
fmap { |value| value.public_send *args, &block }
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
def possibly_nil(value)
|
2
|
+
Monadist::Maybe.unit value
|
3
|
+
end
|
4
|
+
|
5
|
+
|
6
|
+
|
7
|
+
def list(*values)
|
8
|
+
Monadist::List.unit *values
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def with(value)
|
14
|
+
Monadist::Continuation.unit value
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def meanwhile_with(value)
|
20
|
+
Monadist::Meanwhile.unit value
|
21
|
+
end
|
data/lib/monadist.rb
ADDED
data/monadist.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'monadist/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "monadist"
|
8
|
+
spec.version = Monadist::VERSION
|
9
|
+
spec.authors = ["Zoltan Ormandi"]
|
10
|
+
spec.email = ["zoltan.ormandi@gmail.com"]
|
11
|
+
spec.summary = %q{Practical implementation of a couple of popular monads.}
|
12
|
+
spec.description = %q{A practical and useful Ruby implementation of a couple of popular monads. Method naming follows the Haskell convention so the gem can be used for trying to understand monads (mostly described in tutorials using Haskell).}
|
13
|
+
spec.homepage = "https://github.com/zormandi/monadist"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
spec.add_development_dependency "rspec"
|
24
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Monadist
|
4
|
+
describe Identity do
|
5
|
+
|
6
|
+
describe ".unit" do
|
7
|
+
it "returns a monad wrapping the specified value" do
|
8
|
+
result = Identity.unit "value"
|
9
|
+
|
10
|
+
expect(result).to be_an Identity
|
11
|
+
expect(result.value).to eq "value"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
describe "#value" do
|
17
|
+
it "returns the value wrapped by the monad" do
|
18
|
+
expect(Identity.unit("value").value).to eq "value"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
describe "#bind" do
|
24
|
+
it "calls the passed block with the value" do
|
25
|
+
expect { |block| Identity.unit("some value").bind &block }.to yield_with_args "some value"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
it "allows sending messages directly to the wrapped value" do
|
31
|
+
result = Identity.unit("test").length
|
32
|
+
|
33
|
+
expect(result).to be_an Identity
|
34
|
+
expect(result.value).to eq 4
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Monadist
|
4
|
+
describe List do
|
5
|
+
|
6
|
+
describe ".unit" do
|
7
|
+
it "returns a monad wrapping the specified value" do
|
8
|
+
result = List.unit "value"
|
9
|
+
|
10
|
+
expect(result).to be_a List
|
11
|
+
expect(result.values).to eq ["value"]
|
12
|
+
end
|
13
|
+
|
14
|
+
it "treats arrays as a list of values (for convenience)" do
|
15
|
+
expect(List.unit(%w(value1 value2)).values).to eq %w[value1 value2]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
describe "#values" do
|
21
|
+
it "returns the value(s) wrapped by the monad as an array" do
|
22
|
+
expect(List.unit("value").values).to eq %w[value]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
|
27
|
+
describe "#bind" do
|
28
|
+
it "calls the passed block with each value wrapped by the monad and returns a monad wrapping the resulting values" do
|
29
|
+
subject = List.unit [1, 2, 3]
|
30
|
+
|
31
|
+
result = subject.bind { |value| List.unit(value * 10) }
|
32
|
+
|
33
|
+
expect(result).to be_a List
|
34
|
+
expect(result.values).to eq [10, 20, 30]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "handles array values without flattening or nesting them" do
|
38
|
+
subject = List.unit [[1, 2], [3, 4]]
|
39
|
+
|
40
|
+
result = subject.bind { |value| List.unit [value] }
|
41
|
+
|
42
|
+
expect(result.values).to eq [[1, 2], [3, 4]]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
it "forwards messages to all the values it contains" do
|
48
|
+
subject = List.unit %w[HTTP_HOST HTTP_CONTENT_TYPE HTTP_DATE]
|
49
|
+
|
50
|
+
expect(subject[5..-1].tr("_", "-").capitalize.values).to eq %w[Host Content-type Date]
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Monadist
|
4
|
+
describe Maybe do
|
5
|
+
|
6
|
+
describe ".unit" do
|
7
|
+
it "returns a monad wrapping the specified value" do
|
8
|
+
result = Maybe.unit "value"
|
9
|
+
|
10
|
+
expect(result).to be_a Maybe
|
11
|
+
expect(result.value).to eq "value"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
describe "#value" do
|
17
|
+
it "returns the value wrapped by the monad" do
|
18
|
+
expect(Maybe.unit("value").value).to eq "value"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
describe "#bind" do
|
24
|
+
context "when wrapping nil" do
|
25
|
+
subject { Maybe.unit nil }
|
26
|
+
|
27
|
+
it "doesn't execute the passed block" do
|
28
|
+
expect { |block| subject.bind &block }.not_to yield_control
|
29
|
+
end
|
30
|
+
|
31
|
+
it "returns a monad wrapping nil" do
|
32
|
+
result = subject.bind {}
|
33
|
+
|
34
|
+
expect(result).to be_a Maybe
|
35
|
+
expect(result.value).to be_nil
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when wrapping a non-nil value" do
|
40
|
+
subject { Maybe.unit "some value" }
|
41
|
+
|
42
|
+
it "calls the passed block with the value" do
|
43
|
+
expect { |block| subject.bind &block }.to yield_with_args "some value"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns the block's result" do
|
47
|
+
result = subject.bind { |_| Maybe.unit "new value" }
|
48
|
+
|
49
|
+
expect(result).to be_a Maybe
|
50
|
+
expect(result.value).to eq "new value"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
context "when wrapping a non-nil value" do
|
57
|
+
it "forwards messages directly to the wrapped value and returns a monad wrapping the result" do
|
58
|
+
result = Maybe.unit("test").length
|
59
|
+
|
60
|
+
expect(result).to be_a Maybe
|
61
|
+
expect(result.value).to eq 4
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
context "when wrapping nil" do
|
67
|
+
it "disregards all messages and returns a monad wrapping nil" do
|
68
|
+
result = Maybe.unit(nil).concat('some')[:property] + 1
|
69
|
+
|
70
|
+
expect(result).to be_a Maybe
|
71
|
+
expect(result.value).to be_nil
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
RSpec.shared_examples "a monad" do
|
2
|
+
|
3
|
+
let(:test_value) { "test value" }
|
4
|
+
let(:f) { ->(value) { described_class.unit "f#{value}" } }
|
5
|
+
let(:g) { ->(value) { described_class.unit "g#{value}" } }
|
6
|
+
|
7
|
+
subject { described_class.unit test_value }
|
8
|
+
|
9
|
+
|
10
|
+
it "should obey the 1st monad law" do
|
11
|
+
expect(subject.bind &f).to eq f.call(test_value)
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
it "should obey the 2nd monad law" do
|
16
|
+
expect(subject.bind { |value| described_class.unit value }).to eq subject
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
it "should obey the 3rd monad law" do
|
21
|
+
chained_result = subject.bind { |value| f.call value }.bind { |value| g.call value }
|
22
|
+
nested_result = subject.bind { |v1| f.call(v1).bind { |v2| g.call v2 } }
|
23
|
+
|
24
|
+
expect(nested_result).to eq chained_result
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: monadist
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Zoltan Ormandi
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-11-18 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.7'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.7'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '10.0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '10.0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: A practical and useful Ruby implementation of a couple of popular monads.
|
63
|
+
Method naming follows the Haskell convention so the gem can be used for trying to
|
64
|
+
understand monads (mostly described in tutorials using Haskell).
|
65
|
+
email:
|
66
|
+
- zoltan.ormandi@gmail.com
|
67
|
+
executables: []
|
68
|
+
extensions: []
|
69
|
+
extra_rdoc_files: []
|
70
|
+
files:
|
71
|
+
- .gitignore
|
72
|
+
- .rspec
|
73
|
+
- .travis.yml
|
74
|
+
- Gemfile
|
75
|
+
- LICENSE.txt
|
76
|
+
- README.md
|
77
|
+
- Rakefile
|
78
|
+
- examples/continuation/github_fetch_original.rb
|
79
|
+
- examples/continuation/github_fetch_refactored_continuation.rb
|
80
|
+
- examples/continuation/github_fetch_refactored_meanwhile.rb
|
81
|
+
- examples/general.rb
|
82
|
+
- examples/identity.rb
|
83
|
+
- examples/list.rb
|
84
|
+
- examples/maybe.rb
|
85
|
+
- lib/monadist.rb
|
86
|
+
- lib/monadist/continuation.rb
|
87
|
+
- lib/monadist/identity.rb
|
88
|
+
- lib/monadist/list.rb
|
89
|
+
- lib/monadist/maybe.rb
|
90
|
+
- lib/monadist/meanwhile.rb
|
91
|
+
- lib/monadist/monad.rb
|
92
|
+
- lib/monadist/shims.rb
|
93
|
+
- lib/monadist/version.rb
|
94
|
+
- monadist.gemspec
|
95
|
+
- spec/monadist/identity_spec.rb
|
96
|
+
- spec/monadist/list_spec.rb
|
97
|
+
- spec/monadist/maybe_spec.rb
|
98
|
+
- spec/monadist_spec.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
- spec/support/shared_examples_for_monad.rb
|
101
|
+
homepage: https://github.com/zormandi/monadist
|
102
|
+
licenses:
|
103
|
+
- MIT
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
none: false
|
110
|
+
requirements:
|
111
|
+
- - ! '>='
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
115
|
+
none: false
|
116
|
+
requirements:
|
117
|
+
- - ! '>='
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 1.8.23.2
|
123
|
+
signing_key:
|
124
|
+
specification_version: 3
|
125
|
+
summary: Practical implementation of a couple of popular monads.
|
126
|
+
test_files:
|
127
|
+
- spec/monadist/identity_spec.rb
|
128
|
+
- spec/monadist/list_spec.rb
|
129
|
+
- spec/monadist/maybe_spec.rb
|
130
|
+
- spec/monadist_spec.rb
|
131
|
+
- spec/spec_helper.rb
|
132
|
+
- spec/support/shared_examples_for_monad.rb
|