monadist 0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|