funkr 0.0.23 → 0.0.24

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.
data/README ADDED
@@ -0,0 +1,13 @@
1
+ Funkr brings some common functional programming constructs to ruby.
2
+
3
+ In particular, it offers a simple mechanism to create Algebraïc Data
4
+ Types and do pattern matching on them. For an exemple
5
+ implementation, {Funkr::Types see provided classes}.
6
+
7
+ It also provide modules for common categories (Monoid, Monad,
8
+ Functor, Applicative ...), and extends common types to support
9
+ categories they belongs to (Array, Hash ...). Categories can also be
10
+ used with custom types, {Funkr::Types see provided classes}.
11
+
12
+ To get started, we recommand you to read the tests, and get feets wet
13
+ with provided Algebraic Data Types (like Maybe).
@@ -1,8 +1,18 @@
1
+ # -*- coding: utf-8 -*-
1
2
  if RUBY_VERSION.split(".")[0..1] == ["1","8"] then
2
3
  require 'funkr/compat/1.8'
3
4
  end
4
5
 
5
- # Funkr brings some common functional programming constructs to ruby
6
+ # Funkr brings some common functional programming constructs to ruby.
7
+ #
8
+ # In particular, it offers a simple mechanism to create Algebraïc Data
9
+ # Types and do pattern matching on them. For an exemple
10
+ # implementation, {Funkr::Types see provided classes}.
11
+ #
12
+ # It also provide modules for common categories (Monoid, Monad,
13
+ # Functor, Applicative ...), and extends common types to support
14
+ # categories they belongs to (Array, Hash ...). Categories can also be
15
+ # used with custom types, {Funkr::Types see provided classes}.
6
16
  module Funkr
7
-
17
+
8
18
  end
@@ -6,6 +6,8 @@ module Funkr
6
6
  # declare constructors with #adt
7
7
  class ADT
8
8
 
9
+ MATCHER = Funkr::Matchers::SafeMatcher
10
+
9
11
  def initialize(const, *data)
10
12
  @const, @data = const, data
11
13
  end
@@ -25,12 +27,13 @@ module Funkr
25
27
  # on.just{|x| puts x}
26
28
  # on.nothing{ }
27
29
  # end
28
- def match
29
- m = self.class.matcher.new(normal_form)
30
- yield m
31
- m.run_match
30
+ def match(&block)
31
+ self.class.matcher.match_with(normal_form, &block)
32
32
  end
33
-
33
+
34
+ def unsafe_const; @const; end
35
+ def unsafe_data; @data; end
36
+
34
37
  def to_s
35
38
  format("{%s%s%s}",
36
39
  @const,
@@ -56,7 +59,7 @@ module Funkr
56
59
  end
57
60
 
58
61
  def self.build_matcher(constructs)
59
- @matcher = Class.new(Funkr::Matcher) do
62
+ @matcher = Class.new(MATCHER) do
60
63
  build_matchers(constructs)
61
64
  end
62
65
  end
@@ -1,34 +1,45 @@
1
1
  module Funkr
2
+
2
3
  # Should not be used directly
3
- class Matcher
4
-
5
- def initialize(match)
6
- @const, *@data = match
7
- @runner = nil
8
- @undefined = self.class.constructs.clone
9
- end
10
-
11
- def self.build_matchers(constructs)
12
- @constructs = constructs
13
- constructs.each do |c|
14
- name, *data = c
15
- define_method(name) do |&b|
16
- @undefined.delete(name)
17
- if @const == name then
18
- @runner = b
4
+ module Matchers
5
+
6
+ class SafeMatcher
7
+
8
+ def initialize(match)
9
+ @const, *@data = match
10
+ @runner = nil
11
+ @undefined = self.class.constructs.clone
12
+ end
13
+
14
+ def self.build_matchers(constructs)
15
+ @constructs = constructs
16
+ constructs.each do |c|
17
+ name, *data = c
18
+ define_method(name) do |&b|
19
+ @undefined.delete(name)
20
+ if @const == name then
21
+ @runner = b
22
+ end
19
23
  end
20
24
  end
21
25
  end
22
- end
23
-
24
- def self.constructs; @constructs; end
25
-
26
- def run_match
27
- if @undefined.any? then
28
- raise "Incomplete match, missing : #{@undefined.join(" ")}"
26
+
27
+ def self.constructs; @constructs; end
28
+
29
+ def self.match_with(normal_form) # &block
30
+ m = self.new(normal_form)
31
+ yield m
32
+ m.run_match
33
+ end
34
+
35
+ def run_match
36
+ if @undefined.any? then
37
+ raise "Incomplete match, missing : #{@undefined.join(" ")}"
38
+ end
39
+ @runner.call(*@data)
29
40
  end
30
- @runner.call(*@data)
41
+
31
42
  end
32
-
43
+
33
44
  end
34
45
  end
@@ -1,7 +1,11 @@
1
1
  module Funkr
2
2
  module Categories
3
+
4
+ # Functors for which alternative (OR) behaviour can be defined
3
5
  module Alternative
4
6
 
7
+ # Provide an alternative. The type must be as follow :
8
+ # Functor(A).or_else{ Functor(A) } : Functor(A)
5
9
  def or_else
6
10
  raise "Alternative#or_else not implemented"
7
11
  end
@@ -1,16 +1,25 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Funkr
2
3
  module Categories
4
+
5
+ # Functors that can contain a function and be applied to functors
6
+ # containing parameters for the function
3
7
  module Applicative
4
-
8
+
9
+ # Apply the function living inside the functor . The type must be as follow :
10
+ # Functor(λ(A) : B).apply(Functor(A)) : Functor(B)
5
11
  def apply
6
12
  raise "Applicative#apply not implemented"
7
13
  end
8
14
 
9
15
  module ClassMethods
16
+ # Curryfy the lambda block, and lift it into the functor
10
17
  def curry_lift_proc(&block)
11
18
  self.pure(block.curry)
12
19
  end
13
20
 
21
+ # Curryfy the lambda block over N parameter, lifting it to
22
+ # a lambda over N functors
14
23
  def full_lift_proc(&block)
15
24
  lambda do |*args|
16
25
  args.inject(curry_lift_proc(&block)) do |a,e|
@@ -19,6 +28,7 @@ module Funkr
19
28
  end
20
29
  end
21
30
 
31
+ # Lift the block and call parameters on it.
22
32
  def lift_with(*args, &block)
23
33
  full_lift_proc(&block).call(*args)
24
34
  end
@@ -1,7 +1,12 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Funkr
2
3
  module Categories
4
+
5
+ # A functor is a container that can be mapped over
3
6
  module Functor
4
-
7
+
8
+ # Map over the constructor. The type must be as follow :
9
+ # Functor(A).map{|A| λ(A) : B} : Functor(B)
5
10
  def map
6
11
  raise "Functor#map not implemented"
7
12
  end
@@ -1,7 +1,21 @@
1
+ # -*- coding: utf-8 -*-
1
2
  module Funkr
2
3
  module Categories
4
+
5
+ # A functor can also be made an instance of Monad if you can
6
+ # define a bind operation on it. The bind operation must follow
7
+ # the monads laws :
8
+ # - Functor.unit(x).bind(f) == f.call(X)
9
+ # - monad.bind{|x| Functor.unit(x)} == monad
10
+ # - monad.bind{|x| f(monad).bind(g)} == monad.bind{|x| f(x)}.bind{|x| g(x)}
11
+ #
12
+ # Usually you will want your type to be a monad if you need to
13
+ # chain functions returning your type, and want to implement
14
+ # chaining logic once and for all (in bind).
3
15
  module Monad
4
-
16
+
17
+ # Bind operation on monads. The type must be as follow :
18
+ # Monad(A).bind{|A| λ(A) : Monad(B)} : Monad(B)
5
19
  def bind(&block)
6
20
  raise "Monad#bind not implemented"
7
21
  end
@@ -1,7 +1,8 @@
1
1
  module Funkr
2
2
  module Categories
3
+ # Monoids are types that can be added and have a zero element.
3
4
  module Monoid
4
-
5
+
5
6
  def mplus
6
7
  raise "Monoid#mplus not implemented"
7
8
  end
@@ -1,8 +1,19 @@
1
+ # -*- coding: utf-8 -*-
1
2
  require 'funkr/adt/adt'
2
3
  require 'funkr/categories'
3
4
 
4
5
  module Funkr
5
6
  module Types
7
+
8
+ # Algebraïc Data Type representing the possibility of a missing
9
+ # value : nothing. It cleanly replace the 'nil' paradigm often
10
+ # found in ruby code, and often leading to bugs. The Maybe type
11
+ # belongs to multiple categories, making it extremly expressive to
12
+ # use (functor, applicative, alternative, monad, monoid ... !).
13
+ #
14
+ # You will get maximum performance and maximum expressiveness if
15
+ # you can use provided high level functions (map, apply, or_else,
16
+ # ...) instead of pattern-matching yourself.
6
17
  class Maybe < ADT
7
18
 
8
19
  include Funkr::Categories
@@ -13,13 +24,24 @@ module Funkr
13
24
 
14
25
  include Functor
15
26
 
27
+ # Maybe.nothing.map{|x| something(x)} # => nothing
28
+ # Maybe.just(x).map{|x| something(x)} # => just something(x)
29
+ #
30
+ # {Funkr::Categories::Functor#map see functor map}
16
31
  def map(&block)
17
- self.match do |on|
18
- on.just {|v| self.class.just(yield(v))}
19
- on.nothing { self }
20
- end
32
+ # This implementation isn't safe but is a bit faster than the
33
+ # safe one. A safe implementation would be as follow :
34
+ # self.match do |on|
35
+ # on.just {|v| self.class.just(yield(v))}
36
+ # on.nothing { self }
37
+ # end
38
+ if self.just? then self.class.just(yield(unsafe_content))
39
+ else self end
21
40
  end
22
41
 
42
+ include Applicative
43
+ extend Applicative::ClassMethods
44
+
23
45
  # Maybe can be made an applicative functor, for example :
24
46
  # f = Maybe.curry_lift_proc{|x,y| x + y}
25
47
  # a = Maybe.just(3)
@@ -27,23 +49,28 @@ module Funkr
27
49
  # c = Maybe.nothing
28
50
  # f.apply(a).apply(b) => Just 7
29
51
  # f.apply(a).apply(c) => Nothing
30
- include Applicative
31
- extend Applicative::ClassMethods
32
-
52
+ #
53
+ # {Funkr::Categories::Applicative#apply see applicative apply}
33
54
  def apply(to)
34
- self.match do |f_on|
35
- f_on.just do |f|
36
- to.match do |t_on|
37
- t_on.just {|t| self.class.unit(f.call(t)) }
38
- t_on.nothing { to }
39
- end
40
- end
41
- f_on.nothing { self }
42
- end
55
+ # This implementation isn't safe but is a bit faster than the
56
+ # safe one. A safe implementation would be as follow :
57
+ # self.match do |f_on|
58
+ # f_on.just do |f|
59
+ # to.match do |t_on|
60
+ # t_on.just {|t| self.class.unit(f.call(t)) }
61
+ # t_on.nothing { to }
62
+ # end
63
+ # end
64
+ # f_on.nothing { self }
65
+ # end
66
+ if self.just? and to.just? then
67
+ self.class.unit(self.unsafe_content.call(to.unsafe_content))
68
+ else self.class.nothing end
43
69
  end
44
70
 
45
71
  include Alternative
46
72
 
73
+ # {Funkr::Categories::Alternative#or_else see alternative or_else}
47
74
  def or_else(&block)
48
75
  self.match do |on|
49
76
  on.just {|v| self}
@@ -55,6 +82,7 @@ module Funkr
55
82
  include Monoid
56
83
  extend Monoid::ClassMethods
57
84
 
85
+ # {Funkr::Categories::Monoid#mplus see monoid mplus}
58
86
  def mplus(m_y)
59
87
  self.match do |x_on|
60
88
  x_on.nothing { m_y }
@@ -71,13 +99,24 @@ module Funkr
71
99
  include Monad
72
100
  extend Monad::ClassMethods
73
101
 
102
+ # {Funkr::Categories::Monad#bind see monad bind}
74
103
  def bind(&block)
75
- self.match do |on|
76
- on.just {|v| yield(v)}
77
- on.nothing {self}
78
- end
104
+ # This implementation isn't safe but is a bit faster than the
105
+ # safe one. A safe implementation would be as follow :
106
+ # self.match do |on|
107
+ # on.just {|v| yield(v)}
108
+ # on.nothing {self}
109
+ # end
110
+ if self.just? then yield(self.unsafe_content)
111
+ else self end
79
112
  end
80
113
 
114
+ # Unbox a maybe value. You must provide a default in case of
115
+ # nothing. This method is not as safe as the others, as it will
116
+ # escape the content from the Maybe type safety.
117
+ #
118
+ # Maybe.just(5).unbox(:foobar) # => 5
119
+ # Maybe.nothing.unbox(:foobar) # => :foobar
81
120
  def unbox(default=nil)
82
121
  self.match do |on|
83
122
  on.just {|v| v }
@@ -91,6 +130,10 @@ module Funkr
91
130
  alias mzero nothing
92
131
  end
93
132
 
133
+ # unsafe access to content, for performance purpose only
134
+ def unsafe_content; self.unsafe_data.first; end
135
+
136
+ # Box nil as nothing, and the rest as just x
94
137
  def self.box(value)
95
138
  if value.nil? then self.nothing
96
139
  else self.just(value) end
@@ -1,16 +1,27 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  module Funkr
3
3
  module Types
4
-
5
- class SimpleRecord < Array
6
- ### usage : r = SimpleRecord.new(Hash), then r.field
7
- ### r = SimpleRecord.new( name: "Paul", age: 27 )
8
- ### r.name => "Paul" ; r.age => 27
9
- ### name, age = r
10
4
 
11
- ### other usage :
12
- ### class Person < SimpleRecord; fields :name, :age; end
13
- ### Person.new( name: Paul ) => Error, missing :age
5
+ # Simple records are a simple way to create records with named AND
6
+ # positional fields. Records can be updated on a per-field basis
7
+ # and pattern-matched as arrays. SimpleRecord has the following
8
+ # advantages above plain Hash :
9
+ #
10
+ # - fields are strict, you can't update an unexisting field by
11
+ # mistyping it or by combining it with a different structure
12
+ # - you have easy access to fields : named AND positional
13
+ #
14
+ # usage : r = SimpleRecord.new(Hash), then r.field
15
+ # r = SimpleRecord.new( name: "Paul", age: 27 )
16
+ # r.name # => "Paul"
17
+ # r.age # => 27
18
+ # name, age = r # => [ "Paul", 27 ]
19
+ # r.with(age: 29) # => [ "Paul", 29 ]
20
+ #
21
+ # other usage :
22
+ # class Person < SimpleRecord; fields :name, :age; end
23
+ # Person.new( name: Paul ) => Error, missing :age
24
+ class SimpleRecord < Array
14
25
 
15
26
  class << self; attr_accessor :fields_list; end
16
27
  @fields_list = nil
@@ -25,6 +36,8 @@ module Funkr
25
36
  end
26
37
  end
27
38
 
39
+ # Create a new SimpleRecord. Pass a Hash of keys and associated
40
+ # values if you want an inline record.
28
41
  def initialize(key_vals)
29
42
  fields = self.class.fields_list
30
43
  if not fields.nil? then # record paramétré
@@ -46,11 +59,13 @@ module Funkr
46
59
  end
47
60
  end
48
61
 
62
+ # Update a simple record non-destructively
49
63
  def with(new_key_vals)
50
64
  check_keys(new_key_vals.keys)
51
65
  self.class.new(@key_vals.merge(new_key_vals))
52
66
  end
53
67
 
68
+ # Update a simple record destructively !!
54
69
  def update!(new_key_vals)
55
70
  check_keys(new_key_vals.keys)
56
71
  @key_vals.merge!(new_key_vals)
@@ -1,3 +1,3 @@
1
1
  module Funkr
2
- VERSION = "0.0.23"
2
+ VERSION = "0.0.24"
3
3
  end
@@ -28,7 +28,8 @@ class TestMaybe < Test::Unit::TestCase
28
28
  def test_or_else
29
29
  assert_equal(j(10), n.or_else{j(10)})
30
30
  assert_equal(j(4), j(4).or_else{j(2)})
31
- # assert_nothing_raised(j(2).or_else{raise 'should not be raised'})
31
+ assert_nothing_raised{ j(5).or_else{raise 'should not be raised'} }
32
+ assert_raise(RuntimeError){ n.or_else{raise 'should not be raised'} }
32
33
  end
33
34
 
34
35
  def test_full_lift
metadata CHANGED
@@ -1,38 +1,51 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: funkr
3
- version: !ruby/object:Gem::Version
4
- version: 0.0.23
5
- prerelease:
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 24
9
+ version: 0.0.24
6
10
  platform: ruby
7
- authors:
11
+ authors:
8
12
  - Paul Rivier
9
13
  autorequire:
10
14
  bindir: bin
11
15
  cert_chain: []
12
- date: 2011-10-21 00:00:00.000000000Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
16
+
17
+ date: 2011-12-06 00:00:00 +01:00
18
+ default_executable:
19
+ dependencies:
20
+ - !ruby/object:Gem::Dependency
15
21
  name: rake
16
- requirement: &18284740 !ruby/object:Gem::Requirement
22
+ prerelease: false
23
+ requirement: &id001 !ruby/object:Gem::Requirement
17
24
  none: false
18
- requirements:
25
+ requirements:
19
26
  - - ~>
20
- - !ruby/object:Gem::Version
27
+ - !ruby/object:Gem::Version
28
+ segments:
29
+ - 0
30
+ - 9
31
+ - 2
21
32
  version: 0.9.2
22
33
  type: :development
23
- prerelease: false
24
- version_requirements: *18284740
25
- description: ! '[EXPERIMENTAL] Some functionnal constructs for ruby, like ADT, functors,
26
- monads'
27
- email:
34
+ version_requirements: *id001
35
+ description: "[EXPERIMENTAL] Some functionnal constructs for ruby, like ADT, functors, monads"
36
+ email:
28
37
  - paul (dot) r (dot) ml (at) gmail (dot) com
29
38
  executables: []
39
+
30
40
  extensions: []
41
+
31
42
  extra_rdoc_files: []
32
- files:
43
+
44
+ files:
33
45
  - .gitignore
34
46
  - .travis.yml
35
47
  - Gemfile
48
+ - README
36
49
  - Rakefile
37
50
  - funkr.gemspec
38
51
  - lib/funkr.rb
@@ -61,28 +74,37 @@ files:
61
74
  - test/test_hash.rb
62
75
  - test/test_maybe.rb
63
76
  - test/test_simple_records.rb
77
+ has_rdoc: true
64
78
  homepage: http://github.com/paul-r-ml/funkr
65
79
  licenses: []
80
+
66
81
  post_install_message:
67
82
  rdoc_options: []
68
- require_paths:
83
+
84
+ require_paths:
69
85
  - lib
70
- required_ruby_version: !ruby/object:Gem::Requirement
86
+ required_ruby_version: !ruby/object:Gem::Requirement
71
87
  none: false
72
- requirements:
73
- - - ! '>='
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ segments:
92
+ - 0
93
+ version: "0"
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
77
95
  none: false
78
- requirements:
79
- - - ! '>='
80
- - !ruby/object:Gem::Version
81
- version: '0'
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ segments:
100
+ - 0
101
+ version: "0"
82
102
  requirements: []
103
+
83
104
  rubyforge_project: funkr
84
- rubygems_version: 1.8.10
105
+ rubygems_version: 1.3.7
85
106
  signing_key:
86
107
  specification_version: 3
87
- summary: ! '[EXPERIMENTAL] Some functionnal constructs for ruby'
108
+ summary: "[EXPERIMENTAL] Some functionnal constructs for ruby"
88
109
  test_files: []
110
+