ronad 0.3.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: 31ce7e2542c92dad39d02ede97796cae42721049
4
+ data.tar.gz: ad50b90276b62b10dc440d42a8b89dee3bd80a56
5
+ SHA512:
6
+ metadata.gz: 650f5c6fcecb65d671769135daa99e1f8932bcc8ed2f27a1ee9ccba5aaba1aaff68380193f37b577ef1c5feb959ab0a27dfa572043f9963a4f97c9e7cd46a0b7
7
+ data.tar.gz: a09380df556ab58b6aa3cfa63c0d162a2877d0dc472e6caf52587546151b6a2e1354e7631ebbffa69172dab5fb2e525054c46b5070909adb6b1d8fda862e9cc5
@@ -0,0 +1,17 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+
11
+ .DS_Store
12
+ .byebug_history
13
+
14
+ # rspec failure tracking
15
+ .rspec_status
16
+
17
+ *.gem
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in ronad.gemspec
6
+ gemspec
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Uri Gorelik
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,166 @@
1
+ # Ronad
2
+
3
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/D3MNetworks/ronad)
4
+
5
+ Monads implemented in Ruby with sugar. Inspired by [@tomstuart](https://twitter.com/tomstuart)'s
6
+ talk https://codon.com/refactoring-ruby-with-monads
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'ronad', require: 'sugar'
14
+ ```
15
+
16
+ Alernatively without sugar
17
+
18
+ ```ruby
19
+ gem 'ronad'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install ronad
29
+
30
+ ## Sugar
31
+
32
+ If required, you can use the monads as methods:
33
+
34
+ ```ruby
35
+ Maybe('hello')
36
+ Just('world')
37
+ Default('hello', 'world')
38
+ ```
39
+
40
+ Without the sugar you must use the fully qualified name and invoke the constructor:
41
+
42
+ ```ruby
43
+ Ronad::Maybe.new('hello')
44
+ Ronad::Just.new('world')
45
+ Ronad::Default.new('hello', 'world')
46
+ ```
47
+
48
+ All examples will be using the "sugar" syntax but they are interchangeable.
49
+
50
+
51
+ ## Usage
52
+
53
+ Generally every `Ronad::Monad` will respond to `and_then`.
54
+
55
+ `method_missing` is also used to for convenience as a proxy for `and_then`.
56
+
57
+ ```ruby
58
+ maybe_name = Maybe({person: {name: 'Bob'}})
59
+ maybe_name
60
+ .and_then{ |v| v[:person] }
61
+ .and_then{ |v| v[:name] }
62
+ .value
63
+
64
+ # Equivalent:
65
+
66
+ maybe_name = Maybe({person: {name: 'Bob'}})
67
+ ~maybe_name[:person][:name]
68
+ ```
69
+
70
+
71
+ Generally the `and_then` block will only run based on a condition from the specific monad.
72
+
73
+ For example, `Maybe` will only invoke the block from `and_then` if the underlying value is not
74
+
75
+ `nil`.
76
+
77
+ ```ruby
78
+ Maybe(nil).and_then do |_|
79
+ raise "Boom"
80
+ end #=> No error
81
+ ```
82
+
83
+
84
+ ### `value`, `monad_value`, `~`
85
+
86
+ To get the underlying value of a monad you need to call `#value`. If the underlying value also
87
+ responds to `#value`, you can use `#monad_value`. `#~` is a unary operator overload which is an
88
+ alias for `monad_value`.
89
+
90
+ ```ruby
91
+ m = Maybe(5)
92
+
93
+ m.value == m.monad_value #=> true
94
+ m.value == ~m #=> true
95
+ m.monad_value == ~m #=> true
96
+ ```
97
+
98
+ ## Maybe
99
+
100
+ `Maybe` is useful as a safe navigation operator:
101
+
102
+ ```ruby
103
+ response = request(params) #=> {}
104
+ name = ~Maybe(response[:person])[:name] #=> nil
105
+ ```
106
+
107
+ It's also useful as an annotation. If you look at the previous example, you'll notice that you could
108
+ wrap `response` in a maybe instead:
109
+
110
+ ```ruby
111
+ name = ~Maybe(response)[:person][:name] #=> nil
112
+ ```
113
+
114
+ This functionally the same but has different semantics. With the former example, it has correctly
115
+ annotated that `response[:person]` can be `nil`, whereas the latter example is an over eager usage.
116
+
117
+
118
+
119
+ Here's a less trivial example
120
+
121
+ ```ruby
122
+ Document.each do |doc|
123
+ maybe_json = Maybe(doc.data) # data is nil or a json string
124
+ .and_then{|str| JSON.parse str}
125
+
126
+ maybe_json['nodes'] # method_missing in action
127
+ .map do |n|
128
+ n['new_field'] = 'new'
129
+ n
130
+ end
131
+ .continue(&:any?) # see documentation
132
+ .and_then do |new_nodes|
133
+ doc.data['nodes'] = new_nodes
134
+ doc.save!
135
+ end
136
+ end
137
+ ```
138
+
139
+ In this example `value` was no invoked as `Maybe` was use more so for flow control.
140
+
141
+ ## Just
142
+
143
+ Useful for catching a nil immediately
144
+
145
+ ```ruby
146
+ Just(nil) #=> Ronad::Just::CannotBeNil
147
+ never_nil = Just(5).and_then { nil } #=> Ronad::Just::CannotBeNil
148
+ good = ~Just(5) #=> 5
149
+ ```
150
+
151
+ ## Default
152
+
153
+ Similar to a null object pattern. Setting up defaults so `#value` will never return nil. Can be
154
+ combined with a `Maybe`.
155
+
156
+ ```ruby
157
+ d = ~Default('hello', nil) #=> 'hello'
158
+ d = ~Default('hello', Maybe(10)) #=> 10
159
+ ```
160
+
161
+ Notice that default will recursively expand other Monads. As a general rule, you should never
162
+ receive a monad after invoking `#value`.
163
+
164
+ ## License
165
+
166
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "ronad"
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
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ require "ronad/version"
2
+
3
+ # @nodoc
4
+ module Ronad; end
@@ -0,0 +1,49 @@
1
+ require 'ronad/monad'
2
+
3
+ module Ronad
4
+ # A default "monad". Not technically a monad as it take two values.
5
+ class Default < Monad
6
+ # @param fallback [Object] The value on which to fallback if the doesn't
7
+ # satisfy Just(value)
8
+ # @param value [Object]
9
+ def initialize(fallback, value)
10
+ @fallback = Just.new(fallback).value
11
+ super(value)
12
+ end
13
+
14
+ # @override
15
+ def monad_value
16
+ super || @fallback
17
+ end
18
+
19
+ # Allows chaining on nil values with a fallback
20
+ #
21
+ # @note The fallback does not get the transformations
22
+ #
23
+ # @example
24
+ # d = Default('Missing name', nil)
25
+ # d = d.upcase.split.join #=> Ronad::Default
26
+ # name = ~d # 'Missing name'
27
+ #
28
+ # d = Default('Missing name', 'Uri')
29
+ # d = d.upcase #=> Ronad::Default
30
+ # name = ~d # 'URI'
31
+ #
32
+ # Default can be combined with a maybe. Extracting the value is recursive
33
+ # and will never return another monad.
34
+ #
35
+ # @example Combining with a Maybe
36
+ # m = Maybe(nil)
37
+ # d = Default('No name', m) #=> Ronad::Default
38
+ # name = ~d #=> 'No name'
39
+ #
40
+ # @see Ronad::Monad#method_missing
41
+ def and_then &block
42
+ if @value == nil
43
+ Default.new @fallback, nil
44
+ else
45
+ Default.new @fallback, block.call(@value)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ require 'ronad/monad'
2
+
3
+ module Ronad
4
+ # Useful for immediately failing when a nil pops up.
5
+ # name = get_name(person) #=> nil
6
+ # # ... later somewhere
7
+ # name.upcase # Error!
8
+ #
9
+ # Using a Just catches the problem at the source
10
+ #
11
+ # name = Just(get_name(person)) # Error!
12
+ class Just < Monad
13
+ # Raised if the underlying value of Just becomes nil
14
+ class CannotBeNil < StandardError; end
15
+
16
+ # @raise [Ronad::Just::CannotBeNil] if value is provided as nil
17
+ def initialize(value)
18
+ raise CannotBeNil if !(Monad === value) && value == nil
19
+ super
20
+ end
21
+
22
+ # @raise [Ronad::Just::CannotBeNil]
23
+ def monad_value
24
+ val = super
25
+ raise CannotBeNil if val == nil
26
+ val
27
+ end
28
+
29
+ # Similar to Maybe#and_then but raises an error if the value ever becomes
30
+ # nil.
31
+ #
32
+ # @example
33
+ # j = Just('hello') #=> Just
34
+ # text = ~j #=> 'hello'
35
+ #
36
+ # @example Failing when becomes nil
37
+ # j = Just({}) #=> Just
38
+ # j[:name] #=> Ronad::Monad::CannotBeNil
39
+ #
40
+ #
41
+ # @see Maybe#and_then
42
+ # @see Monad#method_missing
43
+ # @return [Just]
44
+ def and_then(&block)
45
+ Just.new block.call(@value)
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,57 @@
1
+ require 'ronad/monad'
2
+
3
+ module Ronad
4
+ class Maybe < Monad
5
+ # Allows for safe navigation on nil.
6
+ #
7
+ # nil&.m1&.m2&.m3 #=> nil
8
+ #
9
+ # ~Maybe(nil).m1.m2.m3 #=> nil
10
+ #
11
+ # Using a Maybe really shines when accessing a hash
12
+ #
13
+ # ~Maybe({})[:person][:name].downcase.split(' ').join('-') #=> nil
14
+ #
15
+ # {}.dig(:person, :name)&.downcase.&split(' ')&.join('-') #=> nil
16
+ #
17
+ # Using #and_then without the Monad#method_missing sugar is also useful if
18
+ # you need to pass your value to another method
19
+ #
20
+ # Maybe(nil).and_then{ |value| JSON.parse(value) } #=> Maybe
21
+ #
22
+ # The block only gets executed if value is not `nil`
23
+ #
24
+ # @return [Maybe]
25
+ # @see Monad#method_missing
26
+ def and_then
27
+ if @value
28
+ Maybe.new yield(@value)
29
+ else
30
+ Maybe.new(nil)
31
+ end
32
+ end
33
+
34
+ # "Fails" a maybe given a certain condition. If the condition is `false`
35
+ # then Maybe(nil) will be returned. Useful for performing side-effects
36
+ # given a certain condition
37
+ #
38
+ # Maybe([]).and_then do |value|
39
+ # value.each(&:some_operation)
40
+ # OtherModule.finalize! # Side Effect should only happen if there
41
+ # end # are any values
42
+ #
43
+ # The following `and_then` only executes if `value` responds truthy to any?
44
+ #
45
+ # Maybe([]).continue(&:any?).and_then do |value|
46
+ # value.each(&:some_operation)
47
+ # OtherModule.finalize!
48
+ # end
49
+ # @return [Monad]
50
+ def continue
51
+ and_then do |value|
52
+ value if yield value
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,99 @@
1
+ module Ronad
2
+ # This class provides a common interface.
3
+ #
4
+ # @abstract
5
+ class Monad
6
+ def initialize(value)
7
+ if Monad === value
8
+ @value = value.value
9
+ else
10
+ @value = value
11
+ end
12
+ end
13
+
14
+ # Extract the value from the monad. If the underlying value responds to
15
+ # #value then this will be bypassed instead.
16
+ #
17
+ # @see #monad_value
18
+ # @return [Object] The unwrapped value
19
+ def value
20
+ if !(Monad === @value) && @value.respond_to?(:value)
21
+ and_then{ |v| v.value }
22
+ else
23
+ monad_value
24
+ end
25
+ end
26
+
27
+
28
+ # Extract the value from the monad. Can also be extracted via `~` unary
29
+ # operator.
30
+ #
31
+ # @example
32
+ # ~Maybe("hello") #=> "HELLO"
33
+ # ~Maybe(nil) #=> nil
34
+ #
35
+ # @note Useful when you want to extract the underlying value and it also
36
+ # responds to #value
37
+ # @return [Object] The unwrapped value
38
+ def monad_value
39
+ if Monad === @value
40
+ @value.value
41
+ else
42
+ @value
43
+ end
44
+ end
45
+
46
+ # Alias for #monad_value
47
+ #
48
+ # @see #monad_value
49
+ def ~@
50
+ monad_value
51
+ end
52
+
53
+
54
+ # Proxy all Object methods to underlying value
55
+ #
56
+ # @private
57
+ Object.new.methods.each do |object_method|
58
+ define_method object_method do |*args|
59
+ and_then { |v| v.public_send(object_method, *args) }
60
+ end
61
+ end
62
+
63
+ private
64
+
65
+ # Provides convinience for chaining methods together while maintaining a
66
+ # monad.
67
+ #
68
+ # @example Chaining fetches on a hash that does not have the keys
69
+ # m = Maybe({})
70
+ # m_name = m[:person][:full_name] #=> Ronad::Maybe
71
+ # name = ~m_name #=> nil
72
+ #
73
+ # @example Safe navigation
74
+ # m = Maybe(nil)
75
+ # m_name = m.does.not.have.methods.upcase #=> Ronad::Maybe
76
+ # name = ~m_name #=> nil
77
+ #
78
+ # @return [Ronad::Monad]
79
+ def method_missing(method, *args, &block)
80
+ unwrapped_args = args.map do |arg|
81
+ case arg
82
+ when Monad then arg.value || arg
83
+ else arg
84
+ end
85
+ end
86
+ and_then { |value| value.public_send(method, *unwrapped_args, &block) }
87
+ rescue TypeError
88
+ self.class.new(nil)
89
+ end
90
+
91
+ def respond_to_missing?(method_name, include_private = false)
92
+ if @value
93
+ @value.respond_to?(method_name) || super
94
+ else
95
+ true
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,3 @@
1
+ module Ronad
2
+ VERSION = "0.3.0"
3
+ end
@@ -0,0 +1,8 @@
1
+ require 'ronad/maybe'
2
+ require 'ronad/just'
3
+ require 'ronad/default'
4
+
5
+ def Maybe(value) ; Ronad::Maybe.new(value) end
6
+ def Just(value); Ronad::Just.new(value) end
7
+ def Default(fallback, value) ; Ronad::Default.new(fallback, value) end
8
+
@@ -0,0 +1,30 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "ronad/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "ronad"
8
+ spec.version = Ronad::VERSION
9
+ spec.authors = ["Uri Gorelik"]
10
+ spec.email = ["ugorelik@teldio.com"]
11
+
12
+ spec.summary = %q{Monads implemented the Ruby way}
13
+ # spec.description = %q{TODO: Write a longer description or delete this line.}
14
+ spec.homepage = "https://github.com/D3MNetworks/ronad"
15
+ spec.license = "MIT"
16
+
17
+
18
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
19
+ f.match(%r{^(test|spec|features)/})
20
+ end
21
+ spec.bindir = "exe"
22
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
23
+ spec.require_paths = ["lib"]
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.15"
26
+ spec.add_development_dependency "rake", "~> 10.0"
27
+ spec.add_development_dependency "rspec", "~> 3.0"
28
+ spec.add_development_dependency "byebug", "~> 9.1"
29
+ spec.add_development_dependency "yard", "~> 0.9.12"
30
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ronad
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Uri Gorelik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-01-05 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.15'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.15'
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: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: byebug
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '9.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '9.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: yard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.9.12
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.9.12
83
+ description:
84
+ email:
85
+ - ugorelik@teldio.com
86
+ executables: []
87
+ extensions: []
88
+ extra_rdoc_files: []
89
+ files:
90
+ - ".gitignore"
91
+ - ".rspec"
92
+ - Gemfile
93
+ - LICENSE.txt
94
+ - README.md
95
+ - Rakefile
96
+ - bin/console
97
+ - bin/setup
98
+ - lib/ronad.rb
99
+ - lib/ronad/default.rb
100
+ - lib/ronad/just.rb
101
+ - lib/ronad/maybe.rb
102
+ - lib/ronad/monad.rb
103
+ - lib/ronad/version.rb
104
+ - lib/sugar.rb
105
+ - ronad.gemspec
106
+ homepage: https://github.com/D3MNetworks/ronad
107
+ licenses:
108
+ - MIT
109
+ metadata: {}
110
+ post_install_message:
111
+ rdoc_options: []
112
+ require_paths:
113
+ - lib
114
+ required_ruby_version: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - ">="
117
+ - !ruby/object:Gem::Version
118
+ version: '0'
119
+ required_rubygems_version: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 2.5.1
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: Monads implemented the Ruby way
130
+ test_files: []