danom 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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 72e3182ef74e2ded6273ca00fd9cf2772ec8a5e1
4
+ data.tar.gz: c20c2215bfecc7332df727610e2f90bed3c8c6ef
5
+ SHA512:
6
+ metadata.gz: 8fea274d49f0694b5b6b3124c10a7b13e8dcf154291de8daaa39e254fcae80a619070489960eb3d2df55dd5b5788bbe03d6b064f22ed6cbb5bfcc2e4dad944d4
7
+ data.tar.gz: 68e8596ce864c5a56c20e6b05e413eab56c75535b505bf98106296c2c3220e617fa494fd06d4ff577f4f7a4eb01a2cd5526869a44e3cf3fc2bee15ac8dc360a4
data/.gitignore ADDED
@@ -0,0 +1,15 @@
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
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 danom.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -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.
data/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # Danom
2
+
3
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/D3MNetworks/danom)
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 'danom', require: 'sugar'
14
+ ```
15
+
16
+ Alernatively without sugar
17
+
18
+ ```ruby
19
+ gem 'danom'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install danom
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
+ Danom::Maybe.new('hello')
44
+ Danom::Just.new('world')
45
+ Danom::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 `Danom::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) #=> Danom::Just::CannotBeNil
147
+ never_nil = Just(5).and_then { nil } #=> Danom::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).
data/Rakefile ADDED
@@ -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
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "danom"
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__)
data/bin/setup ADDED
@@ -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
data/danom.gemspec ADDED
@@ -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 "danom/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "danom"
8
+ spec.version = Danom::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/danom"
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
data/lib/danom.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "danom/version"
2
+
3
+ # @nodoc
4
+ module Danom; end
@@ -0,0 +1,49 @@
1
+ require 'danom/monad'
2
+
3
+ module Danom
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 #=> Danom::Default
26
+ # name = ~d # 'Missing name'
27
+ #
28
+ # d = Default('Missing name', 'Uri')
29
+ # d = d.upcase #=> Danom::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) #=> Danom::Default
38
+ # name = ~d #=> 'No name'
39
+ #
40
+ # @see Danom::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
data/lib/danom/just.rb ADDED
@@ -0,0 +1,41 @@
1
+ require 'danom/monad'
2
+
3
+ module Danom
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 [Danom::Just::CannotBeNil] if value is provided as nil
17
+ def initialize(value)
18
+ raise CannotBeNil if value == nil
19
+ super
20
+ end
21
+
22
+ # Similar to Maybe#and_then but raises an error if the value ever becomes
23
+ # nil.
24
+ #
25
+ # @example
26
+ # j = Just('hello') #=> Just
27
+ # text = ~j #=> 'hello'
28
+ #
29
+ # @example Failing when becomes nil
30
+ # j = Just({}) #=> Just
31
+ # j[:name] #=> Danom::Monad::CannotBeNil
32
+ #
33
+ #
34
+ # @see Maybe#and_then
35
+ # @see Monad#method_missing
36
+ # @return [Just]
37
+ def and_then(&block)
38
+ Just.new block.call(@value)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,57 @@
1
+ require 'danom/monad'
2
+
3
+ module Danom
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,93 @@
1
+ module Danom
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
+ alias_method :~@, :monad_value
46
+
47
+
48
+ # Calls #to_s on the underlying value.
49
+ #
50
+ # @note This happens because method_missing does not override #to_s
51
+ # @private
52
+ def to_s
53
+ and_then { |v| v.to_s }
54
+ end
55
+
56
+
57
+ private
58
+
59
+ # Provides convinience for chaining methods together while maintaining a
60
+ # monad.
61
+ #
62
+ # @example Chaining fetches on a hash that does not have the keys
63
+ # m = Maybe({})
64
+ # m_name = m[:person][:full_name] #=> Danom::Maybe
65
+ # name = ~m_name #=> nil
66
+ #
67
+ # @example Safe navigation
68
+ # m = Maybe(nil)
69
+ # m_name = m.does.not.have.methods.upcase #=> Danom::Maybe
70
+ # name = ~m_name #=> nil
71
+ #
72
+ # @return [Danom::Monad]
73
+ def method_missing(method, *args, &block)
74
+ unwrapped_args = args.map do |arg|
75
+ case arg
76
+ when Monad then arg.value || arg
77
+ else arg
78
+ end
79
+ end
80
+ and_then { |value| value.public_send(method, *unwrapped_args, &block) }
81
+ rescue TypeError
82
+ self.class.new(nil)
83
+ end
84
+
85
+ def respond_to_missing?(method_name, include_private = false)
86
+ if @value
87
+ @value.respond_to?(method_name) || super
88
+ else
89
+ true
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,3 @@
1
+ module Danom
2
+ VERSION = "0.1.0"
3
+ end
data/lib/sugar.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'danom/maybe'
2
+ require 'danom/just'
3
+ require 'danom/default'
4
+
5
+ def Maybe(value) ; Danom::Maybe.new(value) end
6
+ def Just(value); Danom::Just.new(value) end
7
+ def Default(fallback, value) ; Danom::Default.new(fallback, value) end
8
+
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: danom
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Uri Gorelik
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-12-01 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
+ - danom.gemspec
99
+ - lib/danom.rb
100
+ - lib/danom/default.rb
101
+ - lib/danom/just.rb
102
+ - lib/danom/maybe.rb
103
+ - lib/danom/monad.rb
104
+ - lib/danom/version.rb
105
+ - lib/sugar.rb
106
+ homepage: https://github.com/D3MNetworks/danom
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: []