yopt 0.1.0

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: da7d8098ce8b1dcc66d5e8f6ba3baf208daf805d
4
+ data.tar.gz: 6ae68982bb5ff8a3dc09cba07961c04e51b46fa3
5
+ SHA512:
6
+ metadata.gz: 57fa46f4d6f766ac6fcd87a4b261fd049e8e3757c5578efa2851235b096f1086eb5cb1c801d0ca2dfa0266d6f5d593564677f4a170be22636cbcf3708517ddd9
7
+ data.tar.gz: 577ab29f9bdb5283d93941f24995c5d8b4cb02a7847faa2ff66e5018b8c7308f582adac0439ea1ff5c1dd7039fa68d32f8defed983b61dfd34122ddd0f043b0f
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor
11
+ /yopt-0.1.0.gem
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.2.0
4
+ before_install: gem install bundler -v 1.10.6
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'pry', '~> 0.10.3'
4
+ gem 'rb-readline', '~> 0.5.3'
5
+ # Specify your gem's dependencies in yopt.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 lorenzo.barasti
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,206 @@
1
+ # Yopt
2
+
3
+ A [Scala](http://www.scala-lang.org/api/current/index.html#scala.Option) inspired gem that introduces `Option`s to Ruby while aiming for an idiomatic API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'yopt'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install yopt
20
+
21
+ ## Basic usage
22
+
23
+ The Option type models the possible absence of a value. It lets us deal with the uncertainty related to such a value being there without having to resort to errors or conditional blocks.
24
+
25
+ Instances of Option are either an instance of `Yopt::Some` - meaning the option contains a value - or the object `Yopt::None` - meaning the option is *empty*.
26
+
27
+ ```ruby
28
+ require 'yopt'
29
+
30
+ some = Yopt::Some.new(42)
31
+ none = Yopt::None
32
+ ```
33
+
34
+ We can access and manipulate the optional value by passing a block to `Option#map`.
35
+
36
+ ```ruby
37
+ some.map {|value| value + 2} # returns Some(44)
38
+ none.map {|value| value + 2} # returns None
39
+ ```
40
+
41
+ When we are not interested in the result of a computation on the optional value, it is a good practice to use `Option#each` rather than `Option#map`. That will make our intention clearer.
42
+
43
+ ```ruby
44
+ some.each {|value| puts value} # prints 42
45
+ none.each {|value| puts value} # does not print anything
46
+ ```
47
+
48
+ We can safely retrieve the optional value by passing a default value to `Option#get_or_else`
49
+
50
+ ```ruby
51
+ some.get_or_else 0 # returns 42
52
+ none.get_or_else 0 # returns 0
53
+ ```
54
+
55
+ We can also filter the optional value depending on how it evaluates against a block via `Option#select`
56
+
57
+ ```ruby
58
+ some.select {|value| value < 0} # returns None
59
+ none.select {|value| value < 0} # returns None
60
+ some.select {|value| value > 0} # returns Some(42)
61
+ ```
62
+
63
+ We can easily turn any object into an Option by means of `Option.call` - aliased to `Option.[]` for convenience.
64
+ For instance, this is useful when dealing with functions that might return `nil` to express the absence of a result.
65
+
66
+ ```ruby
67
+ Yopt::Option[nil] # returns None
68
+ Yopt::Option[42] # returns Some(42)
69
+ ```
70
+
71
+
72
+ A combination of the few methods just introduced already allows us to implement some pretty interesting logic. Checkout `basics.rb` in the docs folder to get some inspiration.
73
+
74
+ ## Why opt?
75
+
76
+ Using `Option`s reduces the amount of branching in our code and lets us deal with exceptional cases in a seamless way. No more check-for-nil, no more `rescue` blocks, just plain and simple data transformation.
77
+
78
+ It also makes our code safer by treating *the absence of something* like a fully fledged object, and enables us to use the Null Object Pattern everywhere we want without the overhead of having to write specialized Null-type classes for different classes.
79
+
80
+ ## Advanced Usage
81
+ ### #reduce
82
+ Given an Option `opt`, a value `c` and a lambda `f`,
83
+ ```
84
+ opt.reduce(c, &f)
85
+ ```
86
+ returns `c` if `opt` is `None`, and `f.(c, opt)` otherwise.
87
+
88
+ This is a shortcut to
89
+ ```
90
+ opt.map{|v| f.(c,v)}.get_or_else(c)`
91
+ ```
92
+
93
+
94
+ ### #flatten and #flat_map
95
+ When working with functions returning `Option`, we might end up dealing with nested options...
96
+ ```ruby
97
+ maybe_sqrt = lambda {|x| Yopt::Option[x >= 0 ? Math.sqrt(x) : nil]}
98
+ maybe_increment = lambda {|x| Yopt::Option[x > 1 ? x + 1 : nil]}
99
+
100
+ maybe_sqrt.(4).map {|v| maybe_increment.(v)} # Some(Some(3.0))
101
+ maybe_sqrt.(1).map {|v| maybe_increment.(v)} # Some(None)
102
+ ```
103
+
104
+ Usually, this is not what we want, so we call `Option#flatten` on the result
105
+ ```ruby
106
+ maybe_sqrt.(4).map {|v| maybe_increment.(v)}.flatten # Some(3.0)
107
+ maybe_sqrt.(1).map {|v| maybe_increment.(v)}.flatten # None
108
+ ```
109
+
110
+ `Option#flat_map` combines the two calls into one
111
+
112
+ ```ruby
113
+ maybe_sqrt.(4).flat_map {|v| maybe_increment.(v)} # Some(3.0)
114
+ maybe_sqrt.(1).flat_map {|v| maybe_increment.(v)} # None
115
+ ```
116
+
117
+ A difference to keep in mind is that `#flatten` will raise an error if the wrapped value does not respond to `#to_ary`
118
+ ```ruby
119
+ Yopt.Option[42].flatten # raises TypeError: Argument must be an array-like object. Found Fixnum
120
+ ```
121
+ whereas #flat_map behaves like #map when the passed block does not return an array-like value
122
+ ```ruby
123
+ Yopt.Option[42].flat_map{|v| v} # returns Some(42)
124
+ ```
125
+
126
+
127
+ ### #zip
128
+ When dealing with a set of `Option` instances, we might want to ensure that they are all defined - i.e. not __empty__ - before continuing a computation...
129
+ ```ruby
130
+ email_opt.each(&send_pass_recovery) unless (email_opt.empty? or captcha_opt.empty?)
131
+ ```
132
+
133
+ We can avoid `empty?` checks by using `Option#zip`
134
+ ```ruby
135
+ email_opt.zip(captcha_opt).each{|(email,_)| send_pass_recovery(email)}
136
+ ```
137
+
138
+ `Option#zip` returns `None` if any of the arguments is `None` or if the caller is `None`
139
+ ```ruby
140
+ Yopt::None.zip Option.[42] # None
141
+ Option.[42].zip Yopt::None # None
142
+ Option.[42].zip Option.[0], Yopt::None, Option.[-1] # None
143
+ ```
144
+
145
+ When both the caller and all the arguments are defined then `zip` collects all the values in an Array wrapped in a `Yopt::Some`
146
+
147
+ ```ruby
148
+ Option.[42].zip Option.[0], Option.["str"] # Some([42, 0, "str"])
149
+ ```
150
+
151
+
152
+ ### #grep
153
+ We often find ourselves filtering data before applying a transformation...
154
+
155
+ ```ruby
156
+ opt.filter {|v| (1...10).include? v}.map {|v| v + 1}
157
+ ```
158
+
159
+ In this scenario, `Option#grep` can sometimes make the code more concise
160
+
161
+ ```ruby
162
+ opt.grep(1...10) {|v| v + 1}
163
+ ```
164
+
165
+ `Option#grep` supports lambdas as well
166
+
167
+ ```ruby
168
+ is_positive = lambda {|x| x > 0}
169
+
170
+ opt.grep(is_positive) {|v| Math.log(v)}
171
+ # is equivalent to
172
+ opt.filter(&is_positive).map {|v| Math.log(v)}
173
+ ```
174
+
175
+
176
+ ## Haskell Data.Maybe cheat sheet
177
+
178
+ Some (None?) might enjoy a comparison with Haskell's [Maybe](https://hackage.haskell.org/package/base/docs/Data-Maybe.html). Here is how the Data.Maybe API translate to Yopt.
179
+ ```ruby
180
+ maybe default f opt -> opt.map(&f).get_or_else(default)
181
+ isJust opt -> not opt.empty?
182
+ isNothing opt -> opt.empty?
183
+ fromJust opt -> opt.get
184
+ fromMaybe default opt -> opt.get_or_else default
185
+ listToMaybe list -> Option.ary_to_type list
186
+ maybeToList opt -> opt.to_a
187
+ catMaybes listOfOptions -> listOfOptions.flatten
188
+ mapMaybe f list -> list.flat_map &f
189
+ ```
190
+
191
+
192
+ ## Development
193
+
194
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
195
+
196
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
197
+
198
+
199
+ ## Contributing
200
+
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lbarasti/yopt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
202
+
203
+
204
+ ## License
205
+
206
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,39 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :docs do
11
+ partial_keyword = '<<<<<'
12
+ ignore_keyword = "# IGNORE\n"
13
+ comment_keyword = "# COMMENT\n"
14
+ src = './docs/README.template.md'
15
+ target = './README.md'
16
+ content = File.readlines(src).flat_map {|line|
17
+ if line.lstrip.start_with?(partial_keyword)
18
+ partial_file = line.lstrip[partial_keyword.size...-1]
19
+ sh 'ruby', partial_file
20
+ File.readlines partial_file
21
+ else
22
+ line
23
+ end
24
+ }
25
+ File.open(target, 'w') {|f|
26
+ content.reject{|line|
27
+ line.end_with?(ignore_keyword)
28
+ }.each_cons(2){|line1,line2|
29
+ next if line1.end_with?(comment_keyword)
30
+ if line2.end_with?(comment_keyword)
31
+ f.puts "#{line1.chomp} # #{line2.match(/'(.*)'/)[1]}\n"
32
+ else
33
+ f.puts line1
34
+ end
35
+ }
36
+ }
37
+ end
38
+
39
+ task :default => :test
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "yopt"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ require "pry"
10
+ Pry.start
11
+
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,207 @@
1
+ # Yopt
2
+
3
+ A [Scala](http://www.scala-lang.org/api/current/index.html#scala.Option) inspired gem that introduces `Option`s to Ruby while aiming for an idiomatic API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'yopt'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install yopt
20
+
21
+ ## Basic usage
22
+
23
+ The Option type models the possible absence of a value. It lets us deal with the uncertainty related to such a value being there without having to resort to errors or conditional blocks.
24
+
25
+ Instances of Option are either an instance of `Yopt::Some` - meaning the option contains a value - or the object `Yopt::None` - meaning the option is *empty*.
26
+
27
+ ```ruby
28
+ require 'yopt'
29
+
30
+ some = Yopt::Some.new(42)
31
+ none = Yopt::None
32
+ ```
33
+
34
+ We can access and manipulate the optional value by passing a block to `Option#map`.
35
+
36
+ ```ruby
37
+ some.map {|value| value + 2} # returns Some(44)
38
+ none.map {|value| value + 2} # returns None
39
+ ```
40
+
41
+ When we are not interested in the result of a computation on the optional value, it is a good practice to use `Option#each` rather than `Option#map`. That will make our intention clearer.
42
+
43
+ ```ruby
44
+ some.each {|value| puts value} # prints 42
45
+ none.each {|value| puts value} # does not print anything
46
+ ```
47
+
48
+ We can safely retrieve the optional value by passing a default value to `Option#get_or_else`
49
+
50
+ ```ruby
51
+ some.get_or_else 0 # returns 42
52
+ none.get_or_else 0 # returns 0
53
+ ```
54
+
55
+ We can also filter the optional value depending on how it evaluates against a block via `Option#select`
56
+
57
+ ```ruby
58
+ some.select {|value| value < 0} # returns None
59
+ none.select {|value| value < 0} # returns None
60
+ some.select {|value| value > 0} # returns Some(42)
61
+ ```
62
+
63
+ We can easily turn any object into an Option by means of `Option.call` - aliased to `Option.[]` for convenience.
64
+ For instance, this is useful when dealing with functions that might return `nil` to express the absence of a result.
65
+
66
+ ```ruby
67
+ Yopt::Option[nil] # returns None
68
+ Yopt::Option[42] # returns Some(42)
69
+ ```
70
+
71
+
72
+ A combination of the few methods just introduced already allows us to implement some pretty interesting logic. Checkout `basics.rb` in the docs folder to get some inspiration.
73
+
74
+ ## Why opt?
75
+
76
+ Using `Option`s reduces the amount of branching in our code and lets us deal with exceptional cases in a seamless way. No more check-for-nil, no more `rescue` blocks, just plain and simple data transformation.
77
+
78
+ It also makes our code safer by treating *the absence of something* like a fully fledged object, and enables us to use the Null Object Pattern everywhere we want without the overhead of having to write specialized Null-type classes for different classes.
79
+
80
+ ## Advanced Usage
81
+ ### #reduce
82
+ Given an Option `opt`, a value `c` and a lambda `f`,
83
+ ```
84
+ opt.reduce(c, &f)
85
+ ```
86
+ returns `c` if `opt` is `None`, and `f.(c, opt)` otherwise.
87
+
88
+ This is a shortcut to
89
+ ```
90
+ opt.map{|v| f.(c,v)}.get_or_else(c)`
91
+ ```
92
+
93
+
94
+ ### #flatten and #flat_map
95
+ When working with functions returning `Option`, we might end up dealing with nested options...
96
+ ```ruby
97
+ maybe_sqrt = lambda {|x| Yopt::Option[x >= 0 ? Math.sqrt(x) : nil]}
98
+ maybe_increment = lambda {|x| Yopt::Option[x > 1 ? x + 1 : nil]}
99
+
100
+ maybe_sqrt.(4).map {|v| maybe_increment.(v)} # Some(Some(3.0))
101
+ maybe_sqrt.(1).map {|v| maybe_increment.(v)} # Some(None)
102
+ ```
103
+
104
+ Usually, this is not what we want, so we call `Option#flatten` on the result
105
+ ```ruby
106
+ maybe_sqrt.(4).map {|v| maybe_increment.(v)}.flatten # Some(3.0)
107
+ maybe_sqrt.(1).map {|v| maybe_increment.(v)}.flatten # None
108
+ ```
109
+
110
+ `Option#flat_map` combines the two calls into one
111
+
112
+ ```ruby
113
+ maybe_sqrt.(4).flat_map {|v| maybe_increment.(v)} # Some(3.0)
114
+ maybe_sqrt.(1).flat_map {|v| maybe_increment.(v)} # None
115
+ ```
116
+
117
+ A difference to keep in mind is that `#flatten` will raise an error if the wrapped value does not respond to `#to_ary`
118
+ ```ruby
119
+ Yopt.Option[42].flatten # raises TypeError: Argument must be an array-like object. Found Fixnum
120
+ ```
121
+ whereas #flat_map behaves like #map when the passed block does not return an array-like value
122
+ ```ruby
123
+ Yopt.Option[42].flat_map{|v| v} # returns Some(42)
124
+ ```
125
+
126
+
127
+ ### #zip
128
+ When dealing with a set of `Option` instances, we might want to ensure that they are all defined - i.e. not __empty__ - before continuing a computation...
129
+ ```ruby
130
+ email_opt.each(&send_pass_recovery) unless (email_opt.empty? or captcha_opt.empty?)
131
+ ```
132
+
133
+ We can avoid `empty?` checks by using `Option#zip`
134
+ ```ruby
135
+ email_opt.zip(captcha_opt).each{|(email,_)| send_pass_recovery(email)}
136
+ ```
137
+
138
+ `Option#zip` returns `None` if any of the arguments is `None` or if the caller is `None`
139
+ ```ruby
140
+ Yopt::None.zip Option.[42] # None
141
+ Option.[42].zip Yopt::None # None
142
+ Option.[42].zip Option.[0], Yopt::None, Option.[-1] # None
143
+ ```
144
+
145
+ When both the caller and all the arguments are defined then `zip` collects all the values in an Array wrapped in a `Yopt::Some`
146
+
147
+ ```ruby
148
+ Option.[42].zip Option.[0], Option.["str"] # Some([42, 0, "str"])
149
+ ```
150
+
151
+
152
+ ### #grep
153
+ We often find ourselves filtering data before applying a transformation...
154
+
155
+ ```ruby
156
+ opt.filter {|v| (1...10).include? v}.map {|v| v + 1}
157
+ ```
158
+
159
+ In this scenario, `Option#grep` can sometimes make the code more concise
160
+
161
+ ```ruby
162
+ opt.grep(1...10) {|v| v + 1}
163
+ ```
164
+
165
+ `Option#grep` supports lambdas as well
166
+
167
+ ```ruby
168
+ is_positive = lambda {|x| x > 0}
169
+
170
+ opt.grep(is_positive) {|v| Math.log(v)}
171
+ # is equivalent to
172
+ opt.filter(&is_positive).map {|v| Math.log(v)}
173
+ ```
174
+
175
+
176
+ ## Haskell Data.Maybe cheat sheet
177
+
178
+ Some (None?) might enjoy a comparison with Haskell's [Maybe](https://hackage.haskell.org/package/base/docs/Data-Maybe.html). Here is how the Data.Maybe API translate to Yopt.
179
+ ```ruby
180
+ maybe default f opt -> opt.map(&f).get_or_else(default)
181
+ isJust opt -> not opt.empty?
182
+ isNothing opt -> opt.empty?
183
+ fromJust opt -> opt.get
184
+ fromMaybe default opt -> opt.get_or_else default
185
+ listToMaybe list -> Option.ary_to_type list
186
+ maybeToList opt -> opt.to_a
187
+ catMaybes listOfOptions -> listOfOptions.flatten
188
+ mapMaybe f list -> list.flat_map &f
189
+ ```
190
+
191
+
192
+ ## Development
193
+
194
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
195
+
196
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
197
+
198
+
199
+ ## Contributing
200
+
201
+ Bug reports and pull requests are welcome on GitHub at https://github.com/lbarasti/yopt. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
202
+
203
+
204
+ ## License
205
+
206
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
207
+
@@ -0,0 +1,37 @@
1
+ require_relative '../test/test_helper' # IGNORE
2
+ # IGNORE
3
+ require 'test/unit' # IGNORE
4
+ include Test::Unit::Assertions # IGNORE
5
+ # IGNORE
6
+ class Cache < Hash
7
+ def maybe_get key
8
+ Yopt::Option[self[key]]
9
+ end
10
+
11
+ def store_if_some key, opt_value
12
+ opt_value.each {|value| self.store(key, value)}
13
+ end
14
+ end
15
+
16
+ def maybe_sqrt value
17
+ opt = Yopt::Option[value]
18
+ opt.select {|value| value >= 0}
19
+ .map {|value| Math.sqrt(value)}
20
+ end
21
+
22
+ def get_info cache, key
23
+ cache.maybe_get(key)
24
+ .map {|value| "found value %.2f for key %s" % [value, key]}
25
+ .get_or_else "value not found for key #{key}"
26
+ end
27
+
28
+ cache = Cache.new
29
+
30
+ cache.store_if_some 42, maybe_sqrt(42)
31
+
32
+ key = 42 + rand(2) # could be either 42 or 43
33
+
34
+ puts get_info(cache, key)
35
+ # IGNORE
36
+ same_string(cache.maybe_get(key)).(Yopt::Some.new(Math.sqrt(42)).to_s) if key == 42 # IGNORE
37
+ same_string(cache.maybe_get(key)).(Yopt::None.to_s) if key != 42 # IGNORE
@@ -0,0 +1,21 @@
1
+ ## About Enumerable methods
2
+ By including `Enumerable`, `Option` gives access to a set of methods which are either redundant or not really meaningful for a `Some` or a `None` object
3
+
4
+ ```ruby
5
+ :slice_after :slice_before :slice_when :take :take_while :drop :drop_while :chunk :sort_by :each_entry :each_slice :group_by :minmax_by :sort :each_with_index :partition :find_all :max_by :min_by :reverse_each :each_cons :min :max :minmax
6
+ ```
7
+
8
+ On the other hand, all the methods returning a boolean work as expected
9
+ ```ruby
10
+ :all? :any? :include? :none? :one? :member?
11
+ ```
12
+
13
+ The following methods work in a predictable way out-of-the-box
14
+ ```ruby
15
+ :reduce :inject :each_with_object :cycle :to_set :find_index :find :detect :first :count :to_a :entries :to_h
16
+ ```
17
+
18
+ While the following methods have custom definition to always return an `Option` rather than an `Array`
19
+ ```ruby
20
+ :collect :map :flat_map :collect_concat :reject :select :zip :grep
21
+ ```
@@ -0,0 +1,51 @@
1
+ require_relative '../test/test_helper' # IGNORE
2
+ # IGNORE
3
+ require 'test/unit' # IGNORE
4
+ include Test::Unit::Assertions # IGNORE
5
+
6
+ name2phone = [[:a, "+1 310-000-001"],
7
+ [:b, "+1 323-000-002"],
8
+ [:d, "+1 310-000-003"],
9
+ [:e, "+1 213-000-004"],
10
+ [:f, "+1 323-000-005"],
11
+ [:h, "+1 213-000-006"],
12
+ [:k, "+1 213-000-007"],
13
+ [:s, "+1 310-000-009"],
14
+ [:w, "+1 800-000-010"]]
15
+
16
+ phone2postcode = [["+1 310-000-001", "CA 90210"],
17
+ ["+1 310-000-003", "CA 90210"],
18
+ ["+1 323-000-005", "CA 90028"],
19
+ ["+1 213-000-006", "CA 90027"],
20
+ ["+1 213-000-007", "CA 90027"],
21
+ ["+1 800-000-010", "CA 91608"]]
22
+
23
+ postcode2income = [["CA 90210", "$80000"],
24
+ ["CA 91608", "$65000"]]
25
+
26
+
27
+ lookup = -> (table, key) {
28
+ row_or_nil = table.find { |row| row.first == key }
29
+ Yopt::Option[row_or_nil] | :last
30
+ }
31
+
32
+ same_string( # IGNORE
33
+ lookup.(name2phone, :s)
34
+ ).('Some(+1 310-000-009)') # COMMENT
35
+
36
+ same_string( # IGNORE
37
+ lookup.(name2phone, :x)
38
+ ).('None') # COMMENT
39
+
40
+ same_string( # IGNORE
41
+ same_string( # IGNORE
42
+ same_string( # IGNORE
43
+ lookup.(name2phone, :a)
44
+ ).('Some(+1 310-000-001)') # COMMENT
45
+ .flat_map {|phone| lookup.(phone2postcode, phone)}
46
+ ).('Some(CA 90210)') # COMMENT
47
+ .flat_map {|postcode| lookup.(postcode2income, postcode)}
48
+ ).('Some($80000)') # COMMENT
49
+
50
+ # or if you want to go crazy-functional
51
+ lookup.(name2phone, :a) | lookup.curry[phone2postcode] | lookup.curry[postcode2income]
@@ -0,0 +1,8 @@
1
+ Some might find that having to specify the scope every time we want to use `Some` or `None` a bit too verbose. If that is the case, please consider that module scoping is a good receipe to avoid hideous naming clashes.
2
+
3
+ If you understand the risk then you can either `include Yopt` or cherry-pick the tools you need from the module like so:
4
+
5
+ ```ruby
6
+ Some = Yopt::Some
7
+ None = Yopt::None
8
+ ```
@@ -0,0 +1,12 @@
1
+ require 'yopt'
2
+
3
+ compute_increase = Yopt.lift {|user| 10000 if ['Bob', 'Joe', 'Eve'].member?(user)}
4
+
5
+ base_salary = 20000
6
+ user = ['Noel', 'Eve'].sample
7
+ salary_increase_opt = compute_increase.(user) # None | Some(10000)
8
+
9
+ salary_increase_opt.reduce(base_salary, &:+) # 20000 | 30000
10
+ # is equivalent to
11
+ salary_increase_opt.map{|increase| base_salary + increase} # None | Some(30000)
12
+ .get_or_else(base_salary) # 20000 | 30000
@@ -0,0 +1,77 @@
1
+ require_relative '../test/test_helper' # IGNORE
2
+ # IGNORE
3
+ require 'test/unit' # IGNORE
4
+ include Test::Unit::Assertions # IGNORE
5
+
6
+ # You can create an option from any object `obj`
7
+ # In general, any object gets wrapped into a `Some` instance
8
+ some = Yopt::Option[42] # Some(42)
9
+ # The only exception being `nil`, which turns into `None`
10
+ none = Yopt::Option[nil] # None
11
+
12
+ # since Some and None have the same API, let's choose one of the two
13
+ # randomly, and record the intermediate result of each computation
14
+ # for both None and Some(42) following the convention <result_if_none> | <result_if_some>
15
+ opt = [none, some].sample
16
+
17
+ opt # None | Some(42)
18
+ .select{|x| x > 0} # None | Some(42)
19
+ .map(&:succ) # None | Some(43)
20
+ .get_or_else(1) # 1 | 43
21
+
22
+ # Calling select/reject on a Some instance can return None
23
+ # Whereas a None can never be transformed into a Some
24
+ # None | Some(42)
25
+ opt.select{ |x| x < 0 } # None | None
26
+
27
+ # One of the perks of working with options is that it allow us to stay
28
+ # away from `nil` and `if` statements. You can still revert to using them if you need to though
29
+ # There always is a better way to go though.
30
+ opt.or_nil # nil | 42
31
+ # Even better, if the option is None we can return a default value straight away
32
+ opt.get_or_else 31 # 31 | 42
33
+
34
+ # If we intend to stay in the Option domain for some more time we can swap None with an other Option
35
+ opt.or_else(Yopt::Option[61]) # Some(61) | Some(42)
36
+ .reject(&:even?) # Some(61) | None
37
+ .get_or_else 0 # 61 | 0
38
+
39
+ # Once in the Option domain, it's nice to avoid accessing
40
+ # the content of an option explicitly as far as possible.
41
+ # You might eventually need to extract the value wrapped by
42
+ # Option.
43
+ begin # None | Some(42)
44
+ opt.get # raises a RuntimeError | 42
45
+ rescue RuntimeError
46
+ puts "cannot call #get on #{opt}"
47
+ end
48
+
49
+ # This does not look safe. Luckily we can check if the option
50
+ # is None by calling `empty?` on it
51
+ opt.empty? # true | false
52
+
53
+
54
+ # v = s2.reduce(2){|default, opt_val| opt_val - default} # 47
55
+ # n = Yopt::Some.new(v) # Some(47)
56
+ # .collect{|x| if x % 2 == 1 then x + 1 end} # None
57
+ # .map{|x| x ** 2} # None
58
+
59
+ # if s1.reduce(s2.get, &:-) < 0 && s1.include?(42) # true
60
+ # s2.collect{ n.empty? && n } # Some(None)
61
+ # .flatten # None
62
+ # .or_nil # nil
63
+ # end
64
+ # head_opt = Util.lift &:first
65
+ # old_school_validate = -> email {if email.end_with?('my-domain.com') then email else nil end}
66
+ # valid = 'user@my-domain.com'
67
+ # invalid = '@gmail.com'
68
+ # old_school_validate.(invalid).must_equal nil
69
+ # old_school_validate.(valid).wont_be_nil
70
+ # old_school_validate.(valid).must_equal valid
71
+
72
+ # brand_new_validate = Util.lift &old_school_validate
73
+ # brand_new_validate.(invalid).must_equal None
74
+ # brand_new_validate.(valid).must_equal Some.new(valid)
75
+
76
+ # user = brand_new_validate.(valid) | Util.lift{|email| email.split('@')[0]} | :upcase
77
+ # user.get.must_equal "USER"
@@ -0,0 +1,30 @@
1
+ require_relative '../test/test_helper' # IGNORE
2
+ # IGNORE
3
+ require 'test/unit' # IGNORE
4
+ include Test::Unit::Assertions # IGNORE
5
+
6
+ validate_email = Yopt.lift {|x| x if x.end_with? "@domain.com"}
7
+ validate_password = Yopt.lift {|x| x if x.size > 8}
8
+ hash_f = lambda {|str| str.each_char.map(&:ord).reduce(:+)}
9
+
10
+ # returns Yopt::Some(Fixnum) if email and password are both valid
11
+ # returns Yopt::None otherwise
12
+ hash_credentials = -> (email, password) do
13
+ maybe_email = validate_email.(email)
14
+ maybe_password = validate_password.(password)
15
+
16
+ maybe_email.zip(maybe_password)
17
+ .map {|(valid_email, valid_pass)| hash_f.(valid_email + valid_pass)}
18
+ end
19
+
20
+ same_string( # IGNORE
21
+ hash_credentials.('invalid_mail', 'valid_pass')
22
+ ).('None') # COMMENT
23
+ # IGNORE
24
+ same_string( # IGNORE
25
+ hash_credentials.('valid@domain.com', 'invalid')
26
+ ).('None') # COMMENT
27
+ # IGNORE
28
+ same_string( # IGNORE
29
+ hash_credentials.('valid@domain.com', 'valid_pass')
30
+ ).('Some(2651)') # COMMENT
@@ -0,0 +1,88 @@
1
+ require 'yopt/version'
2
+
3
+ module Yopt
4
+ def self.lift &block
5
+ block or raise ArgumentError, 'missing block'
6
+ -> (*args, &other_block) {Option.(block.call *args, &other_block)}
7
+ end
8
+ module Option
9
+ include Enumerable
10
+ def self.ary_to_type value
11
+ raise Option.invalid_argument('an array-like object', value) unless value.respond_to? :to_ary
12
+ return value if value.is_a? Option
13
+ if value.to_ary.empty? then None else Some.new(value.to_ary.first) end
14
+ end
15
+ def self.call(value)
16
+ if value.nil? then None else Some.new(value) end
17
+ end
18
+ singleton_class.send(:alias_method, :[], :call)
19
+ def each &block
20
+ to_ary.each &block
21
+ end
22
+ %i(map flat_map select reject collect collect_concat).each do |method|
23
+ define_method method, ->(&block) {
24
+ block or return enum_for(method)
25
+ Option.ary_to_type super(&block)
26
+ }
27
+ end
28
+ def grep(pattern, &block)
29
+ Option.ary_to_type super
30
+ end
31
+ def flatten
32
+ return self if empty?
33
+ Option.ary_to_type self.get
34
+ end
35
+ def zip *others
36
+ return None if self.empty? || others.any?(&:empty?)
37
+ collection = others.reduce(self.to_a, &:concat)
38
+ Some.new collection
39
+ end
40
+ def | lambda
41
+ self.flat_map &lambda # slow but easy to read + supports symbols out of the box
42
+ end
43
+ def ^ lambda
44
+ self | Yopt.lift(&lambda)
45
+ end
46
+ def or_else other
47
+ raise Option.invalid_argument('an Option', other) unless other.is_a? Option
48
+ if empty? then other else self end
49
+ end
50
+ def get_or_else default
51
+ if empty? then default else self.get end
52
+ end
53
+ def or_nil
54
+ get_or_else nil
55
+ end
56
+ def inspect() to_s end
57
+ private
58
+ def self.invalid_argument type_str, arg
59
+ TypeError.new "Argument must be #{type_str}. Found #{arg.class}"
60
+ end
61
+ end
62
+ class Some
63
+ include Option
64
+ def initialize value
65
+ @value = value.freeze
66
+ end
67
+ def get() @value end
68
+ def empty?() false end
69
+ def to_s() "Some(#{get})" end
70
+ def to_ary() [get] end
71
+ def == other
72
+ other.is_a?(Some) && self.get == other.get
73
+ end
74
+ def === other
75
+ other.is_a?(Some) && self.get === other.get
76
+ end
77
+ end
78
+
79
+ class NoneClass
80
+ include Option
81
+ def get() raise "Cannot call ##{__method__} on #{self}" end
82
+ def empty?() true end
83
+ def to_s() 'None' end
84
+ def to_ary() [] end
85
+ end
86
+
87
+ None = NoneClass.new
88
+ end
@@ -0,0 +1,3 @@
1
+ module Yopt
2
+ VERSION = "0.1.0"
3
+ end
@@ -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 'yopt/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "yopt"
8
+ spec.version = Yopt::VERSION
9
+ spec.authors = ["lorenzo.barasti"]
10
+
11
+ spec.summary = %q{Scala-inspired Options for the idiomatic Rubyist.}
12
+ spec.description = %q{This gem makes it possible to adopt the Option pattern in Ruby. It's meant to make conditional flow in our software clearer and more linear.}
13
+ spec.homepage = "https://github.com/lbarasti/yopt"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "minitest"
24
+ end
metadata ADDED
@@ -0,0 +1,107 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: yopt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - lorenzo.barasti
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-01-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: minitest
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: This gem makes it possible to adopt the Option pattern in Ruby. It's
56
+ meant to make conditional flow in our software clearer and more linear.
57
+ email:
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - CODE_OF_CONDUCT.md
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/console
70
+ - bin/setup
71
+ - docs/README.template.md
72
+ - docs/basics.rb
73
+ - docs/enumerable_methods.md
74
+ - docs/full_example_snippet.rb
75
+ - docs/including yopt.md
76
+ - docs/usage_reduce.rb
77
+ - docs/usage_snippet.rb
78
+ - docs/usage_zip.rb
79
+ - lib/yopt.rb
80
+ - lib/yopt/version.rb
81
+ - yopt.gemspec
82
+ homepage: https://github.com/lbarasti/yopt
83
+ licenses:
84
+ - MIT
85
+ metadata: {}
86
+ post_install_message:
87
+ rdoc_options: []
88
+ require_paths:
89
+ - lib
90
+ required_ruby_version: !ruby/object:Gem::Requirement
91
+ requirements:
92
+ - - ">="
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ version: '0'
100
+ requirements: []
101
+ rubyforge_project:
102
+ rubygems_version: 2.4.5
103
+ signing_key:
104
+ specification_version: 4
105
+ summary: Scala-inspired Options for the idiomatic Rubyist.
106
+ test_files: []
107
+ has_rdoc: