funkr 0.0.23 → 0.0.24
Sign up to get free protection for your applications and to get access to all the features.
- data/README +13 -0
- data/lib/funkr.rb +12 -2
- data/lib/funkr/adt/adt.rb +9 -6
- data/lib/funkr/adt/matcher.rb +36 -25
- data/lib/funkr/categories/alternative.rb +4 -0
- data/lib/funkr/categories/applicative.rb +11 -1
- data/lib/funkr/categories/functor.rb +6 -1
- data/lib/funkr/categories/monad.rb +15 -1
- data/lib/funkr/categories/monoid.rb +2 -1
- data/lib/funkr/types/maybe.rb +63 -20
- data/lib/funkr/types/simple_record.rb +24 -9
- data/lib/funkr/version.rb +1 -1
- data/test/test_maybe.rb +2 -1
- metadata +52 -30
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).
|
data/lib/funkr.rb
CHANGED
@@ -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
|
data/lib/funkr/adt/adt.rb
CHANGED
@@ -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
|
-
|
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(
|
62
|
+
@matcher = Class.new(MATCHER) do
|
60
63
|
build_matchers(constructs)
|
61
64
|
end
|
62
65
|
end
|
data/lib/funkr/adt/matcher.rb
CHANGED
@@ -1,34 +1,45 @@
|
|
1
1
|
module Funkr
|
2
|
+
|
2
3
|
# Should not be used directly
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
constructs
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
@
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
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
|
data/lib/funkr/types/maybe.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
31
|
-
|
32
|
-
|
52
|
+
#
|
53
|
+
# {Funkr::Categories::Applicative#apply see applicative apply}
|
33
54
|
def apply(to)
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
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)
|
data/lib/funkr/version.rb
CHANGED
data/test/test_maybe.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
5
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
76
|
-
|
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
|
-
|
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.
|
105
|
+
rubygems_version: 1.3.7
|
85
106
|
signing_key:
|
86
107
|
specification_version: 3
|
87
|
-
summary:
|
108
|
+
summary: "[EXPERIMENTAL] Some functionnal constructs for ruby"
|
88
109
|
test_files: []
|
110
|
+
|