deterministic 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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