Sometimes it’s fun to do something completely useless.
Recently, I wrote a post about how awesome the Maybe type is in Haskell. In the post, I talked about Functors and Monads and how Maybe can help us understand them.
Shortly thereafter, I was bored on the train one day and decided to implement Maybe and its functor instance in ruby.
obj.(args) is translated
to obj.call(args) in newer rubies. I find it makes the example read
better.
Maybe ๐
So we need an object that can represent “Just something” or “Nothing”.
Ruby already has the concept of nil, so we’ll piggy back on that and
just wrap it all in some sugar.
class Maybe
def initialize(value)
@value = value
end
def nothing?
@value.nil?
end
def just?
!nothing?
end
def value
if just?
@value
else
raise "Can't get something from nothing."
end
end
# we'll need this to prove the laws
def ==(other)
if just? && other.just?
return value == other.value
end
nothing? && other.nothing?
end
end
def Just(x)
raise "Can't make something from nothing." if x.nil?
Maybe.new(x)
end
Nothing = Maybe.new(nil)
Functions ๐
We can’t map functions to methods because methods need targets, they
can’t stand on their own. As an example, take id (which we’ll be using
later on). One might be tempted to define it like this:
def id(x)
x
end
This won’t work for our purposes since that method (defined on the
global object Object) can’t be passed around, partially applied or
composed.
It’s more convenient to do it like this:
# ruby 1.9
id = ->(x) { x }
# ruby 1.8
id = lambda { |x| x }
Now you’ve got an isolated, callable id object which you can pass
around.
Partial Application ๐
Functions need to be partially applied. That means you can give a function a few of the arguments it expects and get back another function which you can then pass around and eventually call with the additional arguments given at that later point:
class Partial
def initialize(f, *args)
@f, @args = f, args
end
def call(*args)
new_args = @args + args
@f.(*new_args)
end
end
def partial(f, *args)
Partial.new(f, *args)
end
max = ->(x,y) { x >= y ? x : y }
max.(4, 5) # => 5
max5 = partial(max, 5)
max5.(6) # => 6
max5.(4) # => 5
[4, 5, 6].map { |i| max5.(i) } # => [5, 5, 6]
Composition ๐
Two functions, when composed together, return a new function which represents the first being applied to the result of the second being applied to the argument given.
class Compose
def initialize(f, g)
@f, @g = f, g
end
def call(x)
@f.( @g.( x ) )
end
end
def compose(f, g)
Compose.new(f, g)
end
get_len = ->(s) { s.length }
add_bar = ->(s) { s + "_bar" }
get_len_with_bar = compose(get_len, add_bar)
get_len_with_bar.("foo") # => 7
Functor ๐
Now that we can define functions, partially apply them and compose them
together, we can finally prove the Functor laws for our new Maybe
class.
Let’s start by defining fmap, just as it is in Haskell:
# fmap f (Just x) = Just (f x)
# fmap _ Nothing = Nothing
fmap = ->(f, x) do
if x.just?
Just(f.(x.value))
else
Nothing
end
end
fmap’s behavior is type-dependant. So a real
implementation (for some definition of “real”) would probably make a
method on Object which needs to be overridden by any classes that are
proper Functors. We won’t worry about that here…
First law, the identity operation must behave the same when it’s
fmapped.
id = ->(x) { x }
fmap_id = partial(fmap, id)
# fmap id = id
fmap_id.(Nothing) == id.(Nothing) # => true
fmap_id.(Just("foo")) == id.(Just("foo")) # => true
So far so good.
Second law, fmapping a composed function is no different than
composing the result of each function fmapped separately.
f = ->(s) { s + "_bar" }
g = ->(s) { s.length }
f_g = compose(f, g)
fmap_f_g = partial(fmap, f_g)
fmap_f = partial(fmap, f)
fmap_g = partial(fmap, g)
fmap_f_fmap_g = compose(fmap_f, fmap_g)
# fmap (f . g) == fmap f . fmap g
fmap_f_g.(Nothing) == fmap_f_fmap_g.(Nothing) # => true
fmap_f_g.(Just("foo")) == fmap_f_fmap_g.(Just("foo") # => true
As suspected, our new Ruby-Maybe is a proper Functor.
Monad? ๐
Is our class a Monad?
# >>=
f = ->(ma, f) do
if ma.just?
f.(ma.value)
else
Nothing
end
end
# >>
f = ->(ma, mb) do
if ma.just?
mb
else
Nothing
end
end
# return
f = ->(x) do
Just(x)
end
# fail
f = -> do
Nothing
end
Proving the laws is left as an exercise to the reader…