├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Gemfile ├── Guardfile ├── HISTORY.md ├── LICENSE.txt ├── README.md ├── Rakefile ├── deterministic.gemspec ├── lib ├── deterministic.rb └── deterministic │ ├── core_ext │ ├── object │ │ └── result.rb │ └── result.rb │ ├── either.rb │ ├── enum.rb │ ├── match.rb │ ├── maybe.rb │ ├── monad.rb │ ├── null.rb │ ├── option.rb │ ├── protocol.rb │ ├── result.rb │ ├── sequencer.rb │ └── version.rb └── spec ├── examples ├── amount_spec.rb ├── config_spec.rb ├── controller_spec.rb ├── list.rb ├── list_spec.rb ├── logger_spec.rb └── validate_address_spec.rb ├── lib ├── deterministic │ ├── class_mixin_spec.rb │ ├── core_ext │ │ ├── object │ │ │ └── either_spec.rb │ │ └── result_spec.rb │ ├── currify_spec.rb │ ├── either_spec.rb │ ├── maybe_spec.rb │ ├── monad_axioms.rb │ ├── monad_spec.rb │ ├── null_spec.rb │ ├── option_spec.rb │ ├── protocol_spec.rb │ ├── result │ │ ├── failure_spec.rb │ │ ├── result_map_spec.rb │ │ ├── result_shared.rb │ │ └── success_spec.rb │ ├── result_spec.rb │ └── sequencer_spec.rb └── enum_spec.rb ├── readme_spec.rb └── spec_helper.rb /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Ruby workflows 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | ruby-version: ['2.7', '3.0', '3.1', '3.2'] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | - uses: ruby/setup-ruby@v1 15 | with: 16 | ruby-version: ${{ matrix.ruby-version }} 17 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically 18 | - name: Run tests 19 | run: bundle exec rake 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in ..gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard :bundler do 2 | watch('Gemfile') 3 | end 4 | 5 | guard :bundler do 6 | watch('Gemfile') 7 | watch(/^.+\.gemspec/) 8 | end 9 | 10 | guard :rspec, cmd: 'bundle exec rspec' do 11 | watch(%r{^spec/.+_spec\.rb$}) 12 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 13 | watch('spec/spec_helper.rb') { "spec" } 14 | 15 | # Turnip features and steps 16 | watch(%r{^spec/acceptance/(.+)\.feature$}) 17 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' } 18 | end 19 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## v0.14.1 2 | 3 | - Make Ruby 1.9.3 compatible 4 | 5 | ## v0.14.0 6 | 7 | - Add `Result#+` 8 | - Alias pipe with << for left association 9 | - Make the None convenience wrapper always return instance 10 | - Add global convenience typdef for Success, Failure 11 | 12 | ## v0.13.0 13 | 14 | - Add `Option#value_to_a` 15 | - Add `Option#+` 16 | - Make `None` a real monad (little impact on the real world) 17 | - Add `Either` 18 | - Add `Option#value_or` 19 | - `None#value` is now private 20 | 21 | ## v0.12.1 22 | 23 | - Fix backwards compatibility with Ruby < 2.0.0 24 | 25 | ## v0.12.0 26 | 27 | - Add Option 28 | - Nest `Success` and `Failure` under `Result` 29 | 30 | ## v0.10.0 31 | ** breaking changes ** 32 | 33 | - Remove `Either#<<` 34 | - Rename `Either` to `Result` 35 | - Add `Result#pipe` aka `Result#**` 36 | - Add `Result#map` and `Result#map_err` 37 | 38 | ## v0.9.0 39 | ** breaking changes ** 40 | 41 | - Remove `Either.attempt_all` in favor of `Either#>>` and `Either#>=` 42 | This greatly reduces the complexity and the code necessary. 43 | 44 | ## 0.8.0 - v0.8.1 45 | 46 | - Introduce `Either#>>` and `Either#>=` 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Piotr Zolnierek 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deterministic 2 | [![Gitter](https://badges.gitter.im/Join Chat.svg)](https://gitter.im/pzol/deterministic?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 3 | 4 | [![Gem Version](https://badge.fury.io/rb/deterministic.png)](http://badge.fury.io/rb/deterministic) 5 | 6 | Deterministic is to help your code to be more confident, by utilizing functional programming patterns. 7 | 8 | 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 too forceful approach 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. 9 | 10 | ## Patterns 11 | 12 | Deterministic provides different monads, here is a short guide, when to use which 13 | 14 | #### Result: Success & Failure 15 | - an operation which can succeed or fail 16 | - the result (content) of of the success or failure is important 17 | - you are building one thing 18 | - chaining: if one fails (Failure), don't execute the rest 19 | 20 | #### Option: Some & None 21 | - an operation which returns either some result or nothing 22 | - in case it returns nothing it is not important to know why 23 | - you are working rather with a collection of things 24 | - chaining: execute all and then select the successful ones (Some) 25 | 26 | #### Either: Left & Right 27 | - an operation which returns several good and bad results 28 | - the results of both are important 29 | - chaining: if one fails, continue, the content of the failed and successful are important 30 | 31 | #### Maybe 32 | - an object may be nil, you want to avoid endless nil? checks 33 | 34 | #### Enums (Algebraic Data Types) 35 | - roll your own pattern 36 | 37 | ## Usage 38 | 39 | ### Result: Success & Failure 40 | 41 | ```ruby 42 | Success(1).to_s # => "1" 43 | Success(Success(1)) # => Success(1) 44 | 45 | Failure(1).to_s # => "1" 46 | Failure(Failure(1)) # => Failure(1) 47 | ``` 48 | 49 | Maps a `Result` with the value `a` to the same `Result` with the value `b`. 50 | 51 | ```ruby 52 | Success(1).fmap { |v| v + 1} # => Success(2) 53 | Failure(1).fmap { |v| v - 1} # => Failure(0) 54 | ``` 55 | 56 | Maps a `Result` with the value `a` to another `Result` with the value `b`. 57 | 58 | ```ruby 59 | Success(1).bind { |v| Failure(v + 1) } # => Failure(2) 60 | Failure(1).bind { |v| Success(v - 1) } # => Success(0) 61 | ``` 62 | 63 | Maps a `Success` with the value `a` to another `Result` with the value `b`. It works like `#bind` but only on `Success`. 64 | 65 | ```ruby 66 | Success(1).map { |n| Success(n + 1) } # => Success(2) 67 | Failure(0).map { |n| Success(n + 1) } # => Failure(0) 68 | ``` 69 | Maps a `Failure` with the value `a` to another `Result` with the value `b`. It works like `#bind` but only on `Failure`. 70 | 71 | ```ruby 72 | Failure(1).map_err { |n| Success(n + 1) } # => Success(2) 73 | Success(0).map_err { |n| Success(n + 1) } # => Success(0) 74 | ``` 75 | 76 | ```ruby 77 | Success(0).try { |n| raise "Error" } # => Failure(Error) 78 | ``` 79 | 80 | Replaces `Success a` with `Result b`. If a `Failure` is passed as argument, it is ignored. 81 | 82 | ```ruby 83 | Success(1).and Success(2) # => Success(2) 84 | Failure(1).and Success(2) # => Failure(1) 85 | ``` 86 | 87 | Replaces `Success a` with the result of the block. If a `Failure` is passed as argument, it is ignored. 88 | 89 | ```ruby 90 | Success(1).and_then { Success(2) } # => Success(2) 91 | Failure(1).and_then { Success(2) } # => Failure(1) 92 | ``` 93 | 94 | Replaces `Failure a` with `Result`. If a `Failure` is passed as argument, it is ignored. 95 | 96 | ```ruby 97 | Success(1).or Success(2) # => Success(1) 98 | Failure(1).or Success(1) # => Success(1) 99 | ``` 100 | 101 | Replaces `Failure a` with the result of the block. If a `Success` is passed as argument, it is ignored. 102 | 103 | ```ruby 104 | Success(1).or_else { Success(2) } # => Success(1) 105 | Failure(1).or_else { |n| Success(n)} # => Success(1) 106 | ``` 107 | 108 | Executes the block passed, but completely ignores its result. If an error is raised within the block it will **NOT** be catched. 109 | 110 | Try failable operations to return `Success` or `Failure` 111 | 112 | ```ruby 113 | include Deterministic::Prelude::Result 114 | 115 | try! { 1 } # => Success(1) 116 | try! { raise "hell" } # => Failure(#) 117 | ``` 118 | 119 | ### Result Chaining 120 | 121 | You can easily chain the execution of several operations. Here we got some nice function composition. 122 | The method must be a unary function, i.e. it always takes one parameter - the context, which is passed from call to call. 123 | 124 | The following aliases are defined 125 | 126 | ```ruby 127 | alias :>> :map 128 | alias :<< :pipe 129 | ``` 130 | 131 | This allows the composition of procs or lambdas and thus allow a clear definiton of a pipeline. 132 | 133 | ```ruby 134 | Success(params) >> 135 | validate >> 136 | build_request << log >> 137 | send << log >> 138 | build_response 139 | ``` 140 | 141 | #### Complex Example in a Builder Class 142 | 143 | ```ruby 144 | class Foo 145 | include Deterministic 146 | alias :m :method # method conveniently returns a Proc to a method 147 | 148 | def call(params) 149 | Success(params) >> m(:validate) >> m(:send) 150 | end 151 | 152 | def validate(params) 153 | # do stuff 154 | Success(validate_and_cleansed_params) 155 | end 156 | 157 | def send(clean_params) 158 | # do stuff 159 | Success(result) 160 | end 161 | end 162 | 163 | Foo.new.call # Success(3) 164 | ``` 165 | 166 | Chaining works with blocks (`#map` is an alias for `#>>`) 167 | 168 | ```ruby 169 | Success(1).map {|ctx| Success(ctx + 1)} 170 | ``` 171 | 172 | it also works with lambdas 173 | ```ruby 174 | Success(1) >> ->(ctx) { Success(ctx + 1) } >> ->(ctx) { Success(ctx + 1) } 175 | ``` 176 | 177 | and it will break the chain of execution, when it encounters a `Failure` on its way 178 | 179 | ```ruby 180 | def works(ctx) 181 | Success(1) 182 | end 183 | 184 | def breaks(ctx) 185 | Failure(2) 186 | end 187 | 188 | def never_executed(ctx) 189 | Success(99) 190 | end 191 | 192 | Success(0) >> method(:works) >> method(:breaks) >> method(:never_executed) # Failure(2) 193 | ``` 194 | 195 | `#map` aka `#>>` will not catch any exceptions raised. If you want automatic exception handling, the `#try` aka `#>=` will catch an error and wrap it with a failure 196 | 197 | ```ruby 198 | def error(ctx) 199 | raise "error #{ctx}" 200 | end 201 | 202 | Success(1) >= method(:error) # Failure(RuntimeError(error 1)) 203 | ``` 204 | 205 | ### Chaining with #in_sequence 206 | 207 | When creating long chains with e.g. `#>>`, it can get cumbersome carrying 208 | around the entire context required for every function within the chain. Also, 209 | every function within the chain requires some boilerplate code for extracting the 210 | relevant information from the context. 211 | 212 | Similarly to, for example, the `do` notation in Haskell and _sequence 213 | comprehensions_ or _for comprehensions_ in Scala, `#in_sequence` can be used to 214 | streamline the same process while keeping the code more readable. Using 215 | `#in_sequence` provides all the benefits of using the `Result` monad while 216 | still allowing to write code that reads very much like standard imperative 217 | Ruby. 218 | 219 | Here's an example: 220 | 221 | ```ruby 222 | class Foo 223 | include Deterministic::Prelude 224 | 225 | def call(input) 226 | in_sequence do 227 | get(:sanitized_input) { sanitize(input) } 228 | and_then { validate(sanitized_input) } 229 | get(:user) { get_user_from_db(sanitized_input) } 230 | let(:name) { user.fetch(:name) } 231 | observe { log('user name', name) } 232 | get(:request) { build_request(sanitized_input, user) } 233 | observe { log('sending request', request) } 234 | get(:response) { send_request(request) } 235 | observe { log('got response', response) } 236 | and_yield { format_response(response) } 237 | end 238 | end 239 | 240 | def sanitize(input) 241 | sanitized_input = input 242 | Success(sanitized_input) 243 | end 244 | 245 | def validate(sanitized_input) 246 | Success(sanitized_input) 247 | end 248 | 249 | def get_user_from_db(sanitized_input) 250 | Success(type: :admin, id: sanitized_input.fetch(:id), name: 'John') 251 | end 252 | 253 | def build_request(sanitized_input, user) 254 | Success(input: sanitized_input, user: user) 255 | end 256 | 257 | def log(message, data) 258 | # logger.info(message, data) 259 | end 260 | 261 | def send_request(request) 262 | Success(status: 200) 263 | end 264 | 265 | def format_response(response) 266 | Success(response: response, message: 'it worked') 267 | end 268 | end 269 | 270 | Foo.new.call(id: 1) 271 | ``` 272 | 273 | Notice how the functions don't necessarily have to accept only a single 274 | argument (`build_request` accepts 2). Also notice how the methods can be used 275 | directly, without having to call `#method` or having them return procs. 276 | 277 | The chain will still be short-circuited when e.g. `#validate` returns a 278 | `Failure`. 279 | 280 | Here's what the operators used in this example mean: 281 | * `get` - Execute the provided block and expect a `Result` as its return value. 282 | If the `Result` is a `Success`, then the `Success` value is assigned to the 283 | specified identifier. The value is then accessible in subsequent blocks by 284 | that identifier. If the `Result` is a `Failure`, then the entire chain will 285 | be short-circuited and the `Failure` will be returned as the result of the 286 | `in_sequence` call. 287 | * `let` - Execute the provided block and assign its result to the specified 288 | identifier. The result can be anything - it is *not* expected to be 289 | a `Result`. This is useful for simple assignments that don't need to be 290 | wrapped in a `Result`. E.g. `let(:four) { 2 + 2 }`. 291 | * `and_then` - Execute the provided block and expect a `Result` as its return 292 | value. If the `Result` is a `Success`, then the chain continues, otherwise 293 | the chain is short-circuited and the `Failure` will be returned as the result 294 | of the `in_sequence` call. 295 | * `observe` - Execute the provided block whose return value will be ignored. 296 | The chain continues regardless. 297 | * `and_yield` - Execute the provided block and expect a `Result` as its return 298 | value. The `Result` will be returned as the result of the `in_sequence` call. 299 | 300 | ### Pattern matching 301 | Now that you have some result, you want to control flow by providing patterns. 302 | `#match` can match by 303 | 304 | * success, failure, result or any 305 | * values 306 | * lambdas 307 | * classes 308 | 309 | ```ruby 310 | Success(1).match do 311 | Success() { |s| "success #{s}"} 312 | Failure() { |f| "failure #{f}"} 313 | end # => "success 1" 314 | ``` 315 | Note1: the variant's inner value(s) have been unwrapped, and passed to the block. 316 | 317 | Note2: only the __first__ matching pattern block will be executed, so order __can__ be important. 318 | 319 | Note3: you can omit block parameters if you don't use them, or you can use `_` to signify that you don't care about their values. If you specify parameters, their number must match the number of values in the variant. 320 | 321 | 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. 322 | 323 | Guards 324 | 325 | ```ruby 326 | Success(1).match do 327 | Success(where { s == 1 }) { |s| "Success #{s}" } 328 | end # => "Success 1" 329 | ``` 330 | 331 | Note1: the guard has access to variable names defined by the block arguments. 332 | 333 | Note2: the guard is not evaluated using the enclosing context's `self`; if you need to call methods on the enclosing scope, you must specify a receiver. 334 | 335 | Also you can match the result class 336 | 337 | ```ruby 338 | Success([1, 2, 3]).match do 339 | Success(where { s.is_a?(Array) }) { |s| s.first } 340 | end # => 1 341 | ``` 342 | 343 | If no match was found a `NoMatchError` is raised, so make sure you always cover all possible outcomes. 344 | 345 | ```ruby 346 | Success(1).match do 347 | Failure() { |f| "you'll never get me" } 348 | end # => NoMatchError 349 | ``` 350 | 351 | Matches must be exhaustive, otherwise an error will be raised, showing the variants which have not been covered. 352 | 353 | ## core_ext 354 | You can use a core extension, to include Result in your own class or in Object, i.e. in all classes. 355 | 356 | ```ruby 357 | require 'deterministic/core_ext/object/result' 358 | 359 | [1].success? # => false 360 | Success(1).failure? # => false 361 | Success(1).success? # => true 362 | Failure(1).result? # => true 363 | ``` 364 | 365 | ## Option 366 | 367 | ```ruby 368 | Some(1).some? # #=> true 369 | Some(1).none? # #=> false 370 | None.some? # #=> false 371 | None.none? # #=> true 372 | ``` 373 | 374 | Maps an `Option` with the value `a` to the same `Option` with the value `b`. 375 | 376 | ```ruby 377 | Some(1).fmap { |n| n + 1 } # => Some(2) 378 | None.fmap { |n| n + 1 } # => None 379 | ``` 380 | 381 | Maps a `Result` with the value `a` to another `Result` with the value `b`. 382 | 383 | ```ruby 384 | Some(1).map { |n| Some(n + 1) } # => Some(2) 385 | Some(1).map { |n| None } # => None 386 | None.map { |n| Some(n + 1) } # => None 387 | ``` 388 | 389 | Get the inner value or provide a default for a `None`. Calling `#value` on a `None` will raise a `NoMethodError` 390 | 391 | ```ruby 392 | Some(1).value # => 1 393 | Some(1).value_or(2) # => 1 394 | None.value # => NoMethodError 395 | None.value_or(0) # => 0 396 | ``` 397 | 398 | Add the inner values of option using `+`. 399 | 400 | ```ruby 401 | Some(1) + Some(1) # => Some(2) 402 | Some([1]) + Some(1) # => TypeError: No implicit conversion 403 | None + Some(1) # => Some(1) 404 | Some(1) + None # => Some(1) 405 | Some([1]) + None + Some([2]) # => Some([1, 2]) 406 | ``` 407 | 408 | ### Coercion 409 | ```ruby 410 | Option.any?(nil) # => None 411 | Option.any?([]) # => None 412 | Option.any?({}) # => None 413 | Option.any?(1) # => Some(1) 414 | 415 | Option.some?(nil) # => None 416 | Option.some?([]) # => Some([]) 417 | Option.some?({}) # => Some({}) 418 | Option.some?(1) # => Some(1) 419 | 420 | Option.try! { 1 } # => Some(1) 421 | Option.try! { raise "error"} # => None 422 | ``` 423 | 424 | ### Pattern Matching 425 | ```ruby 426 | Some(1).match { 427 | Some(where { s == 1 }) { |s| s + 1 } 428 | Some() { |s| 1 } 429 | None() { 0 } 430 | } # => 2 431 | ``` 432 | 433 | ## Enums 434 | All the above are implemented using enums, see their definition, for more details. 435 | 436 | Define it, with all variants: 437 | 438 | ```ruby 439 | Threenum = Deterministic::enum { 440 | Nullary() 441 | Unary(:a) 442 | Binary(:a, :b) 443 | } 444 | 445 | Threenum.variants # => [:Nullary, :Unary, :Binary] 446 | ``` 447 | 448 | Initialize 449 | 450 | ```ruby 451 | n = Threenum.Nullary # => Threenum::Nullary.new() 452 | n.value # => Error 453 | 454 | u = Threenum.Unary(1) # => Threenum::Unary.new(1) 455 | u.value # => 1 456 | 457 | b = Threenum::Binary(2, 3) # => Threenum::Binary(2, 3) 458 | b.value # => { a:2, b: 3 } 459 | ``` 460 | 461 | Pattern matching 462 | 463 | ```ruby 464 | Threenum::Unary(5).match { 465 | Nullary() { 0 } 466 | Unary() { |u| u } 467 | Binary() { |a, b| a + b } 468 | } # => 5 469 | 470 | # or 471 | t = Threenum::Unary(5) 472 | Threenum.match(t) { 473 | Nullary() { 0 } 474 | Unary() { |u| u } 475 | Binary() { |a, b| a + b } 476 | } # => 5 477 | ``` 478 | 479 | If you want to return the whole matched object, you'll need to pass a reference to the object (second case). Note that `self` refers to the scope enclosing the `match` call. 480 | 481 | ```ruby 482 | def drop(n) 483 | match { 484 | Cons(where { n > 0 }) { |h, t| t.drop(n - 1) } 485 | Cons() { |_, _| self } 486 | Nil() { raise EmptyListError } 487 | } 488 | end 489 | ``` 490 | 491 | See the linked list implementation in the specs for more examples 492 | 493 | With guard clauses 494 | 495 | ```ruby 496 | Threenum::Unary(5).match { 497 | Nullary() { 0 } 498 | Unary() { |u| u } 499 | Binary(where { a.is_a?(Fixnum) && b.is_a?(Fixnum) }) { |a, b| a + b } 500 | Binary() { |a, b| raise "Expected a, b to be numbers" } 501 | } # => 5 502 | ``` 503 | 504 | Implementing methods for enums 505 | 506 | ```ruby 507 | Deterministic::impl(Threenum) { 508 | def sum 509 | match { 510 | Nullary() { 0 } 511 | Unary() { |u| u } 512 | Binary() { |a, b| a + b } 513 | } 514 | end 515 | 516 | def +(other) 517 | match { 518 | Nullary() { other.sum } 519 | Unary() { |a| self.sum + other.sum } 520 | Binary() { |a, b| self.sum + other.sum } 521 | } 522 | end 523 | } 524 | 525 | Threenum.Nullary + Threenum.Unary(1) # => Unary(1) 526 | ``` 527 | 528 | All matches must be exhaustive, i.e. cover all variants 529 | 530 | ## Maybe 531 | The simplest NullObject wrapper there can be. It adds `#some?` and `#null?` to `Object` though. 532 | 533 | ```ruby 534 | require 'deterministic/maybe' # you need to do this explicitly 535 | Maybe(nil).foo # => Null 536 | Maybe(nil).foo.bar # => Null 537 | Maybe({a: 1})[:a] # => 1 538 | 539 | Maybe(nil).null? # => true 540 | Maybe({}).null? # => false 541 | 542 | Maybe(nil).some? # => false 543 | Maybe({}).some? # => true 544 | ``` 545 | 546 | ## Mimic 547 | 548 | If you want a custom NullObject which mimicks another class. 549 | 550 | ```ruby 551 | class Mimick 552 | def test; end 553 | end 554 | 555 | naught = Maybe.mimick(Mimick) 556 | naught.test # => Null 557 | naught.foo # => NoMethodError 558 | ``` 559 | 560 | ## Inspirations 561 | * My [Monadic gem](http://github.com/pzol/monadic) of course 562 | * `#attempt_all` was somewhat inspired by [An error monad in Clojure](http://brehaut.net/blog/2011/error_monads) (attempt all has now been removed) 563 | * [Pithyless' rumblings](https://gist.github.com/pithyless/2216519) 564 | * [either by rsslldnphy](https://github.com/rsslldnphy/either) 565 | * [Functors, Applicatives, And Monads In Pictures](http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html) 566 | * [Naught by avdi](https://github.com/avdi/naught/) 567 | * [Rust's Result](http://static.rust-lang.org/doc/master/std/result/enum.Result.html) 568 | 569 | ## Installation 570 | 571 | Add this line to your application's Gemfile: 572 | 573 | gem 'deterministic' 574 | 575 | And then execute: 576 | 577 | $ bundle 578 | 579 | Or install it yourself as: 580 | 581 | $ gem install deterministic 582 | 583 | ## Contributing 584 | 585 | 1. Fork it 586 | 2. Create your feature branch (`git checkout -b my-new-feature`) 587 | 3. Commit your changes (`git commit -am 'Add some feature'`) 588 | 4. Push to the branch (`git push origin my-new-feature`) 589 | 5. Create new Pull Request 590 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /deterministic.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'deterministic/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "deterministic" 8 | spec.version = Deterministic::VERSION 9 | spec.authors = ["Piotr Zolnierek"] 10 | spec.email = ["pz@anixe.pl"] 11 | spec.description = %q{A gem providing failsafe flow} 12 | spec.summary = %q{see above} 13 | spec.homepage = "http://github.com/pzol/deterministic" 14 | spec.license = "MIT" 15 | spec.required_ruby_version = '>=2.7' 16 | 17 | spec.files = `git ls-files`.split($/) 18 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 19 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 20 | spec.require_paths = ["lib"] 21 | 22 | spec.add_development_dependency "bundler", '~> 2.0' 23 | spec.add_development_dependency "rake" 24 | spec.add_development_dependency "rspec", ">= 3" 25 | spec.add_development_dependency "guard" 26 | spec.add_development_dependency "guard-bundler" 27 | spec.add_development_dependency "guard-rspec" 28 | spec.add_development_dependency "simplecov" 29 | end 30 | -------------------------------------------------------------------------------- /lib/deterministic.rb: -------------------------------------------------------------------------------- 1 | require "deterministic/version" 2 | 3 | module Deterministic; end 4 | 5 | require 'deterministic/monad' 6 | require 'deterministic/match' 7 | require 'deterministic/enum' 8 | require 'deterministic/result' 9 | require 'deterministic/option' 10 | require 'deterministic/either' 11 | require 'deterministic/null' 12 | require 'deterministic/sequencer' 13 | -------------------------------------------------------------------------------- /lib/deterministic/core_ext/object/result.rb: -------------------------------------------------------------------------------- 1 | require "deterministic/core_ext/result" 2 | 3 | include Deterministic 4 | class Object 5 | include Deterministic::CoreExt::Result 6 | end 7 | -------------------------------------------------------------------------------- /lib/deterministic/core_ext/result.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | module CoreExt 3 | module Result 4 | def success? 5 | self.is_a? Deterministic::Result::Success 6 | end 7 | 8 | def failure? 9 | self.is_a? Deterministic::Result::Failure 10 | end 11 | 12 | def result? 13 | success? || failure? 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/deterministic/either.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | class Either 3 | include Monad 4 | class << self 5 | public :new 6 | end 7 | 8 | def initialize(left=[], right=[]) 9 | @left, @right = left, right 10 | end 11 | 12 | attr_reader :left, :right 13 | 14 | def +(other) 15 | raise Deterministic::Monad::NotMonadError, "Expected an Either, got #{other.class}" unless other.is_a? Either 16 | 17 | Either.new(left + other.left, right + other.right) 18 | end 19 | 20 | undef :value 21 | 22 | def inspect 23 | "Either(left: #{left.inspect}, right: #{right.inspect})" 24 | end 25 | 26 | end 27 | module_function 28 | def Left(value) 29 | Either.new(Array[value], []) 30 | end 31 | 32 | def Right(value) 33 | Either.new([], Array[value]) 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/deterministic/enum.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | module Enum 3 | class MatchError < StandardError; end 4 | end 5 | 6 | class EnumBuilder 7 | def initialize(parent) 8 | @parent = parent 9 | end 10 | 11 | class DataType 12 | module AnyEnum 13 | include Deterministic::Monad 14 | 15 | def match(&block) 16 | parent.match(self, &block) 17 | end 18 | 19 | def to_s 20 | value.to_s 21 | end 22 | 23 | def name 24 | self.class.name.split("::")[-1] 25 | end 26 | 27 | # Returns array. Will fail on Nullary objects. 28 | # TODO: define a Unary module so we can define this method differently on Unary vs Binary 29 | def wrapped_values 30 | if self.is_a?(Deterministic::EnumBuilder::DataType::Binary) 31 | value.values 32 | else 33 | [value] 34 | end 35 | end 36 | end 37 | 38 | module Nullary 39 | def initialize(*args) 40 | @value = nil 41 | end 42 | 43 | def inspect 44 | name 45 | end 46 | end 47 | 48 | # TODO: this should probably be named Multary 49 | module Binary 50 | def initialize(*init) 51 | raise ArgumentError, "Expected arguments for #{args}, got #{init}" unless (init.count == 1 && init[0].is_a?(Hash)) || init.count == args.count 52 | if init.count == 1 && init[0].is_a?(Hash) 53 | @value = Hash[args.zip(init[0].values)] 54 | else 55 | @value = Hash[args.zip(init)] 56 | end 57 | end 58 | 59 | def inspect 60 | params = value.map { |k, v| "#{k}: #{v.inspect}" } 61 | "#{name}(#{params.join(', ')})" 62 | end 63 | end 64 | 65 | def self.create(parent, name, args) 66 | raise ArgumentError, "#{args} may not contain the reserved name :value" if args.include? :value 67 | dt = Class.new(parent) 68 | 69 | dt.instance_eval { 70 | class << self; public :new; end 71 | include AnyEnum 72 | define_method(:args) { args } 73 | 74 | define_method(:parent) { parent } 75 | private :parent 76 | } 77 | 78 | if args.count == 0 79 | dt.instance_eval { 80 | include Nullary 81 | private :value 82 | } 83 | elsif args.count == 1 84 | dt.instance_eval { 85 | define_method(args[0].to_sym) { value } 86 | } 87 | else 88 | dt.instance_eval { 89 | include Binary 90 | 91 | args.each do |m| 92 | define_method(m) do 93 | @value[m] 94 | end 95 | end 96 | } 97 | end 98 | dt 99 | end 100 | 101 | class << self 102 | public :new; 103 | end 104 | end 105 | 106 | def method_missing(m, *args) 107 | @parent.const_set(m, DataType.create(@parent, m, args)) 108 | end 109 | end 110 | 111 | module_function 112 | def enum(&block) 113 | mod = Class.new do # the enum to be built 114 | class << self; private :new; end 115 | 116 | def self.match(obj, &block) 117 | caller_ctx = block.binding.eval 'self' 118 | 119 | matcher = self::Matcher.new(obj) 120 | matcher.instance_eval(&block) 121 | 122 | variants_in_match = matcher.matches.collect {|e| e[1].name.split('::')[-1].to_sym}.uniq.sort 123 | variants_not_covered = variants - variants_in_match 124 | raise Enum::MatchError, "Match is non-exhaustive, #{variants_not_covered} not covered" unless variants_not_covered.empty? 125 | 126 | type_matches = matcher.matches.select { |r| r[0].is_a?(r[1]) } 127 | 128 | type_matches.each { |match| 129 | obj, type, block, args, guard = match 130 | 131 | if args.count == 0 132 | return caller_ctx.instance_eval(&block) 133 | else 134 | if args.count != obj.args.count 135 | raise Enum::MatchError, "Pattern (#{args.join(', ')}) must match (#{obj.args.join(', ')})" 136 | end 137 | guard_ctx = guard_context(obj, args) 138 | 139 | if guard 140 | if guard_ctx.instance_exec(obj, &guard) 141 | return caller_ctx.instance_exec(* obj.wrapped_values, &block) 142 | end 143 | else 144 | return caller_ctx.instance_exec(* obj.wrapped_values, &block) 145 | end 146 | end 147 | } 148 | 149 | raise Enum::MatchError, "No match could be made" 150 | end 151 | 152 | def self.variants; constants - [:Matcher, :MatchError]; end 153 | 154 | private 155 | def self.guard_context(obj, args) 156 | if obj.is_a?(Deterministic::EnumBuilder::DataType::Binary) 157 | Struct.new(*(args)).new(*(obj.value.values)) 158 | else 159 | Struct.new(*(args)).new(obj.value) 160 | end 161 | end 162 | end 163 | enum = EnumBuilder.new(mod) 164 | enum.instance_eval(&block) 165 | 166 | type_variants = mod.constants 167 | 168 | matcher = Class.new { 169 | def initialize(obj) 170 | @obj = obj 171 | @matches = [] 172 | @vars = [] 173 | end 174 | 175 | attr_reader :matches, :vars 176 | 177 | def where(&guard) 178 | guard 179 | end 180 | 181 | type_variants.each { |m| 182 | define_method(m) { |guard = nil, &block| 183 | raise ArgumentError, "No block given to `#{m}`" if block.nil? 184 | params_spec = block.parameters 185 | if params_spec.any? {|spec| spec.size < 2 } 186 | raise ArgumentError, "Unnamed param found in block parameters: #{params_spec.inspect}" 187 | end 188 | if params_spec.any? {|spec| spec[0] != :req && spec[0] != :opt } 189 | raise ArgumentError, "Only :req & :opt params allowed; parameters=#{params_spec.inspect}" 190 | end 191 | args = params_spec.map {|spec| spec[1] } 192 | 193 | type = mod.const_get(m) 194 | 195 | if guard && !guard.is_a?(Proc) 196 | guard = nil 197 | end 198 | 199 | @matches << [@obj, type, block, args, guard] 200 | } 201 | } 202 | } 203 | 204 | mod.const_set(:Matcher, matcher) 205 | 206 | type_variants.each { |variant| 207 | mod.singleton_class.class_exec { 208 | define_method(variant) { |*args| 209 | const_get(variant).new(*args) 210 | } 211 | } 212 | } 213 | mod 214 | end 215 | 216 | def impl(enum_type, &block) 217 | enum_type.variants.each { |v| 218 | name = "#{enum_type.name}::#{v.to_s}" 219 | type = Kernel.eval(name) 220 | type.class_eval(&block) 221 | } 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /lib/deterministic/match.rb: -------------------------------------------------------------------------------- 1 | # TODO: remove dead code 2 | module Deterministic 3 | module PatternMatching 4 | 5 | def match(context=nil, &block) 6 | context ||= block.binding.eval('self') # the instance containing the match block 7 | match = binding.eval('self.class::Match.new(self, context)') # the class defining the Match 8 | match.instance_eval &block 9 | match.call 10 | end 11 | 12 | class NoMatchError < StandardError; end 13 | 14 | module Match 15 | def initialize(container, context) 16 | @container = container 17 | @context = context 18 | @collection = [] 19 | end 20 | 21 | def call 22 | value = @container.respond_to?(:value) ? @container.value : nil 23 | matcher = @collection.detect { |m| m.matches?(value) } 24 | raise NoMatchError, "No match could be made for #{@container.inspect}" if matcher.nil? 25 | @context.instance_exec(value, &matcher.block) 26 | end 27 | 28 | # catch-all 29 | def any(value=nil, &result_block) 30 | push(Object, value, result_block) 31 | end 32 | 33 | private 34 | Matcher = Struct.new(:condition, :block) do 35 | def matches?(value) 36 | condition.call(value) 37 | end 38 | end 39 | 40 | def push(type, condition, result_block) 41 | condition_pred = case 42 | when condition.nil?; ->(v) { true } 43 | when condition.is_a?(Proc); condition 44 | when condition.is_a?(Class); ->(v) { condition === @container.value } 45 | else ->(v) { @container.value == condition } 46 | end 47 | 48 | matcher_pred = compose_predicates(type_pred[type], condition_pred) 49 | @collection << Matcher.new(matcher_pred, result_block) 50 | end 51 | 52 | def compose_predicates(f, g) 53 | ->(*args) { f[*args] && g[*args] } 54 | end 55 | 56 | # return a partial function for matching a matcher's type 57 | def type_pred 58 | (->(type, x) { @container.is_a? type }).curry 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/deterministic/maybe.rb: -------------------------------------------------------------------------------- 1 | class Object 2 | def null? 3 | false 4 | end 5 | 6 | def some? 7 | true 8 | end 9 | end 10 | 11 | def Maybe(obj) 12 | obj.nil? ? Null.instance : obj 13 | end 14 | -------------------------------------------------------------------------------- /lib/deterministic/monad.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | module Monad 3 | class NotMonadError < StandardError; end 4 | 5 | # Basicly the `pure` function 6 | def initialize(init) 7 | @value = join(init) 8 | end 9 | 10 | # If the passed value is monad already, get the value to avoid nesting 11 | # M[M[A]] is equivalent to M[A] 12 | def join(other) 13 | if other.is_a? self.class then other.value 14 | else other end 15 | end 16 | 17 | # The functor: takes a function (a -> b) and applies it to the inner value of the monad (Ma), 18 | # boxes it back to the same monad (Mb) 19 | # fmap :: (a -> b) -> M a -> M b 20 | def fmap(proc=nil, &block) 21 | result = (proc || block).call(value) 22 | self.class.new(result) 23 | end 24 | 25 | # The monad: takes a function which returns a monad (of the same type), applies the function 26 | # bind :: (a -> Mb) -> M a -> M b 27 | # the self.class, i.e. the containing monad is passed as a second (optional) arg to the function 28 | def bind(proc=nil, &block) 29 | (proc || block).call(value).tap do |result| 30 | 31 | # def parent_name(obj) 32 | # parts = obj.class.name.split('::') 33 | # if parts.count > 1 34 | # parts[0..-2].join('::') 35 | # else 36 | # parts[0] 37 | # end 38 | # end 39 | 40 | # self_parent = parent_name(self) 41 | # other_parent = parent_name(result) 42 | # raise NotMonadError, "Expected #{result.inspect} to be an #{other_parent}" unless self_parent == other_parent 43 | 44 | parent = self.class.superclass === Object ? self.class : self.class.superclass 45 | raise NotMonadError, "Expected #{result.inspect} to be an #{parent}" unless result.is_a? parent 46 | end 47 | end 48 | alias :'>>=' :bind 49 | 50 | # Get the underlying value, return in Haskell 51 | # return :: M a -> a 52 | def value 53 | @value 54 | end 55 | 56 | def to_s 57 | value.to_s 58 | end 59 | 60 | def inspect 61 | name = self.class.name.split("::")[-1] 62 | "#{name}(#{value})" 63 | end 64 | 65 | # Two monads are equivalent if they are of the same type and when their values are equal 66 | def ==(other) 67 | return false unless other.is_a? self.class 68 | @value == other.instance_variable_get(:@value) 69 | end 70 | 71 | # Return the string representation of the Monad 72 | def inspect 73 | pretty_class_name = self.class.name.split('::')[-1] 74 | "#{pretty_class_name}(#{self.value.inspect})" 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/deterministic/null.rb: -------------------------------------------------------------------------------- 1 | # The simplest NullObject there can be 2 | class Null 3 | class << self 4 | def method_missing(m, *args) 5 | if m == :new 6 | super 7 | else 8 | Null.instance 9 | end 10 | end 11 | 12 | def instance 13 | @instance ||= new([]) 14 | end 15 | 16 | def null? 17 | true 18 | end 19 | 20 | def some? 21 | false 22 | end 23 | 24 | def mimic(klas) 25 | new(klas.instance_methods(false)) 26 | end 27 | 28 | def ==(other) 29 | other.respond_to?(:null?) && other.null? 30 | end 31 | end 32 | private_class_method :new 33 | 34 | def initialize(methods) 35 | @methods = methods 36 | end 37 | 38 | # implicit conversions 39 | def to_str 40 | '' 41 | end 42 | 43 | def to_ary 44 | [] 45 | end 46 | 47 | def method_missing(m, *args) 48 | return self if respond_to?(m) 49 | super 50 | end 51 | 52 | def null? 53 | true 54 | end 55 | 56 | def some? 57 | false 58 | end 59 | 60 | def respond_to?(m, *args) 61 | return true if @methods.empty? || @methods.include?(m) 62 | super 63 | end 64 | 65 | def inspect 66 | 'Null' 67 | end 68 | 69 | def ==(other) 70 | other.respond_to?(:null?) && other.null? 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/deterministic/option.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | Option = Deterministic::enum { 3 | Some(:s) 4 | None() 5 | } 6 | 7 | class Option 8 | class << self 9 | def some?(expr) 10 | to_option(expr) { expr.nil? } 11 | end 12 | 13 | def any?(expr) 14 | to_option(expr) { expr.nil? || (expr.respond_to?(:empty?) && expr.empty?) } 15 | end 16 | 17 | def to_option(expr, &predicate) 18 | predicate.call(expr) ? None.new : Some.new(expr) 19 | end 20 | 21 | def try! 22 | yield rescue None.new 23 | end 24 | end 25 | end 26 | 27 | impl(Option) { 28 | class NoneValueError < StandardError; end 29 | 30 | def fmap(&fn) 31 | match { 32 | Some() {|s| self.class.new(fn.(s)) } 33 | None() { self } 34 | } 35 | end 36 | 37 | def map(&fn) 38 | match { 39 | Some() {|s| self.bind(&fn) } 40 | None() { self } 41 | } 42 | end 43 | 44 | def some? 45 | is_a? Option::Some 46 | end 47 | 48 | def none? 49 | is_a? Option::None 50 | end 51 | 52 | alias :empty? :none? 53 | 54 | def value_or(n) 55 | match { 56 | Some() {|s| s } 57 | None() { n } 58 | } 59 | end 60 | 61 | def value_to_a 62 | @value 63 | end 64 | 65 | def +(other) 66 | match { 67 | None() { other } 68 | Some(where { !other.is_a?(Option) }) {|_| raise TypeError, "Other must be an #{Option}"} 69 | Some(where { other.some? }) {|s| Option::Some.new(s + other.value) } 70 | Some() {|_| self } 71 | } 72 | end 73 | } 74 | 75 | module Prelude 76 | module Option 77 | None = Deterministic::Option::None.new 78 | def Some(s); Deterministic::Option::Some.new(s); end 79 | def None(); Deterministic::Prelude::Option::None; end 80 | end 81 | end 82 | end 83 | -------------------------------------------------------------------------------- /lib/deterministic/protocol.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | class ProtocolBuilder 3 | 4 | def initialize(typevar, block) 5 | @typevar, @block = typevar, block 6 | @protocol = Class.new 7 | end 8 | 9 | def build 10 | instance_exec(&@block) 11 | @protocol 12 | end 13 | 14 | def method_missing(m, *args) 15 | [m, args] 16 | end 17 | 18 | Signature = Struct.new(:name, :params, :return_type, :block) 19 | 20 | def fn(signature, &block) 21 | m = signature.to_a.flatten 22 | name = m[0] 23 | return_type = m[-1] 24 | params = Hash[(m[1..-2][0] || {}).map { |k, v| [k[0], v] }] 25 | 26 | @protocol.instance_eval { 27 | define_singleton_method(name) { 28 | Signature.new(name, params, return_type, block) 29 | } 30 | } 31 | 32 | @protocol.instance_eval { 33 | if block 34 | define_method(name) { |*args| 35 | block.call(args) 36 | } 37 | end 38 | } 39 | end 40 | end 41 | 42 | class InstanceBuilder 43 | def initialize(protocol, type, block) 44 | @protocol, @type, @block = protocol, type, block 45 | @instance = Class.new(@protocol::Protocol) 46 | end 47 | 48 | def build 49 | @instance.class_exec(&@block) 50 | protocol = @protocol::Protocol 51 | methods = protocol.methods(false) 52 | inst_type = @type 53 | 54 | @instance.instance_exec { 55 | methods.each { |name| 56 | if method_defined?(name) 57 | meth = instance_method(name) 58 | signature = protocol.send(name) 59 | params = signature.params 60 | expect_type = inst_type[signature.return_type] 61 | 62 | define_method(name) { |*args| 63 | args.each_with_index { |arg, i| 64 | name = params.keys[i] 65 | arg_type = params.fetch(name) 66 | expect_arg_type = inst_type.fetch(arg_type) 67 | 68 | raise TypeError, "Expected arg #{name} to be a #{expect_arg_type}, got #<#{arg.class}: #{arg.inspect}>" unless arg.is_a? expect_arg_type 69 | } 70 | 71 | result = meth.bind(self).call(*args) 72 | raise TypeError, "Expected #{name}(#{args.join(', ')}) to return a #{expect_type}, got #<#{result.class}: #{result.inspect}>" unless result.is_a? expect_type 73 | result 74 | } 75 | end 76 | } 77 | } 78 | 79 | missing = methods.detect { |m| !@instance.instance_methods(false).include?(m) } 80 | 81 | raise NotImplementedError, "`#{missing}` has no default implementation for #{@protocol} #{@type.to_s}" unless missing.nil? 82 | 83 | @instance 84 | end 85 | end 86 | 87 | module_function 88 | def protocol(typevar, &block) 89 | protocol = ProtocolBuilder.new(typevar, block).build 90 | p_module = block.binding.eval('self') 91 | p_module.const_set(:Protocol, protocol) 92 | end 93 | 94 | def instance(protocol, type, &block) 95 | InstanceBuilder.new(protocol, type, block).build 96 | end 97 | 98 | module Protocol 99 | def const_missing(c) 100 | c 101 | end 102 | end 103 | end 104 | -------------------------------------------------------------------------------- /lib/deterministic/result.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | Result = Deterministic::enum { 3 | Success(:s) 4 | Failure(:f) 5 | } 6 | 7 | class Result 8 | class << self 9 | def try! 10 | begin 11 | Success.new(yield) 12 | rescue => err 13 | Failure.new(err) 14 | end 15 | end 16 | end 17 | end 18 | 19 | Deterministic::impl(Result) { 20 | def map(proc=nil, &block) 21 | success? ? bind(proc || block) : self 22 | end 23 | 24 | alias :>> :map 25 | alias :and_then :map 26 | 27 | def map_err(proc=nil, &block) 28 | failure? ? bind(proc || block) : self 29 | end 30 | 31 | alias :or_else :map_err 32 | 33 | def pipe(proc=nil, &block) 34 | (proc || block).call(self) 35 | self 36 | end 37 | 38 | alias :<< :pipe 39 | 40 | def success? 41 | is_a? Result::Success 42 | end 43 | 44 | def failure? 45 | is_a? Result::Failure 46 | end 47 | 48 | def or(other) 49 | raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result 50 | match { 51 | Success() {|_| self } 52 | Failure() {|_| other } 53 | } 54 | end 55 | 56 | def and(other) 57 | raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result 58 | match { 59 | Success() {|_| other } 60 | Failure() {|_| self } 61 | } 62 | end 63 | 64 | def +(other) 65 | raise Deterministic::Monad::NotMonadError, "Expected #{other.inspect} to be a Result" unless other.is_a? Result 66 | match { 67 | Success(where { other.success? } ) {|s| Result::Success.new(s + other.value) } 68 | Failure(where { other.failure? } ) {|f| Result::Failure.new(f + other.value) } 69 | Success() {|_| other } # implied other.failure? 70 | Failure() {|_| self } # implied other.success? 71 | } 72 | end 73 | 74 | def try(proc=nil, &block) 75 | map(proc, &block) 76 | rescue => err 77 | Result::Failure.new(err) 78 | end 79 | 80 | alias :>= :try 81 | 82 | } 83 | end 84 | 85 | module Deterministic 86 | module Prelude 87 | module Result 88 | def try!(&block); Deterministic::Result.try!(&block); end 89 | def Success(s); Deterministic::Result::Success.new(s); end 90 | def Failure(f); Deterministic::Result::Failure.new(f); end 91 | end 92 | 93 | include Result 94 | end 95 | end 96 | -------------------------------------------------------------------------------- /lib/deterministic/sequencer.rb: -------------------------------------------------------------------------------- 1 | require 'delegate' 2 | 3 | module Deterministic 4 | module Sequencer 5 | InvalidSequenceError = Class.new(StandardError) 6 | 7 | module Operation 8 | Get = Struct.new(:block, :name) 9 | Let = Struct.new(:block, :name) 10 | AndThen = Struct.new(:block) 11 | Observe = Struct.new(:block) 12 | AndYield = Struct.new(:block) 13 | end 14 | 15 | def in_sequence(&block) 16 | sequencer = Sequencer.new(self) 17 | sequencer.instance_eval(&block) 18 | sequencer.yield 19 | end 20 | 21 | class Sequencer 22 | def initialize(instance) 23 | @operations = [] 24 | @operation_wrapper = OperationWrapper.new(instance) 25 | end 26 | 27 | def get(name, &block) 28 | raise ArgumentError, 'no block given'.freeze unless block_given? 29 | raise InvalidSequenceError, 'and_yield already called'.freeze if @sequenced_operations 30 | 31 | @operations << Operation::Get.new(block, name) 32 | end 33 | 34 | def let(name, &block) 35 | raise ArgumentError, 'no block given'.freeze unless block_given? 36 | raise InvalidSequenceError, 'and_yield already called'.freeze if @sequenced_operations 37 | 38 | @operations << Operation::Let.new(block, name) 39 | end 40 | 41 | def and_then(&block) 42 | raise ArgumentError, 'no block given'.freeze unless block_given? 43 | raise InvalidSequenceError, 'and_yield already called'.freeze if @sequenced_operations 44 | 45 | @operations << Operation::AndThen.new(block) 46 | end 47 | 48 | def observe(&block) 49 | raise ArgumentError, 'no block given'.freeze unless block_given? 50 | raise InvalidSequenceError, 'and_yield already called'.freeze if @sequenced_operations 51 | 52 | @operations << Operation::Observe.new(block) 53 | end 54 | 55 | def and_yield(&block) 56 | raise ArgumentError, 'no block given'.freeze unless block_given? 57 | raise InvalidSequenceError, 'and_yield already called'.freeze if @sequenced_operations 58 | 59 | @operations << Operation::AndYield.new(block) 60 | 61 | prepare_sequenced_operators 62 | end 63 | 64 | def yield 65 | raise InvalidSequenceError, 'and_yield not called'.freeze unless @sequenced_operations 66 | 67 | @operation_wrapper.instance_eval(&@sequenced_operations) 68 | end 69 | 70 | private 71 | 72 | def prepare_sequenced_operators 73 | operations = @operations 74 | 75 | @sequenced_operations = lambda do |_| 76 | operations.reduce(Result::Success(nil)) do |last_result, operation| 77 | last_result.map do 78 | case operation 79 | when Operation::Get 80 | result = instance_eval(&operation.block) 81 | result.map do |output| 82 | # This will be executed in the context of the OperationWrapper 83 | # and so the results will be stored within the 84 | # OperationWrapper. 85 | @gotten_results[operation.name] = output 86 | result 87 | end 88 | when Operation::Let 89 | @gotten_results[operation.name] = instance_eval(&operation.block) 90 | last_result 91 | when Operation::AndThen 92 | instance_eval(&operation.block) 93 | when Operation::Observe 94 | instance_eval(&operation.block) 95 | last_result 96 | when Operation::AndYield 97 | instance_eval(&operation.block) 98 | else 99 | "Uknown operation: #{operation.class}" 100 | end 101 | end 102 | end 103 | end 104 | end 105 | end 106 | 107 | # OperationWrapper proxies all method calls to the wrapped instance, but 108 | # first checks if the name of the called method matches a value stored 109 | # within @gotten_results and returns the value if it does. 110 | class OperationWrapper < SimpleDelegator 111 | def initialize(*args) 112 | super 113 | @gotten_results = {} 114 | end 115 | 116 | ruby2_keywords def method_missing(name, *args, &block) 117 | if @gotten_results.key?(name) 118 | @gotten_results[name] 119 | else 120 | super 121 | end 122 | end 123 | 124 | def respond_to_missing?(name, include_private = false) 125 | @gotten_results.key?(name) || super 126 | end 127 | end 128 | end 129 | 130 | module Prelude 131 | include Sequencer 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /lib/deterministic/version.rb: -------------------------------------------------------------------------------- 1 | module Deterministic 2 | VERSION = "0.16.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/examples/amount_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'deterministic/enum' 3 | 4 | Amount = Deterministic::enum { 5 | Due(:amount) 6 | Paid(:amount) 7 | Info(:amount) 8 | } 9 | 10 | class Amount 11 | def self.from_f(f) 12 | f >= 0 ? Amount::Due.new(f) : Amount::Paid.new(-1 * f) 13 | end 14 | end 15 | 16 | Deterministic::impl(Amount) { 17 | def to_s 18 | match { 19 | Due() {|a| "%0.2f" % [a] } 20 | Paid() {|a| "-%0.2f" % [a] } 21 | Info() {|a| "(%0.2f)" % [a] } 22 | } 23 | end 24 | 25 | def to_f 26 | match { 27 | Info() {|a| 0 } 28 | Due() {|a| a } 29 | Paid() {|a| -1 * a } 30 | } 31 | end 32 | 33 | def +(other) 34 | raise TypeError "Expected other to be an Amount, got #{other.class}" unless other.is_a? Amount 35 | 36 | Amount.from_f(to_f + other.to_f) 37 | end 38 | } 39 | 40 | describe Amount do 41 | def Due(a); Amount::Due.new(a); end 42 | def Paid(a); Amount::Paid.new(a); end 43 | def Info(a); Amount::Info.new(a); end 44 | 45 | it "due" do 46 | amount = Amount::Due.new(100.2) 47 | expect(amount.to_s).to eq "100.20" 48 | end 49 | 50 | it "paid" do 51 | amount = Amount::Paid.new(100.1) 52 | expect(amount.to_s).to eq "-100.10" 53 | end 54 | 55 | it "paid" do 56 | amount = Amount::Info.new(100.31) 57 | expect(amount.to_s).to eq "(100.31)" 58 | end 59 | 60 | it "+" do 61 | expect(Due(10) + Paid(20)).to eq Paid(10) 62 | expect(Due(10) + Paid(10)).to eq Due(0) 63 | expect(Due(10) + Due(10)).to eq Due(20) 64 | expect(Paid(10) + Paid(10)).to eq Paid(20) 65 | expect(Paid(10) + Due(1) + Info(99)).to eq Paid(9) 66 | end 67 | end 68 | -------------------------------------------------------------------------------- /spec/examples/config_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Deterministic 4 | 5 | class ElasticSearchConfig 6 | def initialize(env="development", proc_env=ENV) 7 | @env, @proc_env = env, proc_env 8 | end 9 | 10 | attr_reader :env 11 | 12 | def hosts 13 | Option.any?(proc_env["RESFINITY_LOG_CLIENT_ES_HOST"]).match { 14 | Some() {|s| { hosts: s.split(/, */) } } 15 | None() { default_hosts } # calls ElasticSearchConfig instance's method 16 | } 17 | end 18 | 19 | private 20 | attr_reader :proc_env 21 | def default_hosts 22 | case env 23 | when "production" 24 | { hosts: ["resfinity.net:9200"] } 25 | when "acceptance" || "development" 26 | { hosts: ["acc.resfinity.net:9200"] } 27 | else 28 | { hosts: ["localhost:9200"] } 29 | end 30 | end 31 | end 32 | 33 | describe ElasticSearchConfig do 34 | # NOTE: the "empty" cases also verify that the variant matchers use the enclosing context as self 35 | 36 | let(:cfg) { ElasticSearchConfig.new(environment, env) } 37 | context "test" do 38 | let(:environment) { "test" } 39 | context "env empty" do 40 | let(:env) { {} } 41 | specify { expect(cfg.hosts).to eq({ hosts: ["localhost:9200"] }) } 42 | end 43 | 44 | context "env empty" do 45 | let(:env) { { "RESFINITY_LOG_CLIENT_ES_HOST" => "" } } 46 | specify { expect(cfg.hosts).to eq({ hosts: ["localhost:9200"] }) } 47 | end 48 | 49 | context "env contains one" do 50 | let(:env) { { "RESFINITY_LOG_CLIENT_ES_HOST" => "foo:9999"} } 51 | specify { expect(cfg.hosts).to eq({ hosts: ["foo:9999"] }) } 52 | end 53 | 54 | context "env contains two" do 55 | let(:env) { { "RESFINITY_LOG_CLIENT_ES_HOST" => "foo:9999,bar:9200"} } 56 | specify { expect(cfg.hosts).to eq({ hosts: ["foo:9999", "bar:9200"] }) } 57 | end 58 | end 59 | 60 | context "production" do 61 | let(:environment) { "production" } 62 | context "env empty" do 63 | let(:env) { {} } 64 | specify { expect(cfg.hosts).to eq({ hosts: ["resfinity.net:9200"] }) } 65 | end 66 | end 67 | 68 | context "acceptance" do 69 | let(:environment) { "acceptance" } 70 | context "env empty" do 71 | let(:env) { {} } 72 | specify { expect(cfg.hosts).to eq({ hosts: ["acc.resfinity.net:9200"] }) } 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /spec/examples/controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Deterministic 4 | module Procify 5 | def py(m, *args) 6 | args.count > 0 ? method(m).to_proc.curry[*args] : method(m) 7 | end 8 | end 9 | end 10 | 11 | class BookingController 12 | include Deterministic::Prelude::Result 13 | include Deterministic::Procify 14 | 15 | Context = Struct.new(:booking, :ability, :format) 16 | 17 | def index(id, format=:html) 18 | get_booking(id) << log(:booking) >> 19 | py(:ability) << log(:ability) >> 20 | py(:present, format) << log(:presenter) >> 21 | py(:render) << log(:render) 22 | end 23 | 24 | def log(step) 25 | ->(data) { [step, data] } 26 | end 27 | 28 | def ability(ctx) 29 | ctx.ability = {} #Ability.new(@booking) 30 | Success(ctx) 31 | end 32 | 33 | def present(format, ctx) 34 | ctx.format = format 35 | 36 | Success(ctx) 37 | end 38 | 39 | def render(ctx) 40 | send(ctx.format, ctx) 41 | end 42 | 43 | def html(ctx) 44 | Success(ctx) 45 | end 46 | 47 | def get_booking(id) 48 | ctx = Context.new 49 | ctx.booking = {ref_anixe: id} 50 | Success(ctx) 51 | # @booking = @bms.booking_by_id(id) 52 | # rescue BSON::InvalidObjectId => ex 53 | # @booking = nil 54 | # @ui.error(404, ex.message) 55 | end 56 | end 57 | 58 | describe BookingController do 59 | it "does something" do 60 | bc = BookingController.new 61 | bc.index('1234', :html) 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/examples/list.rb: -------------------------------------------------------------------------------- 1 | require 'deterministic/enum' 2 | 3 | List = Deterministic::enum { 4 | Cons(:head, :tail) 5 | Nil() 6 | } 7 | 8 | class List 9 | def self.[](*ary) 10 | ary.reverse.inject(Nil.new) { |xs, x| xs.append(x) } 11 | end 12 | 13 | def self.empty 14 | @empty ||= Nil.new 15 | end 16 | end 17 | 18 | Deterministic::impl(List) { 19 | class EmptyListError < StandardError; end 20 | 21 | def append(elem) 22 | List::Cons.new(elem, self) 23 | end 24 | 25 | def null? 26 | is_a? Nil 27 | end 28 | 29 | def first 30 | match { 31 | Cons() {|_, __| self } 32 | Nil() { self } 33 | } 34 | end 35 | 36 | def last 37 | match { 38 | Cons(where { t.null? }) {|h, t| return self } 39 | Cons() {|_, t| t.last } 40 | Nil() { self } 41 | } 42 | end 43 | 44 | def head 45 | match { 46 | Cons() {|h, _| h } 47 | Nil() { self } 48 | } 49 | end 50 | 51 | def tail 52 | match { 53 | Cons() { |_, t| t } 54 | Nil() { raise EmptyListError } 55 | } 56 | end 57 | 58 | def init 59 | match { 60 | Cons(where { t.tail.null? } ) {|h, t| Cons.new(h, Nil.new) } 61 | Cons() {|h, t| Cons.new(h, t.init) } 62 | Nil() { raise EmptyListError } 63 | } 64 | end 65 | 66 | def filter(&pred) 67 | match { 68 | Cons(where { pred.(h) }) {|h, t| Cons.new(h, t.filter(&pred)) } 69 | Cons() {|_, t| t.filter(&pred) } 70 | Nil() { self } 71 | } 72 | end 73 | 74 | # The find function takes a predicate and a list and returns the first element in the list matching the predicate, 75 | # or None if there is no such element. 76 | def find(&pred) 77 | match { 78 | Nil() { Deterministic::Option::None.new } 79 | Cons() {|h, t| pred.(h) ? Deterministic::Option::Some.new(h) : t.find(&pred) } 80 | } 81 | end 82 | 83 | def length 84 | match { 85 | Cons() {|h, t| 1 + t.length } 86 | Nil() { 0 } 87 | } 88 | end 89 | 90 | def map(&fn) 91 | match { 92 | Cons() {|h, t| Cons.new(fn.(h), t.map(&fn)) } 93 | Nil() { self } 94 | } 95 | end 96 | 97 | def sum 98 | foldl(0, &:+) 99 | end 100 | 101 | def foldl(start, &fn) 102 | match { 103 | Nil() { start } 104 | # foldl f z (x:xs) = foldl f (f z x) xs 105 | Cons() {|h, t| t.foldl(fn.(start, h), &fn) } 106 | } 107 | end 108 | 109 | def foldl1(&fn) 110 | match { 111 | Nil() { raise EmptyListError } 112 | Cons() {|h, t| t.foldl(h, &fn)} 113 | } 114 | end 115 | 116 | def foldr(start, &fn) 117 | match { 118 | Nil() { start } 119 | # foldr f z (x:xs) = f x (foldr f z xs) 120 | Cons() {|h, t| fn.(h, t.foldr(start, &fn)) } 121 | } 122 | end 123 | 124 | def foldr1(&fn) 125 | match { 126 | Nil() { raise EmptyListError } 127 | Cons(where { t.null? }) {|h, t| h } 128 | # foldr1 f (x:xs) = f x (foldr1 f xs) 129 | Cons() {|h, t| fn.(h, t.foldr1(&fn)) } 130 | } 131 | end 132 | 133 | def take(n) 134 | match { 135 | Cons(where { n > 0 }) {|h, t| Cons.new(h, t.take(n - 1)) } 136 | Cons() {|_, __| Nil.new } 137 | Nil() { raise EmptyListError } 138 | } 139 | end 140 | 141 | def drop(n) 142 | match { 143 | Cons(where { n > 0 }) {|h, t| t.drop(n - 1) } 144 | Cons() {|_, __| self } 145 | Nil() { raise EmptyListError } 146 | } 147 | end 148 | 149 | def to_a 150 | foldr([]) { |x, ary| ary << x } 151 | end 152 | 153 | def any?(&pred) 154 | match { 155 | Nil() { false } 156 | Cons(where { t.null? }) {|h, t| pred.(h) } 157 | Cons() {|h, t| pred.(h) || t.any?(&pred) } 158 | } 159 | end 160 | 161 | def all?(&pred) 162 | match { 163 | Nil() { false } 164 | Cons(where { t.null? }) {|h, t| pred.(h) } 165 | Cons() {|h, t| pred.(h) && t.all?(&pred) } 166 | } 167 | end 168 | 169 | def reverse 170 | match { 171 | Nil() { self } 172 | Cons(where { t.null? }) {|_, t| self } 173 | Cons() {|h, t| Cons.new(self.last.head, self.init.reverse) } 174 | } 175 | end 176 | 177 | def to_s(joiner = ", ") 178 | match { 179 | Nil() { "Nil" } 180 | Cons() {|head, tail| head.to_s + joiner + tail.to_s } 181 | } 182 | end 183 | } 184 | -------------------------------------------------------------------------------- /spec/examples/list_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative 'list' 3 | 4 | describe List do 5 | Nil = List::Nil 6 | Cons = List::Cons 7 | 8 | subject(:list) { List[1] } 9 | 10 | specify { expect(list).to eq Cons.new(1, Nil.new) } 11 | 12 | context "match" do 13 | it "catches ignores guards with non-matching clauses" do 14 | expect( 15 | list.match { 16 | Nil() { list } 17 | Cons(where { h == 0 }) {|h,t| h } 18 | Cons() {|h, t| h } 19 | }).to eq 1 20 | end 21 | 22 | it "catches matching guards" do 23 | expect( # guard catched 24 | list.match { 25 | Nil() { raise "unreachable" } 26 | Cons(where { h == 1 }) {|h,t| h + 1 } 27 | Cons() {|h| h } 28 | }).to eq 2 29 | end 30 | 31 | it "raises an error when no match was made" do 32 | expect { 33 | list.match { 34 | Cons(where { true == false }) {|_, __| 1 } 35 | Nil(where { true == false }) { 0 } 36 | } 37 | }.to raise_error(Deterministic::Enum::MatchError) 38 | end 39 | 40 | it "raises an error when the match is not exhaustive" do 41 | expect { 42 | list.match { 43 | Cons() {|_, _| } 44 | } 45 | }.to raise_error(Deterministic::Enum::MatchError) 46 | end 47 | end 48 | 49 | it "empty" do 50 | expect(List.empty.object_id).to eq List.empty.object_id 51 | end 52 | 53 | it "from_a" do 54 | expect(List[21, 15, 9].to_s).to eq "21, 15, 9, Nil" 55 | end 56 | 57 | it "append" do 58 | expect(List[1].append(2)).to eq Cons.new(2, Cons.new(1, Nil.new)) 59 | end 60 | 61 | context "head" do 62 | specify { expect(list.head).to eq 1 } 63 | end 64 | 65 | context "tail" do 66 | subject(:list) { List[3, 9, 15, 21] } 67 | specify { expect(list.tail.to_s).to eq "9, 15, 21, Nil" } 68 | end 69 | 70 | context "take" do 71 | subject(:list) { List[3, 9, 15, 21] } 72 | specify { expect(list.take(2).to_s).to eq "3, 9, Nil" } 73 | end 74 | 75 | context "drop" do 76 | subject(:list) { List[3, 9, 15, 21] } 77 | specify { expect(list.drop(2).to_s).to eq "15, 21, Nil" } 78 | end 79 | 80 | context "null" do 81 | specify { expect(Nil.new).to be_null } 82 | specify { expect(Cons.new(1, Nil.new)).not_to be_null } 83 | end 84 | 85 | context "length" do 86 | subject(:list) { List[21, 15, 9, 3] } 87 | specify { expect(list.length).to eq 4 } 88 | specify { expect(Nil.new.length).to eq 0 } 89 | end 90 | 91 | context "filter" do 92 | subject(:list) { List[3, 9, 15, 21] } 93 | specify { expect(list.filter { |n| n < 10 }.to_s).to eq "3, 9, Nil" } 94 | specify { expect(Nil.new.filter { |n| n < 10 }).to eq Nil.new } 95 | end 96 | 97 | context "map" do 98 | subject(:list) { List[1, 2, 3, 4] } 99 | 100 | specify { expect(list.map { |h, t| h + 1 }).to eq Cons.new(2, Cons.new(3, Cons.new(4, Cons.new(5, Nil.new)))) } 101 | end 102 | 103 | context "first & last" do 104 | subject(:list) { List[9, 15, 21] } 105 | specify { expect(list.first.head).to eq 9 } 106 | specify { expect(list.last.head).to eq 21 } 107 | 108 | specify { expect(Nil.new.first).to eq Nil.new } 109 | specify { expect(Nil.new.last).to eq Nil.new } 110 | end 111 | 112 | context "init" do 113 | subject(:list) { List[9, 15, 21] } 114 | specify { expect(list.init.to_s).to eq "9, 15, Nil" } 115 | specify { expect { Nil.new.init}.to raise_error EmptyListError } 116 | end 117 | 118 | it "foldl :: [a] -> b -> (b -> a -> b) -> b" do 119 | list = List[21, 15, 9] 120 | expect(list.foldl(0) { |b, a| a + b }).to eq (((0 + 21) + 15) + 9) 121 | expect(list.foldl(0) { |b, a| b - a }).to eq (((0 - 21) - 15) - 9) 122 | expect(Nil.new.foldl(0, &:+)).to eq 0 123 | end 124 | 125 | it "foldl1 :: [a] -> b -> (b -> a -> b) -> b" do 126 | list = List[21, 15, 9] 127 | expect(list.foldl1 { |b, a| a + b }).to eq ((21 + 15) + 9) 128 | expect(list.foldl1 { |b, a| b - a }).to eq ((21 - 15) - 9) 129 | expect { Nil.new.foldl1(&:+) }.to raise_error EmptyListError 130 | end 131 | 132 | it "foldr :: [a] -> b -> (b -> a -> b) -> b" do 133 | list = List[21, 15, 9] 134 | expect(list.foldr(0) { |b, a| a + b }).to eq (21 + (15 + (9 + 0))) 135 | expect(list.foldr(0) { |b, a| b - a }).to eq (21 - (15 - (9 - 0))) 136 | expect(Nil.new.foldr(0, &:+)).to eq 0 137 | end 138 | 139 | it "foldr1 :: [a] -> b -> (b -> a -> b) -> b" do 140 | list = List[21, 15, 9, 3] 141 | expect(list.foldr1 { |b, a| a + b }).to eq (21 + (15 + (9 + 3))) 142 | expect(list.foldr1 { |b, a| b - a }).to eq (21 - (15 - (9 - 3))) 143 | expect { Nil.new.foldr1(&:+) }.to raise_error EmptyListError 144 | end 145 | 146 | it "find :: [a] -> (a -> Bool) -> Option a" do 147 | list = List[21, 15, 9] 148 | expect(list.find { |a| a == 15 }).to eq Deterministic::Option::Some.new(15) 149 | expect(list.find { |a| a == 1 }).to eq Deterministic::Option::None.new 150 | end 151 | 152 | context "reverse" do 153 | subject(:list) { List[9, 15, 21] } 154 | specify { expect(list.reverse.first.head).to eq 21 } 155 | specify { expect(list.to_s).to eq "9, 15, 21, Nil" } 156 | specify { expect(list.reverse.to_s).to eq "21, 15, 9, Nil" } 157 | end 158 | 159 | context "to_a" do 160 | subject(:list) { List[9, 15, 21] } 161 | specify { expect(list.to_a).to eq [21, 15, 9] } 162 | end 163 | 164 | context "all?" do 165 | subject(:list) { List[21, 15, 9] } 166 | specify { expect(list.all? { |n| n.is_a?(Integer) }).to be_truthy } 167 | end 168 | 169 | context "any?" do 170 | subject(:list) { List[21, 15, 9] } 171 | specify { expect(list.any? { |n| n == 11 }).to be_falsey } 172 | specify { expect(list.any? { |n| n == 15 }).to be_truthy } 173 | end 174 | 175 | it "inspect" do 176 | list = List[9, 15, 21] 177 | expect(list.inspect).to eq "Cons(head: 9, tail: Cons(head: 15, tail: Cons(head: 21, tail: Nil)))" 178 | expect(Nil.new.inspect).to eq "Nil" 179 | end 180 | 181 | it "to_s" do 182 | list = List[9, 15, 21] 183 | expect(list.to_s).to eq "9, 15, 21, Nil" 184 | expect(Nil.new.to_s).to eq "Nil" 185 | end 186 | end 187 | -------------------------------------------------------------------------------- /spec/examples/logger_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # TODO: dead code? 4 | class Logger 5 | alias :m :method 6 | 7 | def initialize(repository) 8 | @repository = repository 9 | end 10 | 11 | def log(item) 12 | validate(item) >> m(:transform) >= m(:save) 13 | end 14 | 15 | private 16 | attr_reader :repository 17 | 18 | # TODO: this is never called; the matcher syntax is old 19 | def validate(item) 20 | return Failure(["Item cannot be empty"]) if item.blank? 21 | return Failure(["Item must be a Hash"]) unless item.is_a?(Hash) 22 | 23 | validate_required_params(item).match { 24 | none { Success(item) } 25 | some { |errors| Failure(errors) } 26 | } 27 | end 28 | 29 | def transform(params) 30 | ttl = params.delete(:ttl) 31 | params.merge!(_ttl: ttl) unless ttl.nil? 32 | Success(params) 33 | end 34 | 35 | def save(item) 36 | Success(repository.bulk_insert([item])) 37 | end 38 | 39 | def validate_required_params(params) 40 | required_params = %w(date tenant contract user facility short data) 41 | Option.any?(required_params 42 | .select{|key| Option.some?(params[key.to_sym]).none? } 43 | .map{|key| "#{key} is required"} 44 | ) 45 | end 46 | end 47 | 48 | class Ensure 49 | include Deterministic 50 | include Deterministic::Monad 51 | 52 | None = Deterministic::Option::None.new 53 | def Some(value) 54 | Option::Some.new(value) 55 | end 56 | 57 | attr_accessor :value 58 | 59 | def method_missing(m, *args) 60 | validator_m = "#{m}!".to_sym 61 | super unless respond_to? validator_m 62 | send(validator_m, *args).map { |v| Some([Error.new(m, v)])} 63 | end 64 | 65 | class Error 66 | attr_accessor :name, :value 67 | def initialize(name, value) 68 | @name, @value = name, value 69 | end 70 | 71 | def inspect 72 | "#{@name}(#{@value.inspect})" 73 | end 74 | end 75 | 76 | def not_empty! 77 | value.nil? || value.empty? ? Some(value) : None 78 | end 79 | 80 | def is_a!(type) 81 | value.is_a?(type) ? None : Some({obj: value, actual: value.class, expected: type}) 82 | end 83 | 84 | def has_key!(key) 85 | value.has_key?(key) ? None : Some(key) 86 | end 87 | end 88 | 89 | class Validator < Ensure 90 | 91 | def date_is_one! 92 | value[:date] == 1 ? None : Some({actual: value[:date], expected: 1}) 93 | end 94 | 95 | def required_params! 96 | params = %w(date tenant contract user facility short data) 97 | params.inject(None) { |errors, param| 98 | errors + (value[:param].nil? || value[:param].empty? ? Some([param]) : None) 99 | } 100 | end 101 | 102 | def call 103 | not_empty + is_a(Array) + None + has_key(:tenant) + Some(["error"]) #+ date_is_one + required_params 104 | end 105 | 106 | end 107 | 108 | describe Ensure do 109 | Some = Deterministic::Option::Some 110 | 111 | it "Ensure" do 112 | params = {date: 2} 113 | 114 | v = Validator.new(params) 115 | 116 | errors = v.call 117 | expect(errors).to be_a Some 118 | expect(errors.value).not_to be_empty 119 | end 120 | end 121 | -------------------------------------------------------------------------------- /spec/examples/validate_address_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | # A Unit of Work for validating an address 5 | module ValidateAddress 6 | extend Deterministic::Prelude::Result 7 | 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 | include Deterministic 20 | subject { ValidateAddress.call(candidate) } 21 | context 'sunny day' do 22 | let(:candidate) { {title: "Hobbiton", street: "501 Buckland Rd", city: "Matamata", postal: "3472", country: "nz"} } 23 | specify { expect(subject).to be_a Deterministic::Result::Success } 24 | specify { expect(subject.value).to eq candidate } 25 | end 26 | 27 | context 'empty data' do 28 | let(:candidate) { {} } 29 | specify { expect(subject).to be_a Deterministic::Result::Failure } 30 | specify { expect(subject.value).to include(:street, :city, :postal) } 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/lib/deterministic/class_mixin_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'Class Mixin' do 4 | describe 'try' do 5 | module MyApp 6 | class Thing 7 | include Deterministic::Prelude::Result 8 | 9 | def run 10 | Success(11) >> method(:double) 11 | end 12 | 13 | def double(num) 14 | Success(num * 2) 15 | end 16 | end 17 | end 18 | 19 | it "cleanly mixes into a class" do 20 | result = MyApp::Thing.new.run 21 | expect(result).to eq Deterministic::Result::Success.new(22) 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/lib/deterministic/core_ext/object/either_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "deterministic/core_ext/object/result" 3 | 4 | describe Deterministic::CoreExt::Result, "object", isolate: true do 5 | it "does something" do 6 | h = {a: 1} 7 | expect(h.success?).to be_falsey 8 | expect(h.failure?).to be_falsey 9 | expect(h.result?).to be_falsey 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/lib/deterministic/core_ext/result_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "deterministic" 3 | require "deterministic/core_ext/result" 4 | 5 | describe Deterministic::CoreExt::Result do 6 | include Deterministic::Prelude::Result 7 | 8 | it "does something" do 9 | h = {} 10 | h.extend(Deterministic::CoreExt::Result) 11 | expect(h.success?).to be_falsey 12 | expect(h.failure?).to be_falsey 13 | expect(h.result?).to be_falsey 14 | end 15 | 16 | it "enables #success?, #failure?, #result? on all Objects" do 17 | ary = [Success(true), Success(1)] 18 | expect(ary.all?(&:success?)).to be_truthy 19 | 20 | ary = [Success(true), Failure(1)] 21 | expect(ary.all?(&:success?)).to be_falsey 22 | expect(ary.any?(&:failure?)).to be_truthy 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/lib/deterministic/currify_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module Deterministic 4 | module Currify 5 | module ClassMethods 6 | def currify(*names) 7 | names.each { |name| 8 | unbound_method = instance_method(name) 9 | 10 | define_method(name) { |*args| 11 | curried_method = unbound_method.bind(self).to_proc.curry 12 | curried_method[*args] 13 | } 14 | } 15 | end 16 | end 17 | 18 | def self.included(curried) 19 | curried.extend ClassMethods 20 | end 21 | 22 | end 23 | end 24 | 25 | class ::Proc 26 | def self.compose(f, g) 27 | lambda { |*args| f[g[*args]] } 28 | end 29 | 30 | # Compose left to right 31 | def |(g) 32 | Proc.compose(g, self) 33 | end 34 | 35 | # Compose right to left 36 | def *(g) 37 | Proc.compose(self, g) 38 | end 39 | end 40 | 41 | class Booking 42 | include Deterministic::Currify 43 | include Deterministic::Prelude::Result 44 | 45 | def initialize(deps) 46 | @deps = deps 47 | end 48 | 49 | def build(id, format) 50 | validate(id) | req | find | render(format) 51 | 52 | validate(id) | rq = request(id) | find() 53 | end 54 | 55 | def validate(id) 56 | Success(id) 57 | end 58 | 59 | def req(a, id) 60 | Success(id: id + a) 61 | end 62 | 63 | def find(req) 64 | Success({ found: req}) 65 | end 66 | 67 | def render(format, req) 68 | Success("rendered in #{format}: #{req[:found]}") 69 | end 70 | 71 | currify :find, :render, :req 72 | 73 | end 74 | 75 | describe "Pref" do 76 | include Deterministic::Prelude::Result 77 | 78 | it "does something" do 79 | b = Booking.new(1) 80 | actual = b.validate(1) >> b.req(2) >> b.find >> b.render(:html) 81 | 82 | expected = Deterministic::Result::Success.new("rendered in html: {:id=>3}") 83 | expect(actual).to eq expected 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/lib/deterministic/either_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Deterministic::Either do 4 | include Deterministic 5 | Either = Deterministic::Either 6 | 7 | it "+ does not change operands" do 8 | l = Left(1) 9 | r = Right(2) 10 | 11 | either = l + r 12 | expect(l).to eq Left(1) 13 | expect(r).to eq Right(2) 14 | expect(either).to eq Either.new([1], [2]) 15 | end 16 | 17 | it "allows adding multiple Eithers" do 18 | either = Left(1) + Left(2) + Right(:a) + Right(:b) 19 | expect(either.left).to eq [1, 2] 20 | expect(either.right).to eq [:a, :b] 21 | end 22 | 23 | it "works" do 24 | actual = [1, 2, 3, 4].inject(Either.new) { |acc, value| 25 | acc + (value % 2 == 0 ? Right(value) : Left(value)) 26 | } 27 | expect(actual).to eq Either.new([1, 3], [2, 4]) 28 | end 29 | 30 | end 31 | -------------------------------------------------------------------------------- /spec/lib/deterministic/maybe_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'deterministic/maybe' 3 | 4 | describe 'maybe' do 5 | it "does something" do 6 | expect(Maybe(nil).foo).to be_null 7 | expect(Maybe(nil).foo.bar.baz).to be_null 8 | expect(Maybe(nil).fetch(:a)).to be_null 9 | expect(Maybe(1)).to be_some 10 | expect(Maybe({a: 1}).fetch(:a)).to eq 1 11 | expect(Maybe({a: 1})[:a]).to eq 1 12 | expect(Maybe("a").upcase).to eq "A" 13 | expect(Maybe("a")).not_to be_null 14 | 15 | # expect(Maybe[[]]).to eq be_null 16 | end 17 | 18 | end 19 | -------------------------------------------------------------------------------- /spec/lib/deterministic/monad_axioms.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'a Monad' do 2 | describe 'axioms' do 3 | it '1st monadic law: left-identity' do 4 | f = ->(value) { monad.new(value + 1) } 5 | expect( 6 | monad.new(1).bind do |value| 7 | f.(value) 8 | end 9 | ).to eq f.(1) 10 | end 11 | 12 | it '2nd monadic law: right-identy - new and bind do not change the value' do 13 | expect( 14 | monad.new(1).bind do |value| 15 | monad.new(value) 16 | end 17 | ).to eq monad.new(1) 18 | end 19 | 20 | it '3rd monadic law: associativity' do 21 | f = ->(value) { monad.new(value + 1) } 22 | g = ->(value) { monad.new(value + 100) } 23 | 24 | id1 = monad.new(1).bind do |a| 25 | f.(a) 26 | end.bind do |b| 27 | g.(b) 28 | end 29 | 30 | id2 = monad.new(1).bind do |a| 31 | f.(a).bind do |b| 32 | g.(b) 33 | end 34 | end 35 | 36 | expect(id1).to eq id2 37 | end 38 | 39 | it '#bind must return a monad' do 40 | expect(monad.new(1).bind { |v| monad.new(v) }).to eq monad.new(1) 41 | expect { monad.new(1).bind {} }.to raise_error(Deterministic::Monad::NotMonadError) 42 | end 43 | 44 | it '#new must return a monad' do 45 | expect(monad.new(1)).to be_a monad 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/lib/deterministic/monad_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative 'monad_axioms' 3 | 4 | 5 | describe Deterministic::Monad do 6 | class Identity 7 | include Deterministic::Monad 8 | end 9 | 10 | let(:monad) { Identity } 11 | it_behaves_like 'a Monad' do 12 | # let(:monad) { monad } 13 | end 14 | 15 | specify { expect(Identity.new(1).inspect).to eq 'Identity(1)' } 16 | specify { expect(Identity.new(1).to_s).to eq '1' } 17 | specify { expect(Identity.new(nil).inspect).to eq 'Identity(nil)' } 18 | specify { expect(Identity.new(nil).to_s).to eq '' } 19 | specify { expect(Identity.new([1, 2]).fmap(&:to_s)).to eq Identity.new("[1, 2]") } 20 | specify { expect(Identity.new(1).fmap {|v| v + 2}).to eq Identity.new(3) } 21 | specify { expect(Identity.new('foo').fmap(&:upcase)).to eq Identity.new('FOO')} 22 | 23 | context '#bind' do 24 | it "raises an error if the passed function does not return a monad of the same class" do 25 | expect { Identity.new(1).bind {} }.to raise_error(Deterministic::Monad::NotMonadError) 26 | end 27 | specify { expect(Identity.new(1).bind {|value| Identity.new(value) }).to eq Identity.new(1) } 28 | 29 | it "passes the monad class, this is ruby-fu?!" do 30 | Identity.new(1) 31 | .bind do |_| 32 | expect(monad).to eq Identity 33 | monad.new(_) 34 | end 35 | end 36 | 37 | specify { expect( 38 | monad.new(1).bind { |value| monad.new(value + 1) } 39 | ).to eq Identity.new(2) 40 | } 41 | 42 | end 43 | specify { expect(Identity.new(Identity.new(1))).to eq Identity.new(1) } 44 | end 45 | -------------------------------------------------------------------------------- /spec/lib/deterministic/null_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "deterministic/null" 3 | 4 | describe Null do 5 | it "Null is a Singleton" do 6 | expect(Null.instance).to be_a Null 7 | expect { Null.new }.to raise_error(NoMethodError, "private method `new' called for Null:Class") 8 | end 9 | 10 | it "explicit conversions" do 11 | expect(Null.to_s).to eq 'Null' 12 | expect(Null.inspect).to eq 'Null' 13 | end 14 | 15 | it "compares to Null" do 16 | expect(Null === Null.instance).to be_truthy 17 | expect(Null.instance === Null).to be_truthy 18 | expect(Null.instance).to eq Null 19 | expect(Null).to eq Null.instance 20 | expect(1).not_to be Null 21 | expect(1).not_to be Null.instance 22 | expect(Null.instance).not_to be 1 23 | expect(Null).not_to be 1 24 | expect(Null.instance).not_to be_nil 25 | expect(Null).not_to be_nil 26 | end 27 | 28 | it "implicit conversions" do 29 | null = Null.instance 30 | expect(null.to_str).to eq "" 31 | expect(null.to_ary).to eq [] 32 | expect("" + null).to eq "" 33 | 34 | a, b, c = null 35 | expect(a).to be_nil 36 | expect(b).to be_nil 37 | expect(c).to be_nil 38 | end 39 | 40 | it "mimicks other classes and returns Null for their public methods" do 41 | class UnderMimickTest 42 | def test; end 43 | end 44 | 45 | mimick = Null.mimic(UnderMimickTest) 46 | expect(mimick.test).to be_null 47 | expect { mimick.i_dont_exist}.to raise_error(NoMethodError) 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /spec/lib/deterministic/option_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Deterministic 4 | 5 | describe Deterministic::Option do 6 | include Deterministic::Prelude::Option 7 | None = Deterministic::Prelude::Option::None 8 | 9 | specify { expect(described_class::Some.new(0)).to be_a described_class::Some } 10 | specify { expect(described_class::Some.new(0)).to be_a described_class } 11 | specify { expect(described_class::Some.new(0)).to eq Some(0) } 12 | 13 | specify { expect(described_class::None.new).to eq described_class::None.new } 14 | specify { expect(described_class::None.new).to be_a described_class::None } 15 | specify { expect(described_class::None.new).to be_a described_class } 16 | specify { expect(described_class::None.new).to eq None } 17 | 18 | it "join" do 19 | expect(Some(Some(1))).to eq Some(1) 20 | end 21 | 22 | it "fmap" do 23 | expect(Some(1).fmap { |n| n + 1}).to eq Some(2) 24 | expect(None.fmap { |n| n + 1}).to eq None 25 | end 26 | 27 | it "map" do 28 | expect(Some(1).map { |n| Some(n + 1)}).to eq Some(2) 29 | expect(Some(1).map { |n| None }).to eq None 30 | expect(None.map { |n| Some(n + 1)}).to eq None 31 | end 32 | 33 | it "some?" do 34 | expect(Some(1).some?).to be_truthy 35 | expect(None.some?).to be_falsey 36 | end 37 | 38 | it "none?" do 39 | expect(None.none?).to be_truthy 40 | expect(Some(1).none?).to be_falsey 41 | end 42 | 43 | it "value" do 44 | expect(Some(1).value).to eq 1 45 | expect{ None.value }.to raise_error NoMethodError 46 | end 47 | 48 | it "value_or" do 49 | expect(Some(1).value_or(2)).to eq 1 50 | expect(None.value_or(0)).to eq 0 51 | end 52 | 53 | it "+" do 54 | expect(Some([1]) + None).to eq Some([1]) 55 | expect(Some(1) + None + None).to eq Some(1) 56 | expect(Some(1) + Some(1)).to eq Some(2) 57 | expect(None + Some(1)).to eq Some(1) 58 | expect(None + None + Some(1)).to eq Some(1) 59 | expect(None + None + Some(1) + None).to eq Some(1) 60 | expect(None + Some({foo: 1})).to eq Some({:foo=>1}) 61 | expect(Some([1]) + Some([1])).to eq Some([1, 1]) 62 | expect { Some([1]) + Some(1)}.to raise_error TypeError 63 | end 64 | 65 | it "inspect" do 66 | expect(Some(1).inspect).to eq "Some(1)" 67 | expect(described_class::None.new.inspect).to eq "None" 68 | end 69 | 70 | it "to_s" do 71 | expect(Some(1).to_s).to eq "1" 72 | expect(described_class::None.new.to_s).to eq "" 73 | end 74 | 75 | it "match" do 76 | expect( 77 | Some(0).match { 78 | Some(where { s == 1 }) {|s| 99 } 79 | Some(where { s == 0 }) {|s| s + 1 } 80 | None() {} 81 | } 82 | ).to eq 1 83 | 84 | expect( 85 | Some(1).match { 86 | None() { 0 } 87 | Some() {|s| 1 } 88 | } 89 | ).to eq 1 90 | 91 | expect( 92 | Some(1).match { 93 | None() { 0 } 94 | Some(where { s.is_a? Integer }) {|s| 1 } 95 | } 96 | ).to eq 1 97 | 98 | expect( 99 | None.match { 100 | None() { 0 } 101 | Some() { 1 } 102 | } 103 | ).to eq 0 104 | end 105 | 106 | it "nil?" do 107 | expect(described_class.some?(nil)).to eq None 108 | expect(described_class.some?(1)).to be_some 109 | expect(described_class.some?(1)).to eq Some(1) 110 | end 111 | 112 | it "any?" do 113 | expect(described_class.any?(nil)).to be_none 114 | expect(described_class.any?(None)).to be_none 115 | expect(described_class.any?("")).to be_none 116 | expect(described_class.any?([])).to be_none 117 | expect(described_class.any?({})).to be_none 118 | expect(described_class.any?([1])).to eq Some([1]) 119 | expect(described_class.any?({foo: 1})).to eq Some({foo: 1}) 120 | expect(described_class.any?(1)).to eq Some(1) 121 | end 122 | 123 | it "try!" do 124 | expect(described_class.try! { raise "error" }).to be_none 125 | end 126 | end 127 | 128 | require_relative 'monad_axioms' 129 | 130 | describe Deterministic::Option::Some do 131 | it_behaves_like 'a Monad' do 132 | let(:monad) { described_class } 133 | end 134 | end 135 | 136 | # describe Deterministic::Option::None do 137 | # it_behaves_like 'a Monad' do 138 | 139 | # let(:monad) { described_class } 140 | # end 141 | # end 142 | -------------------------------------------------------------------------------- /spec/lib/deterministic/protocol_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'deterministic/protocol' 3 | 4 | 5 | module Monoid 6 | extend Deterministic::Protocol 7 | 8 | Deterministic::protocol(M) { 9 | fn empty() => M 10 | fn(append(a => M, b => M) => M) { |a, b| 11 | a + b 12 | } 13 | } 14 | 15 | Int = Deterministic::instance(Monoid, M => Integer) { 16 | def empty() 17 | 0 18 | end 19 | 20 | def append(a, b) 21 | a + b + 1 22 | end 23 | } 24 | 25 | String = Deterministic::instance(Monoid, M => String) { 26 | def empty() 27 | "" 28 | end 29 | } 30 | end 31 | 32 | describe Monoid do 33 | it "does something" do 34 | expect(described_class.constants).to contain_exactly(:Protocol, :Int, :String) 35 | int = described_class::Int.new 36 | expect(int.empty).to eq 0 37 | expect(int.append(1, 2)).to eq 4 38 | 39 | str = described_class::String.new 40 | expect(str.empty).to eq "" 41 | expect(str.append("a", "b")).to eq "ab" 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/lib/deterministic/result/failure_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../monad_axioms' 3 | require_relative 'result_shared' 4 | 5 | describe Deterministic::Result::Failure do 6 | include Deterministic::Prelude::Result 7 | 8 | it_behaves_like 'a Monad' do 9 | let(:monad) { described_class } 10 | end 11 | 12 | subject { described_class.new(1) } 13 | 14 | specify { expect(subject).to be_an_instance_of described_class } 15 | specify { expect(subject).to be_failure } 16 | specify { expect(subject).not_to be_success } 17 | specify { expect(subject.success?).to be_falsey } 18 | specify { expect(subject.failure?).to be_truthy } 19 | 20 | specify { expect(subject).to be_an_instance_of described_class } 21 | specify { expect(subject).to eq(described_class.new(1)) } 22 | specify { expect(subject.fmap { |v| v + 1} ).to eq Failure(2) } 23 | specify { expect(subject.map { |v| Success(v + 1)} ).to eq Failure(1) } 24 | specify { expect(subject.map_err { |v| Success(v + 1)} ).to eq Success(2) } 25 | specify { expect(subject.pipe { |v| raise RuntimeError unless v == Failure(1) } ).to eq Failure(1) } 26 | 27 | specify { expect(subject.or(Success(2))).to eq Success(2)} 28 | specify { expect(subject.or(Failure(2))).to eq Failure(2)} 29 | specify { expect(subject.or_else { Success(2) }).to eq Success(2)} 30 | specify { expect(subject.or_else { Failure(2) }).to eq Failure(2)} 31 | 32 | specify { expect(subject.and(Success(2))).to eq Failure(1)} 33 | specify { expect(subject.and_then { Success(2) }).to eq Failure(1)} 34 | 35 | it_behaves_like 'Result' do 36 | let(:result) { described_class } 37 | end 38 | 39 | it "#or" do 40 | expect(Success(1).or(Failure(2))).to eq Success(1) 41 | expect(Failure(1).or(Success(2))).to eq Success(2) 42 | expect { Failure(1).or(2) }.to raise_error(Deterministic::Monad::NotMonadError) 43 | end 44 | 45 | it "#or_else" do 46 | expect(Success(1).or_else { Failure(2) }).to eq Success(1) 47 | expect(Failure(1).or_else { |v| Success(v + 1) }).to eq Success(2) 48 | expect { Failure(1).or_else { 2 } }.to raise_error(Deterministic::Monad::NotMonadError) 49 | end 50 | 51 | it "#and" do 52 | expect(Success(1).and(Success(2))).to eq Success(2) 53 | expect(Failure(1).and(Success(2))).to eq Failure(1) 54 | expect { Success(1).and(2) }.to raise_error(Deterministic::Monad::NotMonadError) 55 | end 56 | 57 | it "#and_then" do 58 | expect(Success(1).and_then { Success(2) }).to eq Success(2) 59 | expect(Failure(1).and_then { Success(2) }).to eq Failure(1) 60 | expect { Success(1).and_then { 2 } }.to raise_error(Deterministic::Monad::NotMonadError) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /spec/lib/deterministic/result/result_map_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | 4 | describe Deterministic::Result do 5 | include Deterministic::Prelude::Result 6 | 7 | context ">> (map)" do 8 | specify { expect(Success(0).map { |n| Success(n + 1) }).to eq Success(1) } 9 | specify { expect(Failure(0).map { |n| Success(n + 1) }).to eq Failure(0) } 10 | 11 | it "Failure stops execution" do 12 | class ChainUnderTest 13 | include Deterministic::Prelude::Result 14 | alias :m :method 15 | 16 | def call 17 | init >> 18 | m(:validate) >> 19 | m(:send) >> 20 | m(:parse) 21 | end 22 | 23 | def init 24 | Success({step: 1}) 25 | end 26 | 27 | def validate(i) 28 | i[:step] = i[:step] + 1 29 | Success(i) 30 | end 31 | 32 | def send(i) 33 | i[:step] = i[:step] + 1 34 | Failure("Error @ #{i.fetch(:step)}") 35 | end 36 | 37 | def parse(i) 38 | i[:step] = i[:step] + 1 39 | Success(i) 40 | end 41 | end 42 | 43 | test = ChainUnderTest.new 44 | 45 | expect(test.call).to eq Failure("Error @ 3") 46 | end 47 | 48 | it "expects an Result" do 49 | def returns_non_result(i) 50 | 2 51 | end 52 | 53 | expect { Success(1) >> method(:returns_non_result) }.to raise_error(Deterministic::Monad::NotMonadError) 54 | end 55 | 56 | it "works with a block" do 57 | expect( 58 | Success(1).map { |i| Success(i + 1) } 59 | ).to eq Success(2) 60 | end 61 | 62 | it "works with a lambda" do 63 | expect( 64 | Success(1) >> ->(i) { Success(i + 1) } 65 | ).to eq Success(2) 66 | end 67 | 68 | it "does not catch exceptions" do 69 | expect { 70 | Success(1) >> ->(i) { raise "error" } 71 | }.to raise_error(RuntimeError) 72 | end 73 | end 74 | 75 | context "using self as the context for success" do 76 | class SelfContextUnderTest 77 | include Deterministic::Prelude::Result 78 | 79 | def call 80 | @step = 0 81 | Success(self). 82 | map(&:validate). 83 | map(&:build). 84 | map(&:send) 85 | end 86 | 87 | def validate 88 | @step = 1 89 | Success(self) 90 | end 91 | 92 | def build 93 | @step = 2 94 | Success(self) 95 | end 96 | 97 | def send 98 | @step = 3 99 | Success(self) 100 | end 101 | 102 | def inspect 103 | "Step #{@step}" 104 | end 105 | 106 | # # def self.procify(*meths) 107 | # # meths.each do |m| 108 | # # new_m = "__#{m}__procified".to_sym 109 | # # alias new_m m 110 | # # define_method new_m do |ctx| 111 | # # method(m) 112 | # # end 113 | # # end 114 | # # end 115 | 116 | # procify :send 117 | end 118 | 119 | it "works" do 120 | test = SelfContextUnderTest.new.call 121 | expect(test).to be_a described_class::Success 122 | expect(test.inspect).to eq "Success(Step 3)" 123 | end 124 | end 125 | 126 | context "<< (pipe)" do 127 | it "ignores the output of pipe" do 128 | acc = "ctx: " 129 | log = ->(ctx) { acc += ctx.inspect } 130 | 131 | actual = Success(1).pipe(log).map { Success(2) } 132 | expect(actual).to eq Success(2) 133 | expect(acc).to eq "ctx: Success(1)" 134 | end 135 | 136 | it "works with <<" do 137 | log = ->(n) { n.value + 1 } 138 | foo = ->(n) { Success(n + 1) } 139 | 140 | actual = Success(1) << log >> foo 141 | end 142 | end 143 | 144 | context ">= (try)" do 145 | it "try (>=) catches errors and wraps them as Failure" do 146 | def error(ctx) 147 | raise "error #{ctx}" 148 | end 149 | 150 | actual = Success(1) >= method(:error) 151 | expect(actual.inspect).to eq "Failure(#)" 152 | end 153 | end 154 | end 155 | -------------------------------------------------------------------------------- /spec/lib/deterministic/result/result_shared.rb: -------------------------------------------------------------------------------- 1 | shared_examples 'Result' do 2 | let(:result_name) { described_class.name.split("::")[-1]} 3 | specify { expect(subject.value).to eq 1 } 4 | specify { expect(result.new(subject)).to eq result.new(1) } 5 | 6 | it "#fmap" do 7 | expect(result.new(1).fmap { |e| e + 1 }).to eq result.new(2) 8 | end 9 | 10 | it "#bind" do 11 | expect(result.new(1).bind { |v| result.new(v + 1)}).to eq result.new(2) 12 | end 13 | 14 | it "#to_s" do 15 | expect(result.new(1).to_s).to eq "1" 16 | expect(result.new({a: 1}).to_s).to eq "{:a=>1}" 17 | end 18 | 19 | it "#inspect" do 20 | expect(result.new(1).inspect).to eq "#{result_name}(1)" 21 | expect(result.new(:a=>1).inspect).to eq "#{result_name}({:a=>1})" 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/lib/deterministic/result/success_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require_relative '../monad_axioms' 3 | require_relative 'result_shared' 4 | 5 | describe Deterministic::Result::Success do 6 | include Deterministic::Prelude::Result 7 | 8 | it_behaves_like 'a Monad' do 9 | let(:monad) { described_class } 10 | end 11 | 12 | subject { described_class.new(1) } 13 | 14 | specify { expect(subject).to be_an_instance_of described_class } 15 | specify { expect(subject).to be_success } 16 | specify { expect(subject).not_to be_failure } 17 | specify { expect(subject.success?).to be_truthy } 18 | specify { expect(subject.failure?).to be_falsey } 19 | 20 | specify { expect(subject).to be_an_instance_of described_class } 21 | specify { expect(subject).to eq(described_class.new(1)) } 22 | specify { expect(subject.fmap { |v| v + 1} ).to eq Success(2) } 23 | specify { expect(subject.map { |v| Failure(v + 1) } ).to eq Failure(2) } 24 | specify { expect(subject.map_err { |v| Failure(v + 1) } ).to eq Success(1) } 25 | 26 | specify { expect(subject.pipe{ |r| raise RuntimeError unless r == Success(1) } ).to eq Success(1) } 27 | 28 | specify { expect(subject.or(Success(2))).to eq Success(1)} 29 | specify { expect(subject.or_else { Success(2) }).to eq Success(1)} 30 | 31 | specify { expect(subject.and(Success(2))).to eq Success(2)} 32 | specify { expect(subject.and(Failure(2))).to eq Failure(2)} 33 | specify { expect(subject.and_then { Success(2) }).to eq Success(2)} 34 | specify { expect(subject.and_then { Failure(2) }).to eq Failure(2)} 35 | 36 | it_behaves_like 'Result' do 37 | let(:result) { described_class } 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/lib/deterministic/result_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Deterministic::Result do 4 | include Deterministic::Prelude::Result 5 | 6 | it "can't call Result#new directly" do 7 | expect { described_class.new(1)} 8 | .to raise_error(NoMethodError, "private method `new' called for Deterministic::Result:Class") 9 | end 10 | 11 | it "fmap" do 12 | expect(Success(1).fmap { |n| n + 1}).to eq Success(2) 13 | expect(Failure(0).fmap { |n| n + 1}).to eq Failure(1) 14 | end 15 | 16 | it "map" do 17 | expect(Success(1).map { |n| Success(n + 1)}).to eq Success(2) 18 | expect(Failure(0).map { |n| Success(n + 1)}).to eq Failure(0) 19 | end 20 | 21 | it "+" do 22 | expect(Success([1]) + Failure([2])).to eq Failure([2]) 23 | expect(Success(1) + Success(1)).to eq Success(2) 24 | expect(Failure(2) + Success(1)).to eq Failure(2) 25 | expect(Failure([2]) + Failure([3]) + Success(1)).to eq Failure([2, 3]) 26 | expect(Success([1]) + Success([1])).to eq Success([1, 1]) 27 | expect { Success([1]) + Success(1)}.to raise_error TypeError 28 | end 29 | 30 | subject { Success(1) } 31 | # specify { expect(subject).to be_an_instance_of described_class } 32 | specify { expect(subject).to be_success } 33 | specify { expect(subject).not_to be_failure } 34 | specify { expect(subject.success?).to be_truthy } 35 | specify { expect(subject.failure?).to be_falsey } 36 | 37 | specify { expect(subject).to be_a described_class } 38 | # specify { expect(subject).to eq(described_class.new(1)) } 39 | specify { expect(subject.fmap { |v| v + 1} ).to eq Success(2) } 40 | specify { expect(subject.map { |v| Failure(v + 1) } ).to eq Failure(2) } 41 | specify { expect(subject.map_err { |v| Failure(v + 1) } ).to eq Success(1) } 42 | 43 | specify { expect(subject.pipe{ |r| raise RuntimeError unless r == Success(1) } ).to eq Success(1) } 44 | 45 | specify { expect(subject.or(Success(2))).to eq Success(1)} 46 | specify { expect(subject.or_else { Success(2) }).to eq Success(1)} 47 | 48 | specify { expect(subject.and(Success(2))).to eq Success(2)} 49 | specify { expect(subject.and(Failure(2))).to eq Failure(2)} 50 | specify { expect(subject.and_then { Success(2) }).to eq Success(2)} 51 | specify { expect(subject.and_then { Failure(2) }).to eq Failure(2)} 52 | 53 | 54 | it "try!" do 55 | expect(described_class.try! { 1 }).to eq Success(1) 56 | expect(described_class.try! { raise "error" }.inspect).to eq Failure(RuntimeError.new("error")).inspect 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/lib/deterministic/sequencer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Deterministic::Sequencer do 4 | include Deterministic::Prelude::Result 5 | 6 | let(:test_class) { Class.new { include Deterministic::Sequencer } } 7 | let(:test_instance) { test_class.new } 8 | let(:arbitrary_success) { Success(double) } 9 | 10 | # This mock method makes #arbitrary_success available within the operations 11 | # in the test sequences 12 | before { allow(test_instance).to receive(:arbitrary_success).and_return(arbitrary_success) } 13 | 14 | it 'requires #and_yield to be specified' do 15 | expect do 16 | in_sequence do 17 | get(:_) { arbitrary_success } 18 | end 19 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield not called') 20 | end 21 | 22 | it 'does not allow calling #and_yield multiple times' do 23 | expect do 24 | in_sequence do 25 | and_yield { arbitrary_success } 26 | and_yield { arbitrary_success } 27 | end 28 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called') 29 | end 30 | 31 | it 'does not allow calling #get after #and_yield' do 32 | expect do 33 | in_sequence do 34 | and_yield { arbitrary_success } 35 | get(:_) { arbitrary_success } 36 | end 37 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called') 38 | end 39 | 40 | it 'does not allow calling #let after #and_yield' do 41 | expect do 42 | in_sequence do 43 | and_yield { arbitrary_success } 44 | let(:_) { arbitrary_success } 45 | end 46 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called') 47 | end 48 | 49 | it 'does not allow calling #and_then after #and_yield' do 50 | expect do 51 | in_sequence do 52 | and_yield { arbitrary_success } 53 | and_then { arbitrary_success } 54 | end 55 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called') 56 | end 57 | 58 | it 'does not allow calling #observe after #and_yield' do 59 | expect do 60 | in_sequence do 61 | and_yield { arbitrary_success } 62 | observe { arbitrary_success } 63 | end 64 | end.to raise_error(described_class::InvalidSequenceError, 'and_yield already called') 65 | end 66 | 67 | context 'when #and_yield succeeds' do 68 | let(:yielder_result) { Success('yield') } 69 | before { allow(test_instance).to receive(:yielder).and_return(yielder_result) } 70 | 71 | it "returns and_yield's result" do 72 | result = in_sequence do 73 | and_yield { yielder } 74 | end 75 | expect(result).to eq(yielder_result) 76 | end 77 | 78 | it 'ignores the return value of the #in_sequence block' do 79 | result = in_sequence do 80 | and_yield { yielder } 81 | 'a different return value' 82 | end 83 | expect(result).to eq(yielder_result) 84 | end 85 | end 86 | 87 | context 'when #and_yield fails' do 88 | let(:yielder_result) { Failure('yield') } 89 | before { allow(test_instance).to receive(:yielder).and_return(yielder_result) } 90 | 91 | it "returns and_yield's result" do 92 | result = in_sequence do 93 | and_yield { yielder } 94 | end 95 | expect(result).to eq(yielder_result) 96 | end 97 | end 98 | 99 | context 'when #get succeeds' do 100 | let(:getter_result) { Success('get') } 101 | before { allow(test_instance).to receive(:getter).and_return(getter_result) } 102 | 103 | it 'its result is available in a subsequent #get' do 104 | allow(test_instance).to receive(:second_getter).and_return(arbitrary_success) 105 | 106 | in_sequence do 107 | get(:get_result) { getter } 108 | get(:_) { second_getter(get_result) } 109 | and_yield { arbitrary_success } 110 | end 111 | 112 | expect(test_instance).to have_received(:second_getter).with(getter_result.value) 113 | end 114 | 115 | it 'its result is not available in a previous #get' do 116 | allow(test_instance).to receive(:second_getter) 117 | 118 | expect do 119 | in_sequence do 120 | get(:_) { second_getter(get_result) } 121 | get(:get_result) { getter } 122 | and_yield { arbitrary_success } 123 | end 124 | end.to raise_error(NameError) 125 | end 126 | 127 | it 'its result is available in a subsequent #and_then' do 128 | allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success) 129 | 130 | in_sequence do 131 | get(:get_result) { getter } 132 | and_then { and_then_function(get_result) } 133 | and_yield { arbitrary_success } 134 | end 135 | 136 | expect(test_instance).to have_received(:and_then_function).with(getter_result.value) 137 | end 138 | 139 | it 'its result is not available in a previous #and_then' do 140 | allow(test_instance).to receive(:and_then_function) 141 | 142 | expect do 143 | in_sequence do 144 | and_then { and_then_function(get_result) } 145 | get(:get_result) { getter } 146 | and_yield { arbitrary_success } 147 | end 148 | end.to raise_error(NameError) 149 | end 150 | 151 | it 'its result is available in a subsequent #observe' do 152 | allow(test_instance).to receive(:observer) 153 | 154 | in_sequence do 155 | get(:get_result) { getter } 156 | observe { observer(get_result) } 157 | and_yield { arbitrary_success } 158 | end 159 | 160 | expect(test_instance).to have_received(:observer).with(getter_result.value) 161 | end 162 | 163 | it 'its result is not available in a previous #observe' do 164 | allow(test_instance).to receive(:observer) 165 | 166 | expect do 167 | in_sequence do 168 | observe { observer(get_result) } 169 | get(:get_result) { getter } 170 | and_yield { arbitrary_success } 171 | end 172 | end.to raise_error(NameError) 173 | end 174 | 175 | it 'its result is available in #and_yield' do 176 | allow(test_instance).to receive(:yielder).and_return(arbitrary_success) 177 | 178 | in_sequence do 179 | get(:get_result) { getter } 180 | and_yield { yielder(get_result) } 181 | end 182 | 183 | expect(test_instance).to have_received(:yielder).with(getter_result.value) 184 | end 185 | end 186 | 187 | context 'when multiple #gets succeed' do 188 | let(:first_getter_result) { Success('get1') } 189 | let(:second_getter_result) { Success('get2') } 190 | 191 | before do 192 | allow(test_instance).to receive(:first_getter).and_return(first_getter_result) 193 | allow(test_instance).to receive(:second_getter).and_return(second_getter_result) 194 | end 195 | 196 | it 'both results are available in subsequent operations' do 197 | allow(test_instance).to receive(:yielder).and_return(arbitrary_success) 198 | 199 | in_sequence do 200 | get(:first_get_result) { first_getter } 201 | get(:second_get_result) { second_getter } 202 | and_yield { yielder(first_get_result, second_get_result) } 203 | end 204 | 205 | expect(test_instance).to have_received(:yielder) 206 | .with(first_getter_result.value, second_getter_result.value) 207 | end 208 | end 209 | 210 | context 'when #get fails' do 211 | let(:getter_result) { Failure('get') } 212 | before { allow(test_instance).to receive(:getter).and_return(getter_result) } 213 | 214 | it 'does not invoke subsequent #gets' do 215 | allow(test_instance).to receive(:second_getter).and_return(arbitrary_success) 216 | 217 | in_sequence do 218 | get(:get_result) { getter } 219 | get(:_) { second_getter(get_result) } 220 | and_yield { arbitrary_success } 221 | end 222 | 223 | expect(test_instance).not_to have_received(:second_getter) 224 | end 225 | 226 | it 'does not invoke subsequent #and_thens' do 227 | allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success) 228 | 229 | in_sequence do 230 | get(:get_result) { getter } 231 | and_then { and_then_function(get_result) } 232 | and_yield { arbitrary_success } 233 | end 234 | 235 | expect(test_instance).not_to have_received(:and_then_function) 236 | end 237 | 238 | it 'does not invoke subsequent #observes' do 239 | allow(test_instance).to receive(:observer) 240 | 241 | in_sequence do 242 | get(:get_result) { getter } 243 | observe { observer(get_result) } 244 | and_yield { arbitrary_success } 245 | end 246 | 247 | expect(test_instance).not_to have_received(:observer) 248 | end 249 | 250 | it 'does not invoke #and_yield' do 251 | allow(test_instance).to receive(:yielder).and_return(arbitrary_success) 252 | 253 | in_sequence do 254 | get(:get_result) { getter } 255 | and_yield { yielder } 256 | end 257 | 258 | expect(test_instance).not_to have_received(:yielder) 259 | end 260 | 261 | it 'returns the failure' do 262 | result = in_sequence do 263 | get(:get_result) { getter } 264 | and_yield { arbitrary_success } 265 | end 266 | 267 | expect(result).to eq(getter_result) 268 | end 269 | end 270 | 271 | context 'when #let succeeds' do 272 | let(:object) { {a: 'a'} } 273 | before { allow(test_instance).to receive(:object).and_return(object) } 274 | 275 | it 'its result is available in a subsequent #let' do 276 | allow(test_instance).to receive(:test_function) 277 | 278 | in_sequence do 279 | let(:a) { object.fetch(:a) } 280 | let(:_) { test_function(a) } 281 | and_yield { arbitrary_success } 282 | end 283 | 284 | expect(test_instance).to have_received(:test_function).with(object.fetch(:a)) 285 | end 286 | 287 | it 'its result is not available in a previous #let' do 288 | allow(test_instance).to receive(:test_function) 289 | 290 | expect do 291 | in_sequence do 292 | let(:_) { test_function(a) } 293 | let(:a) { object.fetch(:a) } 294 | and_yield { arbitrary_success } 295 | end 296 | end.to raise_error(NameError) 297 | end 298 | 299 | it 'its result is available in a subsequent #and_then' do 300 | allow(test_instance).to receive(:and_then_function).and_return(arbitrary_success) 301 | 302 | in_sequence do 303 | let(:a) { object.fetch(:a) } 304 | and_then { and_then_function(a) } 305 | and_yield { arbitrary_success } 306 | end 307 | 308 | expect(test_instance).to have_received(:and_then_function).with(object.fetch(:a)) 309 | end 310 | 311 | it 'its result is not available in a previous #and_then' do 312 | allow(test_instance).to receive(:and_then_function) 313 | 314 | expect do 315 | in_sequence do 316 | and_then { and_then_function(a) } 317 | let(:a) { object.fetch(:a) } 318 | and_yield { arbitrary_success } 319 | end 320 | end.to raise_error(NameError) 321 | end 322 | 323 | it 'its result is available in a subsequent #observe' do 324 | allow(test_instance).to receive(:observer) 325 | 326 | in_sequence do 327 | let(:a) { object.fetch(:a) } 328 | observe { observer(a) } 329 | and_yield { arbitrary_success } 330 | end 331 | 332 | expect(test_instance).to have_received(:observer).with(object.fetch(:a)) 333 | end 334 | 335 | it 'its result is not available in a previous #observe' do 336 | allow(test_instance).to receive(:observer) 337 | 338 | expect do 339 | in_sequence do 340 | observe { observer(a) } 341 | let(:a) { object.fetch(:a) } 342 | and_yield { arbitrary_success } 343 | end 344 | end.to raise_error(NameError) 345 | end 346 | 347 | it 'its result is available in #and_yield' do 348 | allow(test_instance).to receive(:yielder).and_return(arbitrary_success) 349 | 350 | in_sequence do 351 | let(:a) { object.fetch(:a) } 352 | and_yield { yielder(a) } 353 | end 354 | 355 | expect(test_instance).to have_received(:yielder).with(object.fetch(:a)) 356 | end 357 | end 358 | 359 | context 'when #let raises an error' do 360 | let(:object) { {a: 'a'} } 361 | before { allow(test_instance).to receive(:object).and_return(object) } 362 | 363 | it 'bubbles the raised error' do 364 | expect do 365 | in_sequence do 366 | let(:b) { object.fetch(:b) } 367 | and_yield { arbitrary_success } 368 | end 369 | end.to raise_error(KeyError) 370 | end 371 | 372 | it 'does not invoke #and_yield' do 373 | allow(test_instance).to receive(:yielder).and_return(arbitrary_success) 374 | 375 | begin 376 | in_sequence do 377 | let(:b) { object.fetch(:b) } 378 | and_yield { yielder } 379 | end 380 | rescue KeyError 381 | # Ignore 382 | end 383 | 384 | expect(test_instance).not_to have_received(:yielder) 385 | end 386 | end 387 | 388 | context 'when #and_then succeeds' do 389 | let(:and_then_result) { Success('and_then') } 390 | before { allow(test_instance).to receive(:and_then_function).and_return(and_then_result) } 391 | 392 | it 'continues the sequence' do 393 | allow(test_instance).to receive(:another_step).and_return(arbitrary_success) 394 | 395 | in_sequence do 396 | and_then { and_then_function } 397 | and_then { another_step } 398 | and_yield { arbitrary_success } 399 | end 400 | 401 | expect(test_instance).to have_received(:another_step) 402 | end 403 | end 404 | 405 | context 'when #and_then fails' do 406 | let(:and_then_result) { Failure('and_then') } 407 | before { allow(test_instance).to receive(:and_then_function).and_return(and_then_result) } 408 | 409 | it 'does not continue the sequence' do 410 | allow(test_instance).to receive(:another_step).and_return(arbitrary_success) 411 | 412 | in_sequence do 413 | and_then { and_then_function } 414 | and_then { another_step } 415 | and_yield { arbitrary_success } 416 | end 417 | 418 | expect(test_instance).not_to have_received(:another_step) 419 | end 420 | 421 | it 'returns the failure' do 422 | result = in_sequence do 423 | and_then { and_then_function } 424 | and_yield { arbitrary_success } 425 | end 426 | 427 | expect(result).to eq(and_then_result) 428 | end 429 | end 430 | 431 | context 'when #observe returns a failure' do 432 | let(:observe_result) { Failure('observe') } 433 | before { allow(test_instance).to receive(:observer).and_return(observe_result) } 434 | 435 | it 'its return value is ignored and the sequence continues' do 436 | allow(test_instance).to receive(:another_step).and_return(arbitrary_success) 437 | 438 | in_sequence do 439 | observe { observer } 440 | and_then { another_step } 441 | and_yield { arbitrary_success } 442 | end 443 | 444 | expect(test_instance).to have_received(:another_step) 445 | end 446 | end 447 | 448 | 449 | it 'does not allow calling methods outside of the wrapped instance' do 450 | expect do 451 | in_sequence do 452 | and_yield { top_level_test_method } 453 | end 454 | end.to raise_error(NameError) 455 | end 456 | 457 | context 'when including Deterministic::Prelude' do 458 | let(:test_class) { Class.new { include Deterministic::Prelude } } 459 | 460 | it '#in_sequence is available' do 461 | expect do 462 | in_sequence do 463 | and_yield { arbitrary_success } 464 | end 465 | end.not_to raise_error 466 | end 467 | end 468 | 469 | context 'readme example' do 470 | let(:test_class) do 471 | Class.new do 472 | include Deterministic::Prelude 473 | 474 | def call(input) 475 | in_sequence do 476 | get(:sanitized_input) { sanitize(input) } 477 | and_then { validate(sanitized_input) } 478 | get(:user) { get_user_from_db(sanitized_input) } 479 | let(:name) { user.fetch(:name) } 480 | observe { log('user name', name) } 481 | get(:request) { build_request(sanitized_input, user) } 482 | observe { log('sending request', request) } 483 | get(:response) { send_request(request) } 484 | observe { log('got response', response) } 485 | and_yield { format_response(response) } 486 | end 487 | end 488 | 489 | def sanitize(input) 490 | sanitized_input = input 491 | Success(sanitized_input) 492 | end 493 | 494 | def validate(sanitized_input) 495 | Success(sanitized_input) 496 | end 497 | 498 | def get_user_from_db(sanitized_input) 499 | Success(type: :admin, id: sanitized_input.fetch(:id), name: 'John') 500 | end 501 | 502 | def build_request(sanitized_input, user) 503 | Success(input: sanitized_input, user: user) 504 | end 505 | 506 | def log(message, data) 507 | # logger.info(message, data) 508 | end 509 | 510 | def send_request(request) 511 | Success(status: 200) 512 | end 513 | 514 | def format_response(response) 515 | Success(response: response, message: 'it worked') 516 | end 517 | end 518 | end 519 | 520 | it 'returns expected result' do 521 | result = test_instance.call(id: 1) 522 | 523 | expect(result).to eq(Success( 524 | response: {status: 200}, 525 | message: 'it worked' 526 | )) 527 | end 528 | 529 | it 'logs expected values' do 530 | allow(test_instance).to receive(:log).and_call_original 531 | 532 | test_instance.call(id: 1) 533 | 534 | expect(test_instance).to have_received(:log) 535 | .with('user name', 'John') 536 | .ordered 537 | expect(test_instance).to have_received(:log) 538 | .with('sending request', 539 | input: {id: 1}, 540 | user: {type: :admin, id: 1, name: 'John'} 541 | ) 542 | .ordered 543 | expect(test_instance).to have_received(:log) 544 | .with('got response', status: 200) 545 | .ordered 546 | end 547 | end 548 | 549 | def in_sequence(&block) 550 | test_instance.instance_eval do 551 | in_sequence(&block) 552 | end 553 | end 554 | end 555 | 556 | def top_level_test_method 557 | :empty 558 | end 559 | -------------------------------------------------------------------------------- /spec/lib/enum_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'deterministic/enum' 3 | 4 | describe Deterministic::Enum do 5 | include Deterministic 6 | 7 | it "can't use value" do 8 | expect { InvalidEnum = Deterministic::enum { 9 | Unary(:value) 10 | }}.to raise_error ArgumentError 11 | end 12 | 13 | context "Nullary, Unary, Binary" do 14 | MyEnym = Deterministic::enum { 15 | Nullary() 16 | Unary(:a) 17 | Binary(:a, :b) 18 | } 19 | 20 | it "can't instantiate parent" do 21 | expect { MyEnym.new }.to raise_error NoMethodError, "private method `new' called for MyEnym:Class" 22 | end 23 | 24 | it "Nullary" do 25 | n = MyEnym.Nullary 26 | 27 | expect(n).to be_a MyEnym 28 | expect(n).to be_a MyEnym::Nullary 29 | expect(n.name).to eq "Nullary" 30 | expect { n.value }.to raise_error NoMethodError 31 | expect(n.inspect).to eq "Nullary" 32 | expect(n.to_s).to eq "" 33 | expect(n.fmap { }).to eq n 34 | end 35 | 36 | it "Unary" do 37 | u = MyEnym::Unary(1) 38 | 39 | expect(u).to be_a MyEnym 40 | expect(u).to be_a MyEnym::Unary 41 | expect(u.name).to eq "Unary" 42 | expect(u.a).to eq 1 43 | expect(u.value).to eq 1 44 | expect(u.inspect).to eq "Unary(1)" 45 | expect(u.to_s).to eq "1" 46 | end 47 | 48 | it "Binary" do 49 | # hash 50 | b = MyEnym::Binary(a: 1, b: 2) 51 | expect(b).to be_a MyEnym 52 | expect(b).to be_a MyEnym::Binary 53 | expect(b.name).to eq "Binary" 54 | expect(b.inspect).to eq "Binary(a: 1, b: 2)" 55 | 56 | expect(b.a).to eq 1 57 | expect(b.b).to eq 2 58 | expect(b.value).to eq({a: 1, b: 2}) 59 | 60 | # values only 61 | b = MyEnym::Binary(1, 2) 62 | expect(b.value).to eq({a: 1, b: 2}) 63 | 64 | # other names are ok 65 | b = MyEnym::Binary(c: 1, d: 2) 66 | expect(b.value).to eq({a: 1, b: 2}) 67 | 68 | expect { MyEnym::Binary(1) }.to raise_error ArgumentError 69 | end 70 | 71 | it "generated enum" do 72 | expect(MyEnym.variants).to contain_exactly(:Nullary, :Unary, :Binary) 73 | expect(MyEnym.constants).to contain_exactly(:Nullary, :Unary, :Binary, :Matcher) 74 | 75 | b = MyEnym::Binary(a: 1, b: 2) 76 | 77 | res = 78 | MyEnym.match(b) { 79 | Nullary() { 0 } 80 | Unary() {|a| a } 81 | Binary() {|x,y| [x, y] } 82 | } 83 | 84 | expect(res).to eq [1, 2] 85 | 86 | res = 87 | b.match { 88 | Nullary() { 0 } 89 | Unary() {|a| a } 90 | Binary() {|x,y| [x, y] } 91 | } 92 | 93 | expect(res).to eq [1, 2] 94 | 95 | expect { b.match { 96 | Nullary # Nullary is treated as a constant 97 | } 98 | }.to raise_error(NameError) 99 | 100 | expect { b.match { 101 | Nullary() 102 | Unary() 103 | Binary() 104 | } 105 | }.to raise_error ArgumentError, "No block given to `Nullary`" 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /spec/readme_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | include Deterministic::Prelude::Result 4 | 5 | Success(1).to_s # => "1" 6 | Success(Success(1)) # => Success(1) 7 | 8 | Failure(1).to_s # => "1" 9 | Failure(Failure(1)) # => Failure(1) 10 | 11 | Success(1).fmap { |v| v + 1} # => Success(2) 12 | Failure(1).fmap { |v| v - 1} # => Failure(0) 13 | 14 | 15 | Threenum = Deterministic::enum { 16 | Nullary() 17 | Unary(:a) 18 | Binary(:a, :b) 19 | } 20 | 21 | Deterministic::impl(Threenum) { 22 | def sum 23 | match { 24 | Nullary() { 0 } 25 | Unary() { |u| u } 26 | Binary() { |a, b| a + b } 27 | } 28 | end 29 | 30 | def +(other) 31 | match { 32 | Nullary() { other.sum } 33 | Unary() { |a| self.sum + other.sum } 34 | Binary() { |a, b| self.sum + other.sum } 35 | } 36 | end 37 | } 38 | 39 | describe Threenum do 40 | it "works" do 41 | expect(Threenum.Nullary + Threenum.Unary(1)).to eq 1 42 | expect(Threenum.Nullary + Threenum.Binary(2, 3)).to eq 5 43 | expect(Threenum.Unary(1) + Threenum.Binary(2, 3)).to eq 6 44 | expect(Threenum.Binary(2, 3) + Threenum.Binary(2, 3)).to eq 10 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'simplecov' 2 | SimpleCov.start 3 | 4 | require 'deterministic' 5 | 6 | RSpec.configure do |config| 7 | # Limit the spec run to only specs with the focus metadata. If no specs have 8 | # the filtering metadata and `run_all_when_everything_filtered = true` then 9 | # all specs will run. 10 | config.filter_run :focus 11 | 12 | # Run all specs when none match the provided filter. This works well in 13 | # conjunction with `config.filter_run :focus`, as it will run the entire 14 | # suite when no specs have `:filter` metadata. 15 | config.run_all_when_everything_filtered = true 16 | 17 | # Run specs in random order to surface order dependencies. If you find an 18 | # order dependency and want to debug it, you can fix the order by providing 19 | # the seed, which is printed after each run. 20 | # --seed 1234 21 | config.expect_with :rspec do |c| 22 | c.syntax = :expect 23 | end 24 | config.order = 'random' 25 | 26 | config.filter_run_excluding isolate: true 27 | end 28 | --------------------------------------------------------------------------------