sums_up 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e7e0c128cef808336972ac4b012e8d7d7c4fafa6
4
+ data.tar.gz: 777387cbd3a186d93e81f24e0190ee031ab6415a
5
+ SHA512:
6
+ metadata.gz: 2083e37f3b61a5240fb6d42769ce946f6b36dbaa2da7fe4c2f5c9392cb62c2c5956c834637717ec3ba9c54f6bb4f3303709acc691ffff4423d2cf35f1fdb0c4c
7
+ data.tar.gz: 49596b05073fc5d42365efb07c750403fa8db6f4a2544f3a9bab999cafd05ab279c6b1ea8efe250ab89139bf383d8a1a5de86ff4f7c8647f5a44aa1d6364527c
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ SuggestExtensions: false
4
+ TargetRubyVersion: 2.4.10
5
+
6
+ Layout/LineLength:
7
+ Max: 80
8
+
9
+ Layout/MultilineMethodCallIndentation:
10
+ EnforcedStyle: indented
11
+
12
+ Metrics/AbcSize:
13
+ Enabled: false
14
+
15
+ Metrics/BlockLength:
16
+ Enabled: false
17
+
18
+ Metrics/MethodLength:
19
+ Enabled: false
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.4.10
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.4.10
6
+ - 2.5.8
7
+ - 2.6.6
8
+ - 2.7.2
9
+ - 3.0.0
10
+ before_install: gem install bundler -v 2.1.2
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at hulihan.tom159@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in sums_up.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 Tom Hulihan
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,815 @@
1
+ # sums_up
2
+
3
+ Sum types for Ruby with zero runtime dependencies. Inspired by [hojberg/sums-up](https://github.com/hojberg/sums-up).
4
+
5
+ [![Build Status](https://travis-ci.org/nahiluhmot/sums_up.svg?branch=master)](https://travis-ci.org/nahiluhmot/sums_up)
6
+
7
+ * [What is a Sum Type?](#what-is-a-sum-type)
8
+ * [Quick Start](#quick-start)
9
+ * [Defining Sum Types](#defining-sum-types)
10
+ * [Predicates](#predicates)
11
+ * [Pattern Matching with Hashes](#pattern-matching-with-hashes)
12
+ * [Pattern Matching with Blocks](#pattern-matching-with-blocks)
13
+ * [Methods on Sum Types](#methods-on-sum-types)
14
+ * [Variant Instance Methods](#variant-instance-methods)
15
+ * [A Note on Mutability](#a-note-on-mutability)
16
+ * [Maybes](#maybes)
17
+ * [Results](#results)
18
+ * [Why?](#why)
19
+ * [Development](#development)
20
+ * [Contributing](#contributing)
21
+ * [License](#license)
22
+ * [Code of Conduct](#code-of-conduct)
23
+
24
+ ## What is a Sum Type?
25
+
26
+ Sum types are data structures with multiple variants.
27
+ Ruby does not have sum types, but many concepts in the language (like booleans, integers, errors, state machines, etc.) can be described using sum types.
28
+ Sum types are not limited to those use-cases, however, and are a powerful tool for modeling domain-specific data as well.
29
+
30
+ This README uses non-generalized examples of sum types to help build an intuition for when they might be useful.
31
+ To learn more about sum types, I recommend watching [Philip Wadler's Category Theory for the Working Hacker](https://www.youtube.com/watch?v=V10hzjgoklA) and checking out [Elm's Custom Types](https://guide.elm-lang.org/types/custom_types.html), [Haskell's Sum Types](https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/sum-types), [Rust's Enums](https://doc.rust-lang.org/book/ch06-01-defining-an-enum.html), and the [Wikipedia article on Algebraic data types](https://en.wikipedia.org/wiki/Algebraic_data_type).
32
+
33
+ ## Quick Start
34
+
35
+ Define a sum type:
36
+
37
+ ```ruby
38
+ Direction = SumsUp.define(:north, :south, :east, :west)
39
+ # => Direction
40
+
41
+ Direction.north
42
+ # => #<variant Direction::North>
43
+
44
+ Direction.south
45
+ # => #<variant Direction::South>
46
+
47
+ Direction.east
48
+ # => #<variant Direction::East>
49
+
50
+ Direction.west
51
+ # => #<variant Direction::West>
52
+ ```
53
+
54
+ Use predicates to distinguish between variants:
55
+
56
+ ```ruby
57
+ def latitudinal?(direction)
58
+ direction.north? ||
59
+ direction.south?
60
+ end
61
+
62
+ latitudinal?(Direction.south)
63
+ # => true
64
+
65
+ latitudinal?(Direction.west)
66
+ # => false
67
+ ```
68
+
69
+ Call `#match` to categorically handle each variant by name:
70
+
71
+ ```ruby
72
+ def turn_clockwise(direction)
73
+ direction.match do |m|
74
+ m.north { Direction.east }
75
+ m.south { Direction.west }
76
+ m.east { Direction.south }
77
+ m.west { Direction.north }
78
+ end
79
+ end
80
+
81
+ turn_clockwise(Direction.north)
82
+ # => #<variant Direction::East>
83
+
84
+ turn_clockwise(turn_clockwise(Direction.north))
85
+ # => #<variant Direction::South>
86
+ ```
87
+
88
+ ## Defining Sum Types
89
+
90
+ Imagine we're writing software for a coffee shop.
91
+ The menu might look something like this:
92
+
93
+ | Item | Small | Large |
94
+ |----------------------|-------|-------|
95
+ | Water | Free | |
96
+ | Lemonade | $3.50 | $4.50 |
97
+ | Coffee (Hot or Iced) | $2.95 | $3.95 |
98
+
99
+ To model the menu using sum types, let's start out with some simple enumerations:
100
+
101
+ ```ruby
102
+ Size = SumsUp.define(:small, :large)
103
+ # => Size
104
+
105
+ Temperature = SumsUp.define(:hot, :iced)
106
+ # => Temperature
107
+
108
+ Size.small
109
+ # => #<variant Size::Small>
110
+
111
+ Size.large
112
+ # => #<variant Size::Large>
113
+
114
+ Temperature.hot
115
+ # => #<variant Temperature::Hot>
116
+
117
+ Temperature.iced
118
+ # => #<variant Temperature::Iced>
119
+ ```
120
+
121
+ Enumerations work well for `Size` and `Temperature`, but defining a `Drink` type will be a bit more work.
122
+ There are multiple kinds of drinks (water, lemonade, and coffee), each of which has a varying set of attributes (some drinks are available in multiple sizes, coffee can be served hot or iced).
123
+
124
+ To describe these relationships, let's define a sum type with variants who have members.
125
+ In the below example, `Drink.water` has no members, `Drink.lemonade` has a `size`, and `Drink.coffee` has a `size` and `temperature`:
126
+
127
+ ```ruby
128
+ Drink = SumsUp.define(
129
+ :water,
130
+ lemonade: :size,
131
+ coffee: [:size, :temperature]
132
+ )
133
+ # => Drink
134
+
135
+ Drink.water
136
+ # => #<variant Drink::Water>
137
+
138
+ lemonade = Drink.lemonade(Size.small)
139
+ # => #<variant Drink::Lemonade size=#<variant Size::Small>>
140
+
141
+ lemonade.size
142
+ # => #<variant Size::Small>
143
+
144
+ coffee = Drink.coffee(Size.large, Temperature.iced)
145
+ # => #<variant Drink::Coffe size=#<variant Size::Large> temperature=#<variant Temperature::Iced>>
146
+
147
+ coffee.size
148
+ # => #<variant Size::Large>
149
+
150
+ coffee.temperature
151
+ # => #<variant Temperature::Iced>
152
+
153
+ # Raises because only coffee and lemonade have a size.
154
+ Drink.water.size
155
+ # => NoMethodError: undefined method `size' for #<variant Drink::Water>
156
+ ```
157
+
158
+ ## Predicates
159
+
160
+ Predicates are defined for each variant of a sum type:
161
+
162
+ ```ruby
163
+ Size.large.large?
164
+ # => true
165
+
166
+ Temperature.hot.iced?
167
+ # => false
168
+
169
+ Temperature.iced.iced?
170
+ # => true
171
+
172
+ Drink.water.coffee?
173
+ # => false
174
+
175
+ # Raises because Temperature only has `#hot?` and `#iced?` predicates.
176
+ Temperature.hot.water?
177
+ # => NoMethodError: undefined method `water?' for #<variant Temperature::Hot>
178
+ ```
179
+
180
+ We can use these to write a function which returns the `Temperature` for a given `Drink`.
181
+ `Drink.coffee` is the only variant which has an explicit `temperature` attribute, but we know that both `Drink.water` and `Drink.lemonade` are only served iced.
182
+
183
+ ```ruby
184
+ def drink_temperature(drink)
185
+ if drink.coffee?
186
+ drink.temperature
187
+ else
188
+ Temperature.iced
189
+ end
190
+ end
191
+
192
+ drink_temperature(Drink.lemonade(Size.large))
193
+ # => #<variant Temperature::Iced>
194
+
195
+ drink_temperature(Drink.coffee(Size.small, Temperature.hot))
196
+ # => #<variant Temperature::Hot>
197
+ ```
198
+
199
+ ## Pattern Matching with Hashes
200
+
201
+ Another way to distinguish sum type variants is pattern matching.
202
+ We can use pattern matching with Hashes to define formatters for `Size` and `Temperature`:
203
+
204
+ ```ruby
205
+ def format_size(size)
206
+ size.match(small: 'Small', large: 'Large')
207
+ end
208
+
209
+ def format_temperature(temperature)
210
+ temperature.match(hot: 'Hot', iced: 'Iced')
211
+ end
212
+
213
+ format_size(Size.large)
214
+ # => 'Large'
215
+
216
+ format_temperature(Temperature.iced)
217
+ # => 'Iced'
218
+ ```
219
+
220
+ In some cases, it can be convenient to match against some variants and use a wildcard for the rest:
221
+
222
+ ```ruby
223
+ def free?(drink)
224
+ drink.match(water: true, _: false)
225
+ end
226
+
227
+ free?(Drink.water)
228
+ # => true
229
+
230
+ free?(Drink.lemonade(Size.large))
231
+ # => false
232
+ ```
233
+
234
+ `#match` will raise if any variants are left unmatched.
235
+ The following method does not handle `Drink.water` and will raise whenever any drink is provided:
236
+
237
+ ```ruby
238
+ def added_sugar?(drink)
239
+ drink.match(lemonade: true, coffee: false)
240
+ end
241
+
242
+ # Raises because water is not matched.
243
+ added_sugar?(Drink.water)
244
+ # => SumsUp::UnmatchedVariantError: Did not match the following variants: water
245
+
246
+ # Raises because water is not matched, even though a lemonade is getting passed in.
247
+ added_sugar?(Drink.lemonade(Size.large))
248
+ # => SumsUp::UnmatchedVariantError: Did not match the following variants: water
249
+ ```
250
+
251
+ ## Pattern Matching with Blocks
252
+
253
+ Matching against the variant name is often not enough, we need to be able to use the variant's members as well.
254
+ For these use-cases, `#match` accepts a block.
255
+ For variants with members, each member is yielded to the `#match` block:
256
+
257
+ ```ruby
258
+ def format_drink(drink)
259
+ drink.match do |m|
260
+ m.water { 'Water' }
261
+ m.lemonade { |size| "#{format_size(size)} Lemonade" }
262
+ m.coffee do |size, temperature|
263
+ "#{format_size(size)} #{format_temperature(temperature)} Coffee"
264
+ end
265
+ end
266
+ end
267
+
268
+ format_drink(Drink.water)
269
+ # => 'Water'
270
+
271
+ format_drink(Drink.lemonade(Size.small))
272
+ # => 'Small Lemonade'
273
+
274
+ format_drink(Drink.coffee(Size.large, Temperature.iced))
275
+ # => 'Large Iced Coffee'
276
+ ```
277
+
278
+ Like Hash-based pattern matching, Block-based pattern matching can use wildcards as well.
279
+ The below example redefines `drink_temperature` using pattern matching:
280
+
281
+ ```ruby
282
+ def drink_temperature(drink)
283
+ drink.match do |m|
284
+ m.coffee { |_size, temperature| temperature }
285
+ m._ { Temperature.iced }
286
+ end
287
+ end
288
+
289
+ drink_temperature(Drink.water)
290
+ # => #<variant Temperature::Iced>
291
+ ```
292
+
293
+ Note: if using the wildcard pattern matcher (`_`), it must come after the explicit variant matches.
294
+
295
+ The match syntax also supports passing values directly to the matcher, as opposed to passing a block:
296
+
297
+ ```ruby
298
+ # Waters are always small, other drinks use their specified size.
299
+ def drink_size(drink)
300
+ drink.match do |m|
301
+ m.water Size.small
302
+ m.lemonade { |size| size }
303
+ m.temperature { |size, _temperature| size }
304
+ end
305
+ end
306
+
307
+ drink_size(Drink.water)
308
+ # => #<variant Size::Small>
309
+
310
+ drink_size(Drink.lemonade(Size.small))
311
+ # => #<variant Size::Small>
312
+ ```
313
+
314
+ This syntax will also raise if not all variants of a type are matched:
315
+
316
+ ```ruby
317
+ def drink_price(drink)
318
+ drink.match do |m|
319
+ m.water 0
320
+ m.lemonade { |size| size.match(small: 350, large: 450) }
321
+ end
322
+ end
323
+
324
+ # Raises because coffee is not matched.
325
+ drink_price(Drink.coffee(Size.large, Temperature.hot))
326
+ # => SumsUp::UnmatchedVariantError: Did not match the following variants: coffee
327
+
328
+ # Raises because coffee is not matched, even though a water is getting passed in.
329
+ drink_price(Drink.water)
330
+ # => SumsUp::UnmatchedVariantError: Did not match the following variants: coffee
331
+ ```
332
+
333
+ ## Methods on Sum Types
334
+
335
+ When defining a sum type, we can add methods to it by passing a block to `SumsUp.define`:
336
+
337
+ ```ruby
338
+ Drink = SumsUp.define(:water, lemonade: :size, coffee: [:temperature, :size]) do
339
+ def price_in_cents
340
+ match do |m|
341
+ m.water 0
342
+ m.lemonade { |size| size.match(small: 350, large: 450) }
343
+ m.coffee { |size, _temperature| size.match(small: 295, large: 395) }
344
+ end
345
+ end
346
+ end
347
+
348
+ Drink.water.price_in_cents
349
+ # => 0
350
+
351
+ Drink.lemonade(Size.small).price_in_cents
352
+ # => 350
353
+
354
+ Drink.coffee(Size.large, Temperature.hot).price_in_cents
355
+ # => 395
356
+ ```
357
+
358
+ This syntax also supports class methods and constants:
359
+
360
+ ```ruby
361
+ Size = SumsUp.define(:small, :large) do
362
+ SMALL_STRING = 'Small'.freeze
363
+ LARGE_STRING = 'Large'.freeze
364
+
365
+ def self.parse(str)
366
+ case str
367
+ when SMALL_STRING
368
+ small
369
+ when LARGE_STRING
370
+ large
371
+ else
372
+ raise ArgumentError, "Invalid size: #{str}"
373
+ end
374
+ end
375
+ end
376
+
377
+ Size::SMALL_STRING
378
+ # => 'Small'
379
+
380
+ Size::LARGE_STRING
381
+ # => 'Large'
382
+
383
+ Size.parse('Small')
384
+ # => #<variant Size::Small>
385
+
386
+ Size.parse('Trenta')
387
+ # => ArgumentError: Invalid size: Trenta
388
+ ```
389
+
390
+ ## Variant Instance Methods
391
+
392
+ In addition to user-defined methods, `#inspect`, and `#==`, variant instances come with convenience methods for accessing and updating members.
393
+
394
+ ### Getters
395
+
396
+ Fetch a variant's members by name:
397
+
398
+ ```ruby
399
+ coffee = Drink.coffee(Size.small, Temperature.hot)
400
+ coffee.size
401
+ # => #<variant Size::Small>
402
+
403
+ coffee.temperature
404
+ # => #<variant Temperature::Hot>
405
+
406
+ lemonade = Drink.lemonade(Size.large)
407
+ lemonade.size
408
+ # => #<variant Size::Large>
409
+
410
+ # Lemonade does not have a 'temperature' member.
411
+ lemonade.temperature
412
+ # => NoMethodError: undefined method `temperature' for #<variant Drink::Lemonade size=#<variant Size::Large>>
413
+ ```
414
+
415
+ Another way to access members is `#[]`:
416
+
417
+ ```ruby
418
+ coffee = Drink.coffee(Size.small, Temperature.iced)
419
+ coffee[:size]
420
+ # => #<variant Size::Small>
421
+
422
+ # #[] works with Strings as well.
423
+ lemonade = Drink.lemonade(Size.large)
424
+ lemonade['size']
425
+ # => #<variant Size::Large>
426
+
427
+ # #[] will raise given an invalid member.
428
+ lemonade[:temperature]
429
+ # => NameError: No member 'temperature' in variant lemonade.
430
+ ```
431
+
432
+ ### Setters
433
+
434
+ Members may also be updated by name:
435
+
436
+ ```ruby
437
+ coffee = Drink.coffee(Size.small, Temperature.hot)
438
+ coffee.temperature = Temperature.iced # Oh, sorry, could you make that iced?
439
+ coffee.temperature
440
+ # => #<variant Temperature::Iced>
441
+ ```
442
+
443
+ `#[]=` can also update a member:
444
+
445
+ ```ruby
446
+ lemonade = Drink.lemonade(Size.large)
447
+ lemonade['size'] = Size.small # Oh, a large is 32oz?
448
+ lemonade.size
449
+ # => #<variant Size::Small>
450
+
451
+ lemonade[:temperature] = Temperature.hot # Sorry, we don't do that here.
452
+ # => NameError: No member 'temperature' in variant lemonade.
453
+ ```
454
+
455
+ ### `#attributes`
456
+
457
+ Get a variant's members as a `Hash`:
458
+
459
+ ```ruby
460
+ Drink.water.attributes
461
+ # => {}
462
+
463
+ Drink.lemonade(Size.small).attributes
464
+ # => { size: #<variant Size::Small> }
465
+
466
+ Drink.coffee(Size.large, Temperataure.iced).attributes
467
+ # => { size: #<variant Size::Large> , temperature: #<variant Temperature::Iced> }
468
+ ```
469
+
470
+ ### `#to_h`
471
+
472
+ Return the variant as a `Hash`:
473
+
474
+ ```ruby
475
+ Drink.water.to_h
476
+ # => { water: {} }
477
+
478
+ Drink.lemonade(Size.small).to_h
479
+ # => { lemonade: { size: #<variant Size::Small> } }
480
+
481
+ Drink.coffee(Size.large, Temperataure.iced).to_h
482
+ # => { coffee: { size: #<variant Size::Large> , temperature: #<variant Temperature::Iced> } }
483
+ ```
484
+
485
+ Use `include_root: false` to make this method behave like `#attributes`:
486
+
487
+ ```ruby
488
+ Drink.water.to_h(include_root: false)
489
+ # => {}
490
+
491
+ Drink.lemonade(Size.small).to_h(include_root: false)
492
+ # => { size: #<variant Size::Small> }
493
+
494
+ Drink.coffee(Size.large, Temperataure.iced).to_h(include_root: false)
495
+ # => { size: #<variant Size::Large>, temperature: #<variant Temperature::Iced> }
496
+ ```
497
+
498
+ ### `#members`
499
+
500
+ Get a variant's members in the order they were in when passed into the initializer:
501
+
502
+ ```ruby
503
+ Drink.water.members
504
+ # => []
505
+
506
+ Drink.lemonade(Size.small).members
507
+ # => [#<variant Size::Small>]
508
+
509
+ Drink.coffee(Size.large, Temperataure.iced).members
510
+ # => [#<variant Size::Large>, #<variant Temperature::Iced>]
511
+ ```
512
+
513
+ ## A Note on Mutability
514
+
515
+ All variants without members are memoized and frozen by default.
516
+ In our running example calling `Size.small`, `Size.large`, `Temperature.hot`, `Temperature.iced`, and `Drink.water` would all return memoized and frozen objects, but `Drink.lemonade(size)` and `Drink.coffee(size, temperature)` would not.
517
+ This helps reduce the memory footprint of the gem, but makes it so that we cannot write to instance variables within the class.
518
+
519
+ Let's say that we wanted to memoize the result of `#price_in_cents` like so:
520
+
521
+ ```ruby
522
+ Drink = SumsUp.define(:water, lemonade: :size, coffee: [:temperature, :size]) do
523
+ def price_in_cents
524
+ @price_in_cents ||= match do |m|
525
+ m.water 0
526
+ m.lemonade { |size| size.match(small: 350, large: 450) }
527
+ m.coffee { |size, _temperature| size.match(small: 295, large: 395) }
528
+ end
529
+ end
530
+ end
531
+ ```
532
+
533
+ The `Drink.lemonade` and `Drink.coffee` variants would be unaffected because they are not frozen:
534
+
535
+ ```ruby
536
+ Drink.lemonade(Size.large).price_in_cents
537
+ # => 450
538
+
539
+ Drink.coffee(Size.small, Temperature.hot).price_in_cents
540
+ # => 295
541
+ ```
542
+
543
+ However, `Drink.water` will raise because it is frozen:
544
+
545
+ ```ruby
546
+ Drink.water.price_in_cents
547
+ # => RuntimeError: can't modify frozen Drink::Water
548
+ ```
549
+
550
+ In general, it's better to find solutions which don't require state to be tracked within data types, but if mutability is absolutely required, we can work around this by passing `memo: false` to the memberless variant's initializer:
551
+
552
+ ```ruby
553
+ Drink.water(memo: false).price_in_cents
554
+ # => 0
555
+ ```
556
+
557
+ This will work with any memberless variant:
558
+
559
+ ```ruby
560
+ Size.small(memo: false)
561
+ # => #<variant Size::Small>
562
+
563
+ Size.large(memo: false)
564
+ # => #<variant Size::Large>
565
+
566
+ Temperature.hot(memo: false)
567
+ # => #<variant Temperature::Hot>
568
+
569
+ Temperature.iced(memo: false)
570
+ # => #<variant Temperature::Iced>
571
+ ```
572
+
573
+ ## Maybes
574
+
575
+ `SumsUp::Maybe` represents a value which may or may not be present.
576
+
577
+ Variants:
578
+
579
+ ```ruby
580
+ SumsUp::Maybe.nothing
581
+ # => #<variant SumsUp::Maybe::Nothing>
582
+
583
+ SumsUp::Maybe.just(1)
584
+ # => #<variant SumsUp::Maybe::Just value=1>
585
+ ```
586
+
587
+ Predicates:
588
+
589
+ ```ruby
590
+ SumsUp::Maybe.nothing.nothing?
591
+ # => true
592
+
593
+ SumsUp::Maybe.nothing.just?
594
+ # => false
595
+
596
+ SumsUp::Maybe.just(1).nothing?
597
+ # => false
598
+
599
+ SumsUp::Maybe.just(2).just?
600
+ # => true
601
+ ```
602
+
603
+ Pattern matching:
604
+
605
+ ```ruby
606
+ def maybe_to_int(maybe)
607
+ maybe.match do |m|
608
+ m.nothing 0
609
+ m.just { |num| num }
610
+ end
611
+ end
612
+
613
+ maybe_to_int(SumsUp::Maybe.nothing)
614
+ # => 0
615
+
616
+ maybe_to_int(SumsUp::Maybe.just(1))
617
+ # => 1
618
+ ```
619
+
620
+ `SumsUp::Maybe.of` builds a `SumsUp::Maybe` from a value which may be `nil`:
621
+
622
+ ```ruby
623
+ SumsUp::Maybe.of(nil)
624
+ # => #<variant SumsUp::Maybe::Nothing>
625
+
626
+ SumsUp::Maybe.of('cat')
627
+ # => #<variant SumsUp::Maybe::Just value="cat">
628
+
629
+ SumsUp::Maybe.of(false)
630
+ # => #<variant SumsUp::Maybe::Just value=false>
631
+ ```
632
+
633
+ `SumsUp::Maybe#map` applies a function to the value if it's present:
634
+
635
+ ```ruby
636
+ SumsUp::Maybe.nothing.map { |x| x + 1 }
637
+ # => #<variant SumsUp::Maybe::Nothing>
638
+
639
+ SumsUp::Maybe.just(3).map { |x| x + 1 }
640
+ # => #<variant SumsUp::Maybe::Just value=4>
641
+ ```
642
+
643
+ `SumsUp::Maybe#or_else` returns the wrapped value, or a default if it's not present:
644
+
645
+ ```ruby
646
+ SumsUp::Maybe.nothing.or_else(1)
647
+ # => 1
648
+
649
+ SumsUp::Maybe.nothing.or_else { 2 }
650
+ # => 2
651
+
652
+ SumsUp::Maybe.just(3).or_else(4)
653
+ # => 3
654
+
655
+ SumsUp::Maybe.just(4).or_else { 5 }
656
+ # => 4
657
+ ```
658
+
659
+ ## Results
660
+
661
+ `SumsUp::Result` represents a successful result or an error.
662
+
663
+ Variants:
664
+
665
+ ```ruby
666
+ SumsUp::Result.failure('update failed')
667
+ # => #<variant SumsUp::Maybe::Failure error="update failed">
668
+
669
+ SumsUp::Maybe.success('request payload')
670
+ # => #<variant SumsUp::Maybe::Just value="request payload">
671
+ ```
672
+
673
+ Predicates:
674
+
675
+ ```ruby
676
+ SumsUp::Result.failure(false).failure?
677
+ # => true
678
+
679
+ SumsUp::Result.failure(0).success?
680
+ # => false
681
+
682
+ SumsUp::Result.success(true).failure?
683
+ # => false
684
+
685
+ SumsUp::Result.success(1).success?
686
+ # => true
687
+ ```
688
+
689
+ Pattern matching:
690
+
691
+ ```ruby
692
+ def flip_result(result)
693
+ result.match do |m|
694
+ m.failure { |error| SumsUp::Result.success(error) }
695
+ m.success { |value| SumsUp::Result.failure(value) }
696
+ end
697
+ end
698
+
699
+ flip_result(SumsUp::Result.success('yay'))
700
+ # => #<variant SumsUp::Result::Failure error="yay">
701
+
702
+ flip_result(flip_result(SumsUp::Result.failure('boo')))
703
+ # => #<variant SumsUp::Result::Failure error="boo">
704
+ ```
705
+
706
+ `SumsUp::Result.from_block` converts a block which may raise into a `SumsUp::Result`:
707
+
708
+ ```ruby
709
+ SumsUp::Result.from_block { raise 'unexpected error' }
710
+ # => #<variant SumsUp::Result::Failure error=#<RuntimeError: unexpected error>>
711
+
712
+ SumsUp::Result.from_block { 'good result' }
713
+ # => #<variant SumsUp::Result::Success value="good result">
714
+ ```
715
+
716
+ `SumsUp::Result#map` applies a function to the successful values:
717
+
718
+ ```ruby
719
+ SumsUp::Result.failure('sorry kid').map { |x| x + ', nothing personal' }
720
+ # => #<variant SumsUp::Result::Failure error="sorry kid">
721
+
722
+ SumsUp::Result.success(10).map { |x| x * 2 }
723
+ # => #<variant SumsUp::Result::Success value=20>
724
+ ```
725
+
726
+ `SumsUp::Result#map_failure` applies a function to the failure errors:
727
+
728
+ ```ruby
729
+ SumsUp::Result.failure('sorry kid').map_failure { |x| x + ', nothing personal' }
730
+ # => #<variant SumsUp::Result::Failure error="sorry kid, nothing personal">
731
+
732
+ SumsUp::Result.success(10).map_failure { |x| x * 2 }
733
+ # => #<variant SumsUp::Result::Success value=10>
734
+ ```
735
+
736
+ ## Why?
737
+
738
+ Some of these examples may seem odd if you're not familiar with sum types.
739
+ If we were instead using the tools provided by Ruby, we might use a boolean to determine whether a given drink is a small or large, hot or cold.
740
+ This would work, of course, so why use sum types?
741
+
742
+ Let's illustrate by defining `OtherDrink` using a `Struct` with booleans for `is_hot` and `is_large`
743
+
744
+ ```ruby
745
+ OtherDrink = Struct.new(:type, :is_hot, :is_large) do
746
+ private_class_method(:new)
747
+
748
+ def self.water
749
+ new(:water, false, false)
750
+ end
751
+
752
+ def self.lemonade(is_large)
753
+ new(:lemonade, false, is_large)
754
+ end
755
+
756
+ def self.coffee(is_hot, is_large)
757
+ new(:coffee, is_hot, is_large)
758
+ end
759
+ end
760
+
761
+ OtherDrink.water
762
+ # => #<struct OtherDrink type=:water, is_hot=false, is_large=false>
763
+
764
+ OtherDrink.lemonade(true)
765
+ # => #<struct OtherDrink type=:lemonade, is_hot=false, is_large=true>
766
+
767
+ OtherDrink.coffee(true, false)
768
+ # => #<struct OtherDrink type=:water, is_hot=true, is_large=false>
769
+ ```
770
+
771
+ `OtherDrink` can do all of the things that `Drink` can, but its API is less descriptive.
772
+ For example, to represent a small hot coffee using `OtherDrink`, we would call `OtherDrink.coffee(true, false)`.
773
+ In the `sums_up`-defined `Drink`, we'd instead call `Drink.coffee(Size.small, Temperature.hot)`.
774
+ This may seem a bit contrived, but using a sum type instead of a boolean can help make our code more declarative and self-documenting.
775
+
776
+ Sum types can also provide extensibility when project requirements change.
777
+ If our example cafe started carrying medium coffees and lemonades, we only need update our `Size` type to accomodate that:
778
+
779
+ ```ruby
780
+ Size = SumsUp.define(:small, :medium, :large)
781
+
782
+ Drink.coffee(Size.medium, Temperature.iced)
783
+ # => #<variant Drink::Coffee size=#<variant Size::Medium> temperature=#<variant Temperature::Iced>>
784
+ ```
785
+
786
+ How would we handle this if we were using `OtherDrink`?
787
+ A boolean is no longer suitable given that we need to track three different possible options, so we would probably end up using symbols like `:small`, `:medium`, and `:large`.
788
+ This will work, but refactoring will likely be more difficult.
789
+
790
+ With `Size`, after adding `Size.medium`, we find our `Size#match` calls, ensure that we're handling `Size.medium`, and we're done.
791
+ With `OtherDrink`'s `:small`, `:medium`, and `:large` symbols, we would need to refactor the code which uses `OtherDrink.is_large` to instead match on symbols, and we would also introduce the possibility that a drink's size is invalid.
792
+ This can lead to us writing a lot of checks for invalid data which may or may not be necessary.
793
+ Sum types will do this for you; there's no way to make an invalid `Size`, so we know that our `#match` calls are categorically handling all cases.
794
+
795
+ ## Development
796
+
797
+ After checking out the repo, run `bin/setup` to install dependencies.
798
+ Then, run `bundle exec rake spec` to run the tests.
799
+ You can also run `bin/console` for an interactive prompt that will allow you to experiment.
800
+
801
+ To install this gem onto your local machine, run `bundle exec rake install`.
802
+ To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
803
+
804
+ ## Contributing
805
+
806
+ Bug reports and pull requests are welcome on GitHub at https://github.com/nahiluhmot/sums_up.
807
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/nahiluhmot/sums_up/blob/master/CODE_OF_CONDUCT.md).
808
+
809
+ ## License
810
+
811
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
812
+
813
+ ## Code of Conduct
814
+
815
+ Everyone interacting in the SumsUp projects codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/nahiluhmot/sums_up/blob/master/CODE_OF_CONDUCT.md).