optional 0.0.4 → 0.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,30 +1,201 @@
1
1
  # optional
2
- ### option types to make nils a thing of the past
2
+ ## option types to make nils a thing of the past
3
3
 
4
- Tony Hoare, inventor of the null reference, calls it his "billion-dollar mistake". You will be no stranger to the ubiquitous `NoMethodError: undefined method 'foo' for nil:NilClass`. But it doesn't have to be this way.
4
+ Tony Hoare, inventor of the null reference, calls it his "billion-dollar mistake".
5
+ You will be no stranger to the ubiquitous `NoMethodError: undefined method 'foo' for nil:NilClass`.
6
+ But it doesn't have to be this way.
5
7
 
6
- There are, crucially, two distinct types of values (or rather lack of values) that are usually, in Ruby, represented as `nil`. There are values that should always be present - and here the fact that they are `nil` actually indicates that an error has occurred somewhere else in your program - and those values that *may or may not be set* - where each case is valid. For example, a person may or may not be wearing a hat.
7
-
8
- class Hat
9
- def doff
10
-
11
- end
8
+ There are, crucially, two distinct types of values (or rather lack of values) that are usually, in Ruby, represented as `nil`.
9
+ There are values that should always be present - and here the fact that they are `nil` actually indicates that an error has occurred somewhere else in your program - and those values that *may or may not be set* - where each case is valid.
10
+ For example, a person may or may not be wearing a hat.
11
+
12
+ Using `nil` to represent these optional values leads to lots of ugly defensive coding - or worse, if you forget the ugly defensive coding in even a single place, to `nil`s leaking out into the rest of your program, causing it to blow up at some later point, from where it might be hard to track back to the original cause of the problem.
13
+
14
+ `Option`s, a construct found in many functional languages, provide an alternative.
15
+ They consist of, at the most basic level, a container that either contains a value (`Some` value) or does not (`None`).
16
+ Because its a container, you can't access the value directly, so client code is forced to deal with both the case that it's there and that it isn't.
17
+ It's a simple concept but leads to some surprisingly powerful uses.
18
+
19
+ ## Creating Options
20
+
21
+ Here's how you create an option that has a value (a `Some`), using some syntactic sugar provided by the [] method:
22
+
23
+ Some[4]
24
+ Some[Cat.new("Tabitha")]
25
+ Some["Jelly"]
26
+
27
+ Like `nil`, there's only one instance of `None`, so you don't need to create it. Here's how you use it:
28
+
29
+ None
30
+
31
+ ## Getting the value
32
+
33
+ The simplest way to get the value of an `Option` is using the `value` method:
34
+
35
+ Some[5].value # => 5
36
+ None.value # => raises ValueOfNoneError
37
+
38
+ Why is it a good thing that calling `value` on `None` raises an error? Because you should only use `value` if you're sure that you should have a value at this point - if not having a value *is* an error.
39
+ This means the code will blow up at the earliest possible opportunity, making it easier to track down the root cause of the problem.
40
+
41
+ ## How do I know if I have a value?
42
+
43
+ There are methods named as you'd expect to test for this:
44
+
45
+ Some[5].some? # => true
46
+ Some[5].none? # => false
47
+
48
+ None.some? # => false
49
+ None.none? # => true
50
+
51
+ You can also pass a class or module constant to `some?`:
52
+
53
+ Some["Perpugilliam Brown"].some? String # => true
54
+ Some["Perpugilliam Brown"].some? Fixnum # => false
55
+
56
+ This can come in handy when writing rspec tests:
57
+
58
+ it { should be_none }
59
+ it { should be_some Fixnum }
60
+
61
+ But you shouldn't usually need to use these `some?` or `none?` methods.
62
+ They basically add up to exactly the same ugly defensive code we're trying to avoid with `nil`s.
63
+ Luckily `Option`s provide plenty of other, more elegant ways to access their values.
64
+
65
+ ## Providing a default
66
+
67
+ If not having a value is a valid case, you might want to provide a default in its place. You can do this using the `value_or` method:
68
+
69
+ Some[5].value_or 0 # => 5
70
+ None.value_or 0 # => 0
71
+
72
+ You can also pass a block to `value_or` in case you don't want the default to be evaluated unless it is used:
73
+
74
+ Some[5].value_or { puts "This won't be printed." ; 0 } # => 5
75
+ None.value_or { puts "This will be printed!" ; 0 } # => 0
76
+
77
+ ## Option is enumerable!
78
+
79
+ `Option` supports all the same methods that enumerable supports - although it will return `Option`s rather than `Array`s in most cases.
80
+ This leads to some pretty cool stuff.
81
+
82
+ Here's `each`:
83
+
84
+ Some[5].each do |value|
85
+ puts "The value is #{value}!"
86
+ end
87
+ # => prints "The value is 5!"
88
+
89
+ None.each do |value|
90
+ puts "The value is #{value}!"
12
91
  end
13
-
14
- gwen = Person.create(name: "Gwen", hat: Some[:fedora])
15
- charlie = Person.create(name: "Charlie", hat: None)
16
-
17
- class Person
18
-
19
- def greet
20
- puts "hello!"
21
- hat.do { |h| doff h }
22
- end
23
-
92
+ # => doesn't print anything (because there is no value)
93
+
94
+ Or you can also use `for`:
95
+
96
+ for value in Some[5]
97
+ puts "The value is #{value}!"
24
98
  end
99
+ # => prints "The value is 5!"
100
+
101
+ for value in None
102
+ puts "The value is #{value}!"
103
+ end
104
+ # => doesn't print anything
105
+
106
+ And here's `map` (or `collect`):
107
+
108
+ Some["caterpillar!"].map(&:upcase) # => Some["CATERPILLAR!"]
109
+ None.map(&:upcase) # => None
110
+
111
+ Options being enumerable comes in handy if you're using rails, too.
112
+ Let's say you have a person model that has an `Option[Hat]`, and you want to render a `Hat` partial only if the person has one.
113
+ You can simply use the `collection` key when rendering the partial - you don't even need an `if`:
114
+
115
+ <%= render partial: 'hat', collection: @person.hat %>
116
+
117
+ And one last useful example, `flatten`:
118
+
119
+ Some[Some[5], None, Some[7]].flatten # => Some[5,7]
120
+
121
+ [Some[6], None, Some[999]].flatten # => [6,999]
122
+
123
+ ## Pattern-matching
124
+
125
+ You'll find `Option`s in many functional languages such as ML, Haskell (as the Maybe monad), Scala and F#.
126
+ And in most cases, they will provide a way to access the values (not specific to `Option`s, but a more general part of the language), called pattern-matching.
127
+ We don't have pattern-matching in Ruby, but `Optional` provides a basic version for use with `Option`s. Here's how to use it:
128
+
129
+ option.match do |m|
130
+ m.some { |value| puts "The value is #{value}" }
131
+ m.none { puts "No value here" }
132
+ end
133
+
134
+ As you'd expect, the `some` branch is executed if there is a value, the `none` branch if there isn't.
135
+
136
+ You can also add cases that assert based on the content of the option:
137
+
138
+ option.match do |m|
139
+ m.some(:fedora) { puts "This will be printed only if I'm passed a Some[:fedora]" }
140
+ m.some(:trilby) { puts "This will be printed only if I'm passed a Some[:trilby]" }
141
+ m.none { puts "This is printed if I'm passed a None" }
142
+ end
143
+
144
+ The first case that matches is the one the match clause evaluates to.
145
+
146
+ You can also use lambdas as part of the match clauses like this:
147
+
148
+ option.match do |m|
149
+ m.some ->(x){ x.length > 2 } { puts "Printed if value's length is > 2" }
150
+ m.some ->(x){ x.is_a? Array } { puts "Printed if value is an array (with lengt <= 2)" }
151
+ m.none { puts "This is printed if I'm passed a None" }
152
+ end
153
+
154
+ ## Extra fun and goodness
155
+
156
+ What's been described so far is what you'd usually expect from `Option`s in other languages.
157
+ `Optional`, however, provides a few extra bits and pieces you may want to have a play with.
158
+
159
+ ## Logical operators (sort of logical, anyway)
160
+
161
+ Got two optional values and want to do something only if they *both* have values? Use `&`:
162
+
163
+ Some[5] & Some[6] # => Some[5,6]
164
+ Some[5] & None # => None
165
+ None & Some[5] # => None
166
+ None & None # => None
167
+
168
+ Got two optional values, either of which might be `None`, and want to do something with one of them, doesn't matter which? Use `|`:
169
+
170
+ Some[5] | Some[6] # => Some[5]
171
+ Some[5] | None # => Some[5]
172
+ None | Some[6] # => Some[6]
173
+ None | None # => None
174
+
175
+ NB. Technically, an `Option` should only have up to one value, but to allow the `&` operator and similar things `Optional` sort of cheats by treating 'multiple values' as a single value of type `Array`.
176
+
177
+ ## Mapping through multiple functions
178
+
179
+ You might find yourself needing to map an optional value through a number of functions. There's a handy way to do this with `Option`s:
180
+
181
+ p = Person.create(name: "Russell!")
182
+
183
+ Some[p].map_through(:name, :upcase) # => Some["RUSSELL!"]
184
+ None.map_through(:name, :upcase) # => None
185
+
186
+ ## Juxtaposing the result of multiple functions
187
+
188
+ This one is nicked from Clojure (there might be other languages that have it, I don't know). Call a list of functions on an option value, returning a `Some` of their results:
189
+
190
+ p = Person.create(name: "Russell!", age: 29, hat: :fedora)
191
+
192
+ Some[p].juxt(:name, :age, :class) # => Some["Russell", 29, Person]
193
+ None.juxt(:name, :age, :class) # => None
194
+
195
+ If you're feeling adventurous you could always monkey patch this onto `Object` so it's available for everything. Might come in useful.
196
+
197
+ ## That's about it
198
+
199
+ I hope you find `Option`s useful. Let me know if you do: I'm @rsslldnphy on that Twitter. I'm very enthusiastic about optional types and functional programming techniques in general, so if you have a question or want advice on using options in your project I'll be happy to answer!
25
200
 
26
- gwen.hat.match do |m|
27
- m.some (:flat_cap) { puts "Hey up!" }
28
- m.some (:fedora) { |h| puts "*touches brim of #{h} respectfully*" }
29
- m.none { puts "Hello!" }
30
- end
201
+ If you'd like to contribute, create a pull request for the feature or fix you have in mind. That way we can discuss whether it's a good fit and how best to approach it before you start on the code. Contributions very welcome!
@@ -1,6 +1,10 @@
1
1
  module Option
2
2
  include Option::Enumerable
3
3
 
4
+ def self.[] value
5
+ value.nil? ? None : Some[value]
6
+ end
7
+
4
8
  def match(&block)
5
9
  Match.new.tap { |m| block.call(m) }.evaluate(self)
6
10
  end
@@ -2,11 +2,6 @@ module Option
2
2
  module Enumerable
3
3
  include ::Enumerable
4
4
 
5
- def do &block
6
- each &block
7
- self
8
- end
9
-
10
5
  def to_ary
11
6
  to_a
12
7
  end
@@ -20,7 +15,7 @@ module Option
20
15
  end
21
16
 
22
17
  def map_through(*methods)
23
- methods.reduce(self) { |acc, m| acc.map(&m) }
18
+ map &methods.reduce(->(x){x}){ |acc, m| ->(x) { acc.call(x).send(m) } }
24
19
  end
25
20
 
26
21
  def map
@@ -1,9 +1,9 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'optional'
3
- s.version = '0.0.4'
3
+ s.version = '0.0.5'
4
4
  s.date = '2013-04-19'
5
5
  s.summary = "Optional values with pattern matching"
6
- s.description = "Make nils go bye bye with Options!"
6
+ s.description = "Make nils a thing of the past with Options!"
7
7
  s.authors = ["Russell Dunphy", "Radek Molenda"]
8
8
  s.email = ['russell@russelldunphy.com', 'radek.molenda@gmail.com']
9
9
  s.files = `git ls-files`.split($\)
@@ -5,19 +5,6 @@ describe Option::Enumerable do
5
5
  let (:cat) { Cat.new("MOGGIE!") }
6
6
  let (:dog) { Dog.new("DOGGIE!") }
7
7
 
8
- it "makes radek less sad :-(" do
9
- Some[4].each { |e| e.should be_a Fixnum }
10
- Some[4,5].each { |e| e.should be_a Fixnum }
11
- end
12
-
13
- describe "#do" do
14
- it "allows ops with side effects to be performed using the value as part of a method chain" do
15
- names = ""
16
- Some[cat].do { |c| names << c.name }.map(&:name).should eq Some["MOGGIE!"]
17
- names.should eq "MOGGIE!"
18
- end
19
- end
20
-
21
8
  describe "#map_through" do
22
9
  it "allows mapping through multiple methods" do
23
10
  Some[cat, dog].map_through(:name, :chars, :first).should eq Some["M", "D"]
@@ -39,4 +39,11 @@ describe Option do
39
39
  end
40
40
  end
41
41
 
42
+ it 'can create a some from a passed value' do
43
+ Option[4].should eq Some[4]
44
+ end
45
+
46
+ it 'can create a none from a passed value' do
47
+ Option[nil].should be_none
48
+ end
42
49
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: optional
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -44,7 +44,7 @@ dependencies:
44
44
  - - ! '>='
45
45
  - !ruby/object:Gem::Version
46
46
  version: '0'
47
- description: Make nils go bye bye with Options!
47
+ description: Make nils a thing of the past with Options!
48
48
  email:
49
49
  - russell@russelldunphy.com
50
50
  - radek.molenda@gmail.com