monadist 0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,16 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
15
+ /.idea
16
+ /.ruby-version
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.1.3
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in monadist.gemspec
4
+ gemspec
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
@@ -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
@@ -0,0 +1,6 @@
1
+ require 'monadist'
2
+
3
+ puts Monadist::Identity.unit("Hello").
4
+ bind { |value| Monadist::Identity.unit value + " world!" }.
5
+ value
6
+
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,15 @@
1
+ module Monadist
2
+ class Meanwhile < Continuation
3
+
4
+ def bind(&block)
5
+ self.class.new do |next_block|
6
+ Thread.new do
7
+ run do |value|
8
+ block.call(value).run(&next_block)
9
+ end
10
+ end
11
+ end
12
+ end
13
+
14
+ end
15
+ 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
@@ -0,0 +1,3 @@
1
+ module Monadist
2
+ VERSION = '0.1'
3
+ end
data/lib/monadist.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'monadist/version'
2
+ require 'monadist/monad'
3
+ require 'monadist/identity'
4
+ require 'monadist/maybe'
5
+ require 'monadist/list'
6
+ require 'monadist/continuation'
7
+ require 'monadist/meanwhile'
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
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe Monadist do
4
+ it 'should have a version number' do
5
+ expect(Monadist::VERSION).to eq '0.1'
6
+ end
7
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'monadist'
@@ -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