ruby-monads 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 20b2534e81fe4b81311ea0dc9b9ab696edab9a2cffc124ef922bb9d26edc87b8
4
+ data.tar.gz: 050a8dea7bfeb6dea6b6635c90ce216968208d73346023440ef4410cbdd00e62
5
+ SHA512:
6
+ metadata.gz: 0a8ce58d40d7c361e1e860c1700b5edbbbb927698449ecddc412fdd3ce6f3bc3f3bd1e923ef62ab33fd7f92e13fc3b7f1afb7e7d7cfb79337bda0cfcb9aacf19
7
+ data.tar.gz: 604d7fddc645a0e08591d192736339eacc17174a39f3d4b58500c4aaa9df19b02c5917cd7cc6f0cfcfeedb5ee62ca2673fd611e00216d603671ec6e174ec00ee
data/.gems ADDED
@@ -0,0 +1 @@
1
+ cutest -v 1.2.3
@@ -0,0 +1 @@
1
+ *.gem
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Guillaume BOUDON
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ .PHONY: test
2
+
3
+ test:
4
+ cutest -r ./test/helper.rb ./test/*_test.rb
@@ -0,0 +1,68 @@
1
+ # ruby-monads
2
+
3
+ These are some Ruby implementations of some common monads.
4
+
5
+ ## Usage
6
+
7
+ If you don't already have it installed:
8
+
9
+ ```sh
10
+ gem install ruby-monads
11
+ ```
12
+
13
+ As these are just some new abstractions, just include it:
14
+
15
+ ```ruby
16
+ require "monads"
17
+
18
+ include Monads
19
+ ```
20
+
21
+ ## Monads
22
+
23
+ ### Maybe
24
+
25
+ The maybe monad returns one of the following classes, wether it wraps a value or not:
26
+
27
+ - `Maybe.unit(42)` returns an instance of `Just` wrapping the `42` value
28
+ - `Maybe.unit(nil)` returns an instance of `Nothing` wrapping no value
29
+
30
+ #### Examples
31
+
32
+ ```ruby
33
+ just = Maybe.unit("Hello, World!") # => #<Monads::Just @value="Hello, World!">
34
+ nothing = Maybe.unit(nil) # => #<Monads::Nothing>
35
+
36
+ just.upcase # => #<Monads::Just @value="HELLO, WORLD!">
37
+ just.upcase.split.unwrap([]) # => ["HELLO,", "WORLD!"]
38
+ just.bind { |v| Maybe.unit(nil) } # => #<Monads::Nothing>
39
+ just.fmap { |v| v.gsub(/\w/, "*") } # => #<Monads::Just @value="*****, *****!">
40
+ Maybe.unit(just).join # => #<Monads::Just @value="Hello, World!">
41
+
42
+ nothing.upcase # => #<Monads::Nothing>
43
+ nothing.upcase.split.unwrap([]) # => []
44
+ nothing.bind { |v| just } # => #<Monads::Nothing>
45
+ nothing.fmap { |v| v.gsub(/\w/, "*") } # => #<Monads::Nothing>
46
+ Maybe.unit(nothing).join # => #<Monads::Nothing>
47
+ ```
48
+
49
+ ## Why this gem
50
+
51
+ This gem is heavily inspired by the following monads implementations:
52
+
53
+ - [Monadic](https://github.com/pzol/monadic)
54
+ - [Monads](https://github.com/tomstuart/monads)
55
+
56
+ These gems, and many others are really great, and it was very instructive to learn about this topic in my beloved language. However, after reading a lot of implementations and articles, I had a clear opinion of how I wanted it:
57
+
58
+ - _Minimalist:_ as simple an lightweight as possible
59
+ - _Respectful of uses:_ respectful of the usual terms ([Monad (functional programming)](https://en.wikipedia.org/wiki/Monad_(functional_programming))), like `Maybe`, `unit`, `bind`, `join`, etc.
60
+ - _Vanilla Ruby syntax:_ I love some of the abstractions I discovered (`>=` operator, `Just(value)` syntax). However, I also love vanilla Ruby. So, I wanted to target new methods and abstractions, not new syntaxes comming from other languages.
61
+
62
+ ## Tests
63
+
64
+ Test suite uses [cutest](https://github.com/djanowski/cutest). You can execute it with:
65
+
66
+ ```sh
67
+ make
68
+ ```
@@ -0,0 +1,4 @@
1
+ require_relative "monads/monad"
2
+
3
+ require_relative "monads/maybe"
4
+ require_relative "monads/result"
@@ -0,0 +1,47 @@
1
+ module Monads
2
+ class Maybe
3
+ include Monads::Monad
4
+
5
+ # unit :: a -> M a
6
+ def self.unit(value)
7
+ value.nil? || value.is_a?(Nothing) ? Nothing.new : Just.new(value)
8
+ end
9
+
10
+ # bind :: (a -> M b) -> M a -> M b
11
+ def bind(&block)
12
+ ensure_monadic_result(&block).call
13
+ end
14
+
15
+ # unwrap :: a -> M a -> a
16
+ def unwrap(default_value)
17
+ @value || default_value
18
+ end
19
+
20
+ private
21
+
22
+ def monad_type
23
+ Maybe
24
+ end
25
+ end
26
+
27
+ class Just < Maybe
28
+ public_class_method :new
29
+
30
+ def self.new(value)
31
+ raise TypeError, "value should not be of #{value.class}" if value.nil?
32
+ super
33
+ end
34
+ end
35
+
36
+ class Nothing < Maybe
37
+ public_class_method :new
38
+
39
+ def initialize
40
+ end
41
+
42
+ # bind :: (a -> M b) -> M a -> M b
43
+ def bind(&block)
44
+ self
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,44 @@
1
+ module Monads
2
+ module Monad
3
+ def self.included(base)
4
+ base.class_eval do
5
+ private_class_method :new
6
+ end
7
+ end
8
+
9
+ def initialize(value)
10
+ @value = value
11
+ end
12
+
13
+ # fmap :: (a -> b) -> M a -> M b
14
+ def fmap(&block)
15
+ bind do |value|
16
+ self.class.unit(block.call(value))
17
+ end
18
+ end
19
+
20
+ # join :: M (M a) -> M a
21
+ def join
22
+ value = @value.is_a?(monad_type) ? @value.unwrap(nil) : @value
23
+ monad_type.unit(value)
24
+ end
25
+
26
+ def method_missing(method, *args, &block)
27
+ fmap do |value|
28
+ value.public_send(method, *args, &block)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def ensure_monadic_result(&block)
35
+ proc do
36
+ block.call(@value).tap do |result|
37
+ unless result.is_a?(monad_type)
38
+ raise TypeError, "block must return #{monad_type.name}"
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
1
+ module Monads
2
+ class Result
3
+ include Monads::Monad
4
+
5
+ FAILURE_TRIGGER = StandardError
6
+
7
+ # unit :: a -> M a
8
+ def self.unit(value)
9
+ value.is_a?(FAILURE_TRIGGER) ? Failure.new(value) : Success.new(value)
10
+ end
11
+
12
+ # bind :: (a -> M b) -> M a -> M b
13
+ def bind(&block)
14
+ return self if is_a?(Failure)
15
+
16
+ begin
17
+ ensure_monadic_result(&block).call
18
+ rescue FAILURE_TRIGGER => error
19
+ Failure.new(error)
20
+ end
21
+ end
22
+
23
+ # unwrap :: a -> M a -> a
24
+ def unwrap(default_value = @value)
25
+ is_a?(Failure) ? default_value : @value
26
+ end
27
+
28
+ private
29
+
30
+ def monad_type
31
+ Result
32
+ end
33
+ end
34
+
35
+ class Success < Result
36
+ public_class_method :new
37
+
38
+ def self.new(value)
39
+ raise TypeError, "value should not be of #{value.class}" if value.is_a?(FAILURE_TRIGGER)
40
+ super
41
+ end
42
+ end
43
+
44
+ class Failure < Result
45
+ public_class_method :new
46
+
47
+ def self.new(value)
48
+ raise TypeError, "value should not be of #{value.class}" unless value.is_a?(FAILURE_TRIGGER)
49
+ super
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "ruby-monads"
3
+ s.version = "0.1.0"
4
+ s.summary = "Some common monads"
5
+ s.description = "Simple and minimalist ruby implementation of some common monads"
6
+ s.author = "Guillaume BOUDON"
7
+ s.email = "guillaumeboudon@gmail.com"
8
+ s.homepage = "https://github.com/guillaumeboudon/ruby-monads"
9
+ s.license = "MIT"
10
+
11
+ s.files = `git ls-files`.split("\n")
12
+
13
+ s.add_development_dependency "cutest", "~> 1.2", ">= 1.2.3"
14
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "../lib/monads"
2
+
3
+ include Monads
@@ -0,0 +1,63 @@
1
+ print "\n\"Maybe\" monad test suite"
2
+
3
+ setup do
4
+ [Maybe.unit("Hello, World!"), Maybe.unit(nil)]
5
+ end
6
+
7
+ test "Maybe::new" do
8
+ assert_raise NoMethodError do Maybe.new(42) end
9
+
10
+ assert_raise TypeError do Just.new(nil) end
11
+ assert Just.new(42).is_a?(Just)
12
+
13
+ assert_raise ArgumentError do Nothing.new(42) end
14
+ assert Nothing.new.is_a?(Nothing)
15
+ end
16
+
17
+ test "Maybe::unit" do |just, nothing|
18
+ assert just.is_a?(Maybe)
19
+ assert just.is_a?(Just)
20
+ assert !just.is_a?(Nothing)
21
+
22
+ assert nothing.is_a?(Maybe)
23
+ assert !nothing.is_a?(Just)
24
+ assert nothing.is_a?(Nothing)
25
+
26
+ assert Maybe.unit(just).is_a?(Just)
27
+ assert Maybe.unit(nothing).is_a?(Nothing)
28
+ end
29
+
30
+ test "Maybe#bind" do |just, nothing|
31
+ assert just.bind { |v| Maybe.unit(v.to_sym) }.is_a?(Just)
32
+ assert just.bind { |v| Maybe.unit(nil) }.is_a?(Nothing)
33
+ assert_raise TypeError do just.bind { |v| v.to_sym } end
34
+
35
+ assert nothing.bind { |v| Maybe.unit(42) }.is_a?(Nothing)
36
+ assert nothing.bind { |v| Maybe.unit(nil) }.is_a?(Nothing)
37
+ assert nothing.bind { |v| v.to_sym }.is_a?(Nothing)
38
+ end
39
+
40
+ test "Maybe#fmap" do |just, nothing|
41
+ assert just.fmap(&:to_sym).is_a?(Just)
42
+ assert just.fmap { |v| nil }.is_a?(Nothing)
43
+
44
+ assert nothing.fmap(&:to_sym).is_a?(Nothing)
45
+ assert nothing.fmap { |v| 42 }.is_a?(Nothing)
46
+ end
47
+
48
+ test "Maybe#join" do |just, nothing|
49
+ assert_equal Maybe.unit(just).join.unwrap("default"), "Hello, World!"
50
+ assert_equal Maybe.unit(nothing).join.unwrap("default"), "default"
51
+ end
52
+
53
+ test "Maybe#unwrap" do |just, nothing|
54
+ assert_equal just.unwrap("default"), "Hello, World!"
55
+ assert_equal just.unwrap(nil), "Hello, World!"
56
+ assert_equal nothing.unwrap("default"), "default"
57
+ assert_equal nothing.unwrap(nil), nil
58
+ end
59
+
60
+ test "Maybe accepts methods chaining" do |just, nothing|
61
+ assert_equal just.upcase.reverse.split.unwrap([]), ["!DLROW", ",OLLEH"]
62
+ assert_equal nothing.upcase.reverse.split.unwrap([]), []
63
+ end
metadata ADDED
@@ -0,0 +1,74 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ruby-monads
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Guillaume BOUDON
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cutest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.2'
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.2.3
23
+ type: :development
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: '1.2'
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.2.3
33
+ description: Simple and minimalist ruby implementation of some common monads
34
+ email: guillaumeboudon@gmail.com
35
+ executables: []
36
+ extensions: []
37
+ extra_rdoc_files: []
38
+ files:
39
+ - ".gems"
40
+ - ".gitignore"
41
+ - LICENSE
42
+ - Makefile
43
+ - README.md
44
+ - lib/monads.rb
45
+ - lib/monads/maybe.rb
46
+ - lib/monads/monad.rb
47
+ - lib/monads/result.rb
48
+ - ruby-monads.gemspec
49
+ - test/helper.rb
50
+ - test/maybe_test.rb
51
+ homepage: https://github.com/guillaumeboudon/ruby-monads
52
+ licenses:
53
+ - MIT
54
+ metadata: {}
55
+ post_install_message:
56
+ rdoc_options: []
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ requirements: []
70
+ rubygems_version: 3.1.2
71
+ signing_key:
72
+ specification_version: 4
73
+ summary: Some common monads
74
+ test_files: []