deterministic 0.1.1 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 718affa7e1715d28293a98600c6112c800d32d3a
4
- data.tar.gz: 600f1c5da38df13e625da735485af9e373a240e4
3
+ metadata.gz: 24278b8af8e996a30226c448b0677f8669e14606
4
+ data.tar.gz: 6a67ad79ac2e6ff81e0530b547c658f0cfb4633a
5
5
  SHA512:
6
- metadata.gz: fe73aa1b7315083c0ccfc507b240ef981293f2d8e3ceb89dc02ad3386ca5e14286b7fd944f911feb1e5d7ba9874fb364071e293062b77ef1172e9e43af334172
7
- data.tar.gz: 6070733d16a169a9e2b4b7685b90805a5758fb5146ffc9803a26ce58793a24ce93f4711332ae320aac3a81bec51cd8b530ddcc3be93dbfd068bd29f10d13f33c
6
+ metadata.gz: 0fd77c14af51a1814426d1c33bffccda8b97a58c5266638503dca676e1cbd78ac8a159becabfb3e4fe262016b5a2d0bf1663111f76d0fcbe92e7d105fa296f52
7
+ data.tar.gz: 6181518d77416ec8cf02712cd738f56b842deaf212a4ac368b8babcc141c01bf718fb7b9b10f2d11d6109cf36a35db0b3306fce8629cdafca940d239d9a2c675
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Deterministic
2
2
 
3
- This is a spiritual successor of the [Monadic gem](http://github.com/pzol/monadic).
3
+ This is a spiritual successor of the [Monadic gem](http://github.com/pzol/monadic). The goal of the rewrite is to get away from a bit to forceful aproach I took in Monadic, especially when it comes to coercing monads, but also a more practical but at the same time more strict adherence to monad laws.
4
4
 
5
5
  This gem is still __WORK IN PROGRESS__.
6
6
 
@@ -60,6 +60,12 @@ However, the real fun starts if you use it with your own context. You can use th
60
60
 
61
61
  ### Pattern matching
62
62
  Now that you have some result, you want to control flow by providing patterns.
63
+ `#match` can match by
64
+
65
+ * success, failure, either or any
66
+ * values
67
+ * lambdas
68
+ * classes
63
69
 
64
70
  ```ruby
65
71
  Success(1).match do
@@ -70,9 +76,9 @@ end # => "either 1"
70
76
  ```
71
77
  Note1: the inner value has been unwrapped!
72
78
 
73
- Note2: only the __last__ matching pattern block will be executed, so order __can__ be important.
79
+ Note2: only the __first__ matching pattern block will be executed, so order __can__ be important.
74
80
 
75
- The result returned will be the result of the __last__ `#try` or `#let`. As a side note, `#try` is a monad, `#let` is a functor.
81
+ The result returned will be the result of the __first__ `#try` or `#let`. As a side note, `#try` is a monad, `#let` is a functor.
76
82
 
77
83
  Values for patterns are good, too:
78
84
 
@@ -90,6 +96,14 @@ Success(1).match do
90
96
  end # => "Success 1"
91
97
  ```
92
98
 
99
+ Also you can match the result class
100
+
101
+ ```ruby
102
+ Success([1, 2, 3]).match do
103
+ success(Array) { |v| v.first }
104
+ end # => 1
105
+ ```
106
+
93
107
  Combining `#attempt_all` and `#match` is the ultimate sophistication:
94
108
 
95
109
  ```ruby
data/lib/deterministic.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  require "deterministic/version"
2
2
 
3
+ warn "WARN: Deterministic is meant to run on Ruby 2+" if RUBY_VERSION.to_f < 2
4
+
3
5
  module Deterministic; end
4
6
 
5
7
  require 'deterministic/monad'
@@ -1,12 +1,4 @@
1
1
  module Deterministic
2
- def Success(value)
3
- Success.new(value)
4
- end
5
-
6
- def Failure(value)
7
- Failure.new(value)
8
- end
9
-
10
2
  class Either
11
3
  include Monad
12
4
  include Deterministic::PatternMatching
@@ -23,5 +15,19 @@ module Deterministic
23
15
  return self if failure?
24
16
  return other if other.is_a? Either
25
17
  end
18
+
19
+ class << self
20
+ protected :new
21
+ end
22
+ end
23
+
24
+ module_function
25
+
26
+ def Success(value)
27
+ Success.new(value)
28
+ end
29
+
30
+ def Failure(value)
31
+ Failure.new(value)
26
32
  end
27
33
  end
@@ -19,6 +19,7 @@ class Deterministic::Either
19
19
  end
20
20
  end
21
21
 
22
+ # This is a functor
22
23
  def try(&block)
23
24
  try_p = ->(acc) {
24
25
  begin
@@ -32,10 +33,11 @@ class Deterministic::Either
32
33
  @tries << try_p
33
34
  end
34
35
 
36
+ # Basicly a monad
35
37
  def let(sym=nil, &block)
36
38
  @tries << ->(acc) {
37
- @context.instance_exec(acc, &block).tap do |value|
38
- raise EitherExpectedError unless value.is_a? Either
39
+ @context.instance_exec(acc.value, &block).tap do |value|
40
+ raise EitherExpectedError, "Expected the result to be either Success or Failure" unless value.is_a? Either
39
41
  end
40
42
  }
41
43
  end
@@ -1,3 +1,5 @@
1
1
  module Deterministic
2
- class Failure < Either; end
2
+ class Failure < Either
3
+ class << self; public :new; end
4
+ end
3
5
  end
@@ -1,23 +1,24 @@
1
1
  module Deterministic::PatternMatching
2
2
 
3
- def match(proc=nil, &block)
4
- match = Match.new(self)
5
- match.instance_eval &(proc || block)
3
+ def match(context=self, &block)
4
+ match = Match.new(self, context)
5
+ match.instance_eval &block
6
6
  match.result
7
7
  end
8
8
 
9
9
  class NoMatchError < StandardError; end
10
10
 
11
11
  class Match
12
- def initialize(container)
12
+ def initialize(container, context)
13
13
  @container = container
14
+ @context = context
14
15
  @collection = []
15
16
  end
16
17
 
17
18
  def result
18
- matcher = @collection.select { |m| m.matches?(@container.value) }.last
19
- raise NoMatchError if matcher.nil?
20
- matcher.result(@container.value)
19
+ matcher = @collection.detect { |m| m.matches?(@container.value) }
20
+ raise NoMatchError, "No could be made for #{@container}" if matcher.nil?
21
+ @context.instance_exec(@container.value, &matcher.block)
21
22
  end
22
23
 
23
24
  # TODO: Either specific DSL, will need to move it to Either, later on
@@ -38,17 +39,14 @@ module Deterministic::PatternMatching
38
39
  def matches?(value)
39
40
  condition.call(value)
40
41
  end
41
-
42
- def result(value)
43
- block.call(value)
44
- end
45
42
  end
46
43
 
47
44
  def push(type, condition, result_block)
48
45
  condition_pred = case
49
- when condition.nil?; ->(v) { true }
50
- when condition.is_a?(Proc); condition
51
- else ->(v) { condition == @container.value }
46
+ when condition.nil?; ->(v) { true }
47
+ when condition.is_a?(Proc); condition
48
+ when condition.is_a?(Class); ->(v) { condition === @container.value }
49
+ else ->(v) { condition == @container.value }
52
50
  end
53
51
 
54
52
  matcher_pred = compose_predicates(type_pred[type], condition_pred)
@@ -1,3 +1,5 @@
1
1
  module Deterministic
2
- class Success < Either; end
2
+ class Success < Either
3
+ class << self; public :new; end
4
+ end
3
5
  end
@@ -15,26 +15,29 @@ module Deterministic
15
15
 
16
16
  # The functor: takes a function (a -> b) and applies it to the inner value of the monad (Ma),
17
17
  # boxes it back to the same monad (Mb)
18
- # fmap :: (a -> b) -> Ma -> Mb
18
+ # fmap :: (a -> b) -> M a -> M b
19
19
  def map(proc=nil, &block)
20
20
  result = (proc || block).call(value)
21
21
  self.class.new(result)
22
22
  end
23
23
 
24
- # The monad: takes a function which returns a monad, applies
25
- # bind :: Ma -> (a -> Mb) -> Mb
24
+ # The monad: takes a function which returns a monad (of the same type), applies the function
25
+ # bind :: M a -> (a -> Mb) -> M b
26
+ # the self.class, i.e. the containing monad is passed as a second (optional) arg to the function
26
27
  def bind(proc=nil, &block)
27
- result = (proc || block).call(value)
28
- raise NotMonadError unless result.is_a? Monad
29
- self.class.new(result)
28
+ (proc || block).call(value, self.class).tap do |result|
29
+ raise NotMonadError unless result.is_a? self.class
30
+ end
30
31
  end
32
+ alias :'>>=' :bind
31
33
 
32
34
  # Get the underlying value, return in Haskell
33
- # return :: m a -> a
35
+ # return :: M a -> a
34
36
  def value
35
37
  @value
36
38
  end
37
39
 
40
+ # Two monads are equivalent if they are of the same type and when their values are equal
38
41
  def ==(other)
39
42
  return false unless other.is_a? self.class
40
43
  @value == other.instance_variable_get(:@value)
@@ -1,3 +1,3 @@
1
1
  module Deterministic
2
- VERSION = "0.1.1"
2
+ VERSION = "0.1.2"
3
3
  end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+ require 'deterministic'
3
+
4
+ include Deterministic
5
+
6
+ module Examples
7
+ module Bookings
8
+
9
+ module Adapters
10
+ class BookingsRepo
11
+ def find(params)
12
+ end
13
+ end
14
+ end
15
+
16
+ class FakeWebUi
17
+ def method_missing(m, *args)
18
+ { m => args }
19
+ end
20
+ end
21
+
22
+ class Dependencies
23
+ include Adapters
24
+ def bookings_repo
25
+ BookingsRepo.new
26
+ end
27
+ end
28
+
29
+ class ShowBookings
30
+ def initialize(world=FakeWebUi.new, deps=Dependencies.new)
31
+ @world = world
32
+ @world.booking_list([])
33
+ end
34
+
35
+ def call(dirty_params)
36
+ result = Either.attempt_all(self) do
37
+ try { parse_params(dirty_params) }
38
+ let { |params| read_bookings(params) }
39
+ end.match(world) do
40
+ success(Array) { |bookings| booking_list(bookings) }
41
+ success { |booking| booking(booking) }
42
+ failure(:no_bookings) { empty_booking_list }
43
+ failure(StandardError) { |ex| raise ex }
44
+ any { |result| raise "Something went terribly wrong `#{result.class}`" }
45
+ end
46
+ end
47
+
48
+ # private
49
+ attr_reader :bookings_repo, :world
50
+
51
+ def parse_params(dirty_params)
52
+ dirty_params
53
+ end
54
+
55
+ def read_bookings(params)
56
+ bookings = [params]
57
+ case bookings
58
+ when nil; Failure(:no_bookings)
59
+ when bookings.count == 1;
60
+ else Success(bookings)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+
67
+ describe Examples::Bookings::ShowBookings do
68
+ subject { described_class.new }
69
+ it "works" do
70
+ expect(
71
+ subject.call({status: 'confirmed'})
72
+ ).to eq({:booking_list=>[[{:status=>"confirmed"}]]})
73
+ end
74
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+ require 'deterministic'
3
+
4
+ include Deterministic
5
+
6
+ # A Unit of Work for validating an address
7
+ module ValidateAddress
8
+ def self.call(candidate)
9
+ errors = {}
10
+ errors[:street] = "Street cannot be empty" unless candidate.has_key? :street
11
+ errors[:city] = "Street cannot be empty" unless candidate.has_key? :city
12
+ errors[:postal] = "Street cannot be empty" unless candidate.has_key? :postal
13
+
14
+ errors.empty? ? Success(candidate) : Failure(errors)
15
+ end
16
+ end
17
+
18
+ describe ValidateAddress do
19
+ subject { ValidateAddress.call(candidate) }
20
+ context 'sunny day' do
21
+ let(:candidate) { {title: "Hobbiton", street: "501 Buckland Rd", city: "Matamata", postal: "3472", country: "nz"} }
22
+ specify { expect(subject).to be_a Success }
23
+ specify { expect(subject.value).to eq candidate }
24
+ end
25
+
26
+ context 'empty data' do
27
+ let(:candidate) { {} }
28
+ specify { expect(subject).to be_a Failure }
29
+ specify { expect(subject.value).to include(:street, :city, :postal) }
30
+ end
31
+ end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'deterministic'
3
2
 
4
3
  include Deterministic
5
4
 
@@ -64,6 +63,15 @@ describe Deterministic::Either::AttemptAll do
64
63
  }.to raise_error
65
64
  end
66
65
 
66
+ it "#let passes params unboxed" do
67
+ expect(
68
+ Either.attempt_all do
69
+ try { 1 }
70
+ let { |v| Success(v + 1) }
71
+ end
72
+ ).to eq Success(2)
73
+ end
74
+
67
75
  it "works with an OpenStruct" do
68
76
  context = OpenStruct.new
69
77
  attempt = Either.attempt_all(context) do
@@ -75,23 +83,23 @@ describe Deterministic::Either::AttemptAll do
75
83
  expect(context.alpha).to eq 2
76
84
  end
77
85
 
78
- it "can operate in the context of a host" do
79
- class Host
86
+ it "can operate in the context of a context" do
87
+ class Context
80
88
  attr_accessor :a
81
89
  def initialize
82
90
  @a = 1
83
91
  end
84
92
  end
85
93
 
86
- host = Host.new
94
+ context = Context.new
87
95
 
88
96
  expect(
89
- Either.attempt_all(host) do
97
+ Either.attempt_all(context) do
90
98
  try { self.a += 1 }
91
99
  try { self.a + 1 }
92
100
  end
93
101
  ).to eq Success(3)
94
102
 
95
- expect(host.a).to eq 2
103
+ expect(context.a).to eq 2
96
104
  end
97
105
  end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'deterministic'
3
2
 
4
3
  include Deterministic
5
4
 
@@ -34,6 +33,20 @@ describe Deterministic::Either::Match do
34
33
  ).to eq "matched 2"
35
34
  end
36
35
 
36
+ it "can match with classes" do
37
+ expect(
38
+ Success([1, 2, 3]).match do
39
+ success(Array) { |v| v.first }
40
+ end
41
+ ).to eq 1
42
+
43
+ expect(
44
+ Success(1).match do
45
+ success(Fixnum) { |v| v }
46
+ end
47
+ ).to eq 1
48
+ end
49
+
37
50
  it "catch-all" do
38
51
  expect(
39
52
  Success(1).match do
@@ -55,6 +68,7 @@ describe Deterministic::Either::Match do
55
68
  it "can match with lambdas" do
56
69
  expect(
57
70
  Success(1).match do
71
+ failure { "not me" }
58
72
  success ->(v) { v == 1 } { |v| "matched #{v}" }
59
73
  end
60
74
  ).to eq "matched 1"
@@ -1,6 +1,5 @@
1
1
  require 'spec_helper'
2
2
  require_relative '../monad_axioms'
3
- require 'deterministic'
4
3
 
5
4
  include Deterministic
6
5
 
@@ -17,7 +16,6 @@ describe Deterministic::Success do
17
16
  specify { expect(subject).to be_success }
18
17
  specify { expect(subject).not_to be_failure }
19
18
 
20
- # public constructor #Success[]
21
19
  specify { expect(subject).to be_an_instance_of described_class }
22
20
  specify { expect(subject).to eq(described_class.new(1)) }
23
21
  specify { expect(subject << Success(2)).to eq(Success(2)) }
@@ -30,6 +28,4 @@ describe Deterministic::Success do
30
28
  subject.bind { |v| true ? Success(v + 1) : Failure(v + 2)}
31
29
  ).to eq Success(2)
32
30
  end
33
-
34
- specify { expect { Success("a").bind(&:upcase) }.to raise_error(Deterministic::Monad::NotMonadError) }
35
31
  end
@@ -0,0 +1,8 @@
1
+ require 'spec_helper'
2
+
3
+ describe Deterministic::Either do
4
+ it "can't call Either#new directly" do
5
+ expect { described_class.new(1)}
6
+ .to raise_error(NoMethodError, "protected method `new' called for Deterministic::Either:Class")
7
+ end
8
+ end
@@ -1,6 +1,5 @@
1
1
  require 'spec_helper'
2
2
  require_relative 'monad_axioms'
3
- require 'deterministic'
4
3
 
5
4
 
6
5
  describe Deterministic::Monad do
@@ -17,7 +16,28 @@ describe Deterministic::Monad do
17
16
  specify { expect(Identity.new([1, 2]).map(&:to_s)).to eq Identity.new("[1, 2]") }
18
17
  specify { expect(Identity.new(1).map {|v| v + 2}).to eq Identity.new(3) }
19
18
  specify { expect(Identity.new('foo').map(&:upcase)).to eq Identity.new('FOO')}
20
- specify { expect { Identity.new(1).bind {} }.to raise_error(Deterministic::Monad::NotMonadError) }
19
+
20
+ context '#bind' do
21
+ it "raises an error if the passed function does not return a monad of the same class" do
22
+ expect { Identity.new(1).bind {} }.to raise_error(Deterministic::Monad::NotMonadError)
23
+ end
24
+ specify { expect(Identity.new(1).bind {|value| Identity.new(value) }).to eq Identity.new(1) }
25
+
26
+ it "passes the monad class, this is ruby-fu?!" do
27
+ Identity.new(1)
28
+ .bind do |_, monad|
29
+ p self.class
30
+ expect(monad).to eq Identity
31
+ monad.new(_)
32
+ end
33
+ end
34
+
35
+ specify { expect(
36
+ Identity.new(1).bind { |value, monad| monad.new(value + 1) }
37
+ ).to eq Identity.new(2)
38
+ }
39
+
40
+ end
21
41
  specify { expect(Identity.new(Identity.new(1))).to eq Identity.new(1) }
22
42
 
23
43
  # it 'delegates #flat_map to an underlying collection and wraps the resulting collection' do
data/spec/spec_helper.rb CHANGED
@@ -21,3 +21,4 @@ RSpec.configure do |config|
21
21
  end
22
22
  config.order = 'random'
23
23
  end
24
+ require 'deterministic'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deterministic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Zolnierek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-12-28 00:00:00.000000000 Z
11
+ date: 2014-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -132,9 +132,12 @@ files:
132
132
  - lib/deterministic/either/success.rb
133
133
  - lib/deterministic/monad.rb
134
134
  - lib/deterministic/version.rb
135
+ - spec/examples/bookings_spec.rb
136
+ - spec/examples/validate_address_spec.rb
135
137
  - spec/lib/deterministic/attempt_all_spec.rb
136
138
  - spec/lib/deterministic/either/match_spec.rb
137
139
  - spec/lib/deterministic/either/success_spec.rb
140
+ - spec/lib/deterministic/either_spec.rb
138
141
  - spec/lib/deterministic/monad_axioms.rb
139
142
  - spec/lib/deterministic/monad_spec.rb
140
143
  - spec/spec_helper.rb
@@ -158,14 +161,17 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
161
  version: '0'
159
162
  requirements: []
160
163
  rubyforge_project:
161
- rubygems_version: 2.0.14
164
+ rubygems_version: 2.0.3
162
165
  signing_key:
163
166
  specification_version: 4
164
167
  summary: see above
165
168
  test_files:
169
+ - spec/examples/bookings_spec.rb
170
+ - spec/examples/validate_address_spec.rb
166
171
  - spec/lib/deterministic/attempt_all_spec.rb
167
172
  - spec/lib/deterministic/either/match_spec.rb
168
173
  - spec/lib/deterministic/either/success_spec.rb
174
+ - spec/lib/deterministic/either_spec.rb
169
175
  - spec/lib/deterministic/monad_axioms.rb
170
176
  - spec/lib/deterministic/monad_spec.rb
171
177
  - spec/spec_helper.rb