Repository: txus/kleisli Branch: master Commit: 65d5eac2d70e Files: 23 Total size: 21.9 KB Directory structure: gitextract_4nn5_ypf/ ├── .github/ │ └── workflows/ │ └── ci.yml ├── .gitignore ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── kleisli.gemspec ├── lib/ │ ├── kleisli/ │ │ ├── composition.rb │ │ ├── either.rb │ │ ├── functor.rb │ │ ├── future.rb │ │ ├── maybe.rb │ │ ├── monad.rb │ │ ├── try.rb │ │ └── version.rb │ └── kleisli.rb └── test/ ├── kleisli/ │ ├── composition_test.rb │ ├── either_test.rb │ ├── future_test.rb │ ├── maybe_test.rb │ └── try_test.rb └── test_helper.rb ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: push: pull_request: jobs: test: runs-on: ubuntu-latest strategy: matrix: ruby-version: - 3.2 - 3.1 - "3.0" steps: - uses: actions/checkout@v3 - name: Set up Ruby ${{ matrix.ruby-version }} uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby-version }} bundler-cache: true # runs 'bundle install' and caches installed gems automatically - name: Run tests run: bundle exec rake ================================================ FILE: .gitignore ================================================ /.bundle/ /.yardoc /Gemfile.lock /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ *.bundle *.so *.o *.a mkmf.log *.gem ================================================ FILE: .travis.yml ================================================ language: ruby dist: trusty matrix: include: - rvm: 2.1 - rvm: 2.2 - rvm: 2.3.3 - rvm: 2.4.0 - rvm: ruby-head - rvm: jruby-head env: "JRUBY_OPTS=--debug" before_install: - gem install bundler -v'1.13.7' - rvm: jruby-9.1.7.0 env: "JRUBY_OPTS=--debug" script: - rake ================================================ FILE: Gemfile ================================================ source 'https://rubygems.org' # Specify your gem's dependencies in kleisli.gemspec gemspec ================================================ FILE: LICENSE.txt ================================================ Copyright (c) 2014 Josep M. Bach, Ryan Levick MIT License Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Kleisli [![Build Status](https://secure.travis-ci.org/txus/kleisli.svg)](http://travis-ci.org/txus/kleisli) An idiomatic, clean implementation of a few common useful monads in Ruby, written by [Ryan Levick][rylev] and me. It aims to be idiomatic Ruby to use in Enter-Prise production apps, not a proof of concept. In your Gemfile: ```ruby gem 'kleisli' ``` We would like to thank Curry and Howard for their correspondence. ## Notation For all its monads, Kleisli implements `return` (we call it `lift` instead, as `return` is a reserved keyword in Ruby) with convenience global methods (see which for each monad below). Kleisli uses a clever Ruby syntax trick to implement the `bind` operator, which looks like this: `>->` when used with a block. We will probably burn in hell for this. You can also use `>` or `>>` if you're going to pass in a proc or lambda object. `Maybe` and `Either` are applicative functors with the apply operator `*`. Read further to see how it works. ### Function composition You can use Haskell-like function composition with F and the familiar `.`. This is such a perversion of Ruby syntax that Matz would probably condemn this: Think of `F` as the identity function. Although it's just a hack to make it work in Ruby. ```ruby # Reminder that (f . g) x= f(g(x)) f = F . first . last f.call [[1,2], [3,4]] # => 3 f = F . capitalize . reverse f.call "hello" # => "Olleh" ``` Functions and methods are interchangeable: ```ruby foo = lambda { |s| s.reverse } f = F . capitalize . fn(&foo) f.call "hello" # => "Olleh" ``` All functions and methods are partially applicable: ```ruby # Partially applied method: f = F . split(":") . strip f.call " localhost:9092 " # => ["localhost", "9092"] # Partially applied lambda: my_split = lambda { |str, *args| str.split(*args) } f = F . fn(":", &my_split) . strip f.call " localhost:9092 " # => ["localhost", "9092"] ``` Finally, for convenience, `F` is the identity function: ```ruby F.call(1) # => 1 ``` ## Maybe monad The Maybe monad is useful to express a pipeline of computations that might return nil at any point. `user.address.street` anyone? ### `>->` (bind) ```ruby require "kleisli" maybe_user = Maybe(user) >-> user { Maybe(user.address) } >-> address { Maybe(address.street) } # If user exists # => Some("Monad Street") # If user is nil # => None() # You can also use Some and None as type constructors yourself. x = Some(10) y = None() ``` As usual (with Maybe and Either), using point-free style is much cleaner: ```ruby Maybe(user) >> F . fn(&Maybe) . address >> F . fn(&Maybe) . street ``` ### `fmap` ```ruby require "kleisli" # If we know that a user always has an address with a street Maybe(user).fmap(&:address).fmap(&:street) # If the user exists # => Some("Monad Street") # If the user is nil # => None() ``` ### `*` (applicative functor's apply) ```ruby require "kleisli" add = -> x, y { x + y } Some(add) * Some(10) * Some(2) # => Some(12) Some(add) * None() * Some(2) # => None ``` ## Try The Try monad is useful to express a pipeline of computations that might throw an exception at any point. ### `>->` (bind) ```ruby require "kleisli" json_string = get_json_from_somewhere result = Try { JSON.parse(json_string) } >-> json { Try { json["dividend"].to_i / json["divisor"].to_i } } # If no exception was thrown: result # => # result.value # => 123 # If there was a ZeroDivisionError exception for example: result # => #> result.exception # => # ``` ### `fmap` ```ruby require "kleisli" Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).value # If everything went well: # => { :my => "json", :with => "symbolized keys" } # If an exception was thrown: # => nil ``` ### `to_maybe` Sometimes it's useful to interleave both `Try` and `Maybe`. To convert a `Try` into a `Maybe` you can use `to_maybe`: ```ruby require "kleisli" Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_maybe # If everything went well: # => Some({ :my => "json", :with => "symbolized keys" }) # If an exception was thrown: # => None() ``` ### `to_either` Sometimes it's useful to interleave both `Try` and `Either`. To convert a `Try` into a `Either` you can use `to_either`: ```ruby require "kleisli" Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_either # If everything went well: # => Right({ :my => "json", :with => "symbolized keys" }) # If an exception was thrown: # => Left(#) ``` ## Either The Either monad is useful to express a pipeline of computations that might return an error object with some information. It has two type constructors: `Right` and `Left`. As a useful mnemonic, `Right` is for when everything went "right" and `Left` is used for errors. Think of it as exceptions without messing with the call stack. ### `>->` (bind) ```ruby require "kleisli" result = Right(3) >-> value { if value > 1 Right(value + 3) else Left("value was less or equal than 1") end } >-> value { if value % 2 == 0 Right(value * 2) else Left("value was not even") end } # If everything went well result # => Right(12) result.value # => 12 # If it failed in the first block result # => Left("value was less or equal than 1") result.value # => "value was less or equal than 1" # If it failed in the second block result # => Left("value was not even") result.value # => "value was not even" # Point-free style bind! result = Right(3) >> F . fn(&Right) . *(2) result # => Right(6) result.value # => 6 ``` ### `fmap` ```ruby require "kleisli" result = if foo > bar Right(10) else Left("wrong") end.fmap { |x| x * 2 } # If everything went well result # => Right(20) # If it didn't result # => Left("wrong") ``` ### `*` (applicative functor's apply) ```ruby require "kleisli" add = -> x, y { x + y } Right(add) * Right(10) * Right(2) # => Right(12) Right(add) * Left("error") * Right(2) # => Left("error") ``` ### `or` `or` does pretty much what would you expect: ```ruby require 'kleisli' Right(10).or(Right(999)) # => Right(10) Left("error").or(Left("new error")) # => Left("new error") Left("error").or { |err| Left("new #{err}") } # => Left("new error") ``` ### `to_maybe` Sometimes it's useful to turn an `Either` into a `Maybe`. You can use `to_maybe` for that: ```ruby require "kleisli" result = if foo > bar Right(10) else Left("wrong") end.to_maybe # If everything went well: result # => Some(10) # If it didn't result # => None() ``` ## Future The Future monad models a pipeline of computations that will happen in the future, as soon as the value needed for each step is available. It is useful to model, for example, a sequential chain of HTTP calls. There's a catch unfortunately -- values passed to the functions are wrapped in lambdas, so you need to call `.call` on them. See the examples below. ### `>->` (bind) ```ruby require "kleisli" f = Future("myendpoint.com") >-> url { Future { HTTP.get(url.call) } } >-> response { Future { other_url = JSON.parse(response.call.body)[:other_url] HTTP.get(other_url) } } >-> other_response { Future { JSON.parse(other_response.call.body) } } # Do some other stuff... f.await # => block until the whole pipeline is realized # => { "my" => "response body" } ``` ### `fmap` ```ruby require "kleisli" Future { expensive_operation }.fmap { |x| x * 2 }.await # => result of expensive_operation * 2 ``` ## Who's this This was made by [Josep M. Bach (Txus)](http://blog.txus.io) and [Ryan Levick][rylev] under the MIT license. We are [@txustice][twitter] and [@itchyankles][itchyankles] on twitter (where you should probably follow us!). [twitter]: https://twitter.com/txustice [itchyankles]: https://twitter.com/itchyankles [rylev]: https://github.com/rylev ================================================ FILE: Rakefile ================================================ require "bundler/gem_tasks" require "rake/testtask" Rake::TestTask.new do |t| t.libs << "test" t.test_files = FileList['test/**/*_test.rb'] t.verbose = true end task :default => :test ================================================ FILE: kleisli.gemspec ================================================ # coding: utf-8 lib = File.expand_path('../lib', __FILE__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'kleisli/version' Gem::Specification.new do |spec| spec.name = "kleisli" spec.version = Kleisli::VERSION spec.authors = ["Josep M. Bach", "Ryan Levick"] spec.email = ["josep.m.bach@gmail.com", "ryan.levick@gmail.com"] spec.summary = %q{Usable, idiomatic common monads in Ruby} spec.description = %q{Usable, idiomatic common monads in Ruby} spec.homepage = "https://github.com/txus/kleisli" spec.license = "MIT" spec.files = `git ls-files -z`.split("\x0") spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) spec.require_paths = ["lib"] spec.add_development_dependency "bundler", "~> 1.6" spec.add_development_dependency "rake", "~> 10.0" spec.add_development_dependency "minitest", "~> 5.5" end ================================================ FILE: lib/kleisli/composition.rb ================================================ module Kleisli class ComposedFn < BasicObject def self.comp(f, g) lambda { |*args| f[g[*args]] } end def initialize(fns=[]) @fns = fns end def fn(*args, &block) f = -> arguments, receiver { block.call(receiver, *arguments) }.curry[args] ComposedFn.new(@fns + [f]) end def method_missing(meth, *args, &block) f = -> arguments, receiver { receiver.send(meth, *arguments, &block) }.curry[args] ComposedFn.new(@fns + [f]) end def call(*args) if @fns.any? @fns.reduce { |f, g| ComposedFn.comp(f, g) }.call(*args) else args.first end end def to_ary @fns.to_ary end end end F = Kleisli::ComposedFn.new ================================================ FILE: lib/kleisli/either.rb ================================================ require 'kleisli/monad' require 'kleisli/maybe' module Kleisli class Either < Monad attr_reader :right, :left def ==(other) right == other.right && left == other.left end def *(other) self >-> f { other >-> val { Right(f.arity > 1 ? f.curry.call(val) : f.call(val)) } } end class Right < Either alias value right def initialize(right) @right = right end def >(f) f.call(@right) end def fmap(&f) Right.new(f.call(@right)) end def to_maybe Maybe::Some.new(@right) end def or(other=nil, &other_blk) self end def to_s "Right(#{@right.inspect})" end alias inspect to_s end class Left < Either alias value left def initialize(left) @left = left end def >(f) self end def fmap(&f) self end def to_maybe Maybe::None.new end def or(other=self, &other_blk) if other_blk other_blk.call(@left) else other end end def to_s "Left(#{@left.inspect})" end alias inspect to_s end end end Right = Kleisli::Either::Right.method(:new) Left = Kleisli::Either::Left.method(:new) def Right(v) Kleisli::Either::Right.new(v) end def Left(v) Kleisli::Either::Left.new(v) end ================================================ FILE: lib/kleisli/functor.rb ================================================ module Kleisli class Functor def fmap(&f) raise NotImplementedError, "this functor doesn't implement fmap" end end end ================================================ FILE: lib/kleisli/future.rb ================================================ require 'kleisli/monad' module Kleisli class Future < Monad def self.lift(v=nil, &block) if block new(Thread.new(&block)) else new(Thread.new { v }) end end def initialize(t) @t = t end def >(f) f.call(-> { await }) end def fmap(&f) Future.lift(f.call(-> { await })) end def await @t.join.value end end end def Future(v=nil, &block) Kleisli::Future.lift(v, &block) end ================================================ FILE: lib/kleisli/maybe.rb ================================================ require 'kleisli/monad' module Kleisli class Maybe < Monad attr_reader :value def self.lift(value) if value.nil? None.new else Some.new(value) end end def ==(other) value == other.value end def *(other) self >-> f { f = f.to_proc other >-> val { Maybe.lift(f.arity > 1 ? f.curry.call(val) : f.call(val)) } } end class None < Maybe def fmap(&f) self end def >(block) self end def or(other=self, &other_blk) if other_blk other_blk.call else other end end def to_s "None" end alias inspect to_s end class Some < Maybe def initialize(value) @value = value end def fmap(&f) Maybe.lift(f.call(@value)) end def >(block) block.call(@value) end def or(other=nil, &other_blk) self end def to_s "Some(#{@value.inspect})" end alias inspect to_s end end end Maybe = Kleisli::Maybe.method(:lift) def Maybe(v) Maybe.(v) end def None() Maybe(nil) end def Some(v) Maybe(v) end ================================================ FILE: lib/kleisli/monad.rb ================================================ require "kleisli/functor" module Kleisli class Monad < Functor def >(block) raise NotImplementedError, "this monad doesn't implement >->" end def >>(block) self > block end end end ================================================ FILE: lib/kleisli/try.rb ================================================ require 'kleisli/monad' require 'kleisli/maybe' module Kleisli class Try < Monad attr_reader :exception, :value def self.lift(f) Success.new(f.call) rescue => e Failure.new(e) end class Success < Try def initialize(value) @value = value end def >(f) f.call(@value) rescue => e Failure.new(e) end def fmap(&f) Try { f.call(@value) } end def to_maybe Maybe::Some.new(@value) end def to_either Either::Right.new(@value) end end class Failure < Try def initialize(exception) @exception = exception end def >(f) self end def fmap(&f) self end def to_maybe Maybe::None.new end def to_either Either::Left.new(@exception) end end end end def Try(&f) Kleisli::Try.lift(f) end ================================================ FILE: lib/kleisli/version.rb ================================================ module Kleisli VERSION = "0.2.7" end ================================================ FILE: lib/kleisli.rb ================================================ require "kleisli/version" require "kleisli/maybe" require "kleisli/try" require "kleisli/future" require "kleisli/either" require "kleisli/composition" module Kleisli end ================================================ FILE: test/kleisli/composition_test.rb ================================================ require 'test_helper' class CompositionTest < Minitest::Test def test_one_method f = F . first result = f.call([1]) assert Fixnum === result, "#{result} is not a number" assert_equal 1, result end def test_two_methods f = F . first . last result = f.call([1, [2,3]]) assert Fixnum === result, "#{result} is not a number" assert_equal 2, result end def test_one_function my_first = lambda { |x| x.first } f = F . fn(&my_first) result = f.call([1]) assert Fixnum === result, "#{result} is not a number" assert_equal 1, result end def test_two_functions my_first = lambda { |x| x.first } my_last = lambda { |x| x.last } f = F . fn(&my_first) . fn(&my_last) result = f.call([1, [2,3]]) assert Fixnum === result, "#{result} is not a number" assert_equal 2, result end def test_one_function_one_block my_last = lambda { |x| x.last } f = F . fn { |x| x.first } . fn(&my_last) result = f.call([1, [2,3]]) assert Fixnum === result, "#{result} is not a number" assert_equal 2, result end def test_one_function_one_method my_last = lambda { |x| x.last } f = F . first . fn(&my_last) result = f.call([1, [2,3]]) assert Fixnum === result, "#{result} is not a number" assert_equal 2, result end def test_undefined_method f = F . foo assert_raises(NoMethodError) { f.call(1) } end def test_identity assert_equal 1, F.call(1) end def test_partially_applied_method f = F . split(":") result = f.call("localhost:9092") assert Array === result, "#{result} is not an array" assert_equal ["localhost", "9092"], result end def test_partially_applied_fn split = lambda { |x, *args| x.split(*args) } f = F . fn(":", &split) result = f.call("localhost:9092") assert Array === result, "#{result} is not an array" assert_equal ["localhost", "9092"], result end end ================================================ FILE: test/kleisli/either_test.rb ================================================ require 'test_helper' class EitherTest < Minitest::Test def test_lift_right assert_equal 3, Right(3).value end def test_lift_left assert_equal "error", Left("error").value end def test_bind_right v = Right(1) >-> x { if x == 1 Right(x + 90) else Left("FAIL") end } assert_equal Right(91), v end def test_bind_left v = Left("error") >-> x { Right(x * 20) } assert_equal Left("error"), v end def test_fmap_right assert_equal Right(2), Right(1).fmap { |x| x * 2 } end def test_fmap_left assert_equal Left("error"), Left("error").fmap { |x| x * 2 } end def test_to_maybe_right assert_equal Some(2), Right(1).fmap { |x| x * 2 }.to_maybe end def test_to_maybe_left assert_equal None(), Left("error").fmap { |x| x * 2 }.to_maybe end def test_pointfree assert_equal Right(10), Right(5) >> F . fn(&Right) . *(2) end def test_applicative_functor_right_arity_1 assert_equal Right(20), Right(-> x { x * 2 }) * Right(10) end def test_applicative_functor_right_arity_2 assert_equal Right(20), Right(-> x, y { x * y }) * Right(10) * Right(2) end def test_applicative_functor_left assert_equal Left("error"), Right(-> x, y { x * y }) * Left("error") * Right(2) end end ================================================ FILE: test/kleisli/future_test.rb ================================================ require 'test_helper' class FutureTest < Minitest::Test def test_immediate_value assert_equal 30, Future(30).await end def test_simple_future_executes_in_parallel str = "" Future { sleep 0.1; str << "bar" }.tap { str << "foo" }.await assert_equal "foobar", str end def test_bind f = Future(30) >-> n { Future { n.call * 2 } } >-> n { Future { n.call * 2 } >-> m { Future(m.call + 2) } } assert_equal 122, f.await end def test_fmap f = Future(30).fmap { |x| x.call * 2 } assert_equal 60, f.await end end ================================================ FILE: test/kleisli/maybe_test.rb ================================================ require 'test_helper' class MaybeTest < Minitest::Test def test_unwrapping_some assert_equal 3, Some(3).value end def test_unwrapping_none assert_equal nil, None().value end def test_bind_none assert_equal None(), None() >> F . fn(&Maybe) . *(2) end def test_bind_some assert_equal Some(6), Some(3) >> F . fn(&Maybe) . *(2) end def test_fmap_none assert_equal None(), None().fmap { |x| x * 2 } end def test_fmap_some assert_equal Some(6), Some(3).fmap { |x| x * 2 } end def test_applicative_functor_some_arity_1 assert_equal Some(20), Maybe(-> x { x * 2 }) * Maybe(10) end def test_applicative_functor_some_arity_2 assert_equal Some(20), Maybe(-> x, y { x * y }) * Maybe(10) * Maybe(2) end def test_applicative_functor_none assert_equal None(), Maybe(-> x, y { x * y }) * None() * Maybe(2) end end ================================================ FILE: test/kleisli/try_test.rb ================================================ require 'test_helper' class TryTest < Minitest::Test def test_success assert_equal 2, Try { 10 / 5 }.value end def test_failure assert_kind_of ZeroDivisionError, Try { 10 / 0 }.exception end def test_to_maybe_success assert_equal Some(2), Try { 10 / 5 }.to_maybe end def test_to_maybe_failure assert_equal None(), Try { 10 / 0 }.to_maybe end def test_to_either_success assert_equal Right(2), Try { 10 / 5 }.to_either end def test_to_either_failure assert_kind_of ZeroDivisionError, Try { 10 / 0 }.to_either.left end def test_fmap_success assert_equal 4, Try { 10 / 5 }.fmap { |x| x * 2 }.value end def test_fmap_failure assert_kind_of ZeroDivisionError, Try { 10 / 0 }.fmap { |x| x * 2 }.exception end def test_bind try = Try { 20 / 10 } >-> number { Try { 10 / number } } assert_equal 5, try.value end end ================================================ FILE: test/test_helper.rb ================================================ require 'kleisli' require 'minitest' require 'minitest/autorun'