-
-
Save madlep/4d2b90184cded2b19ae658ab1ec08bbd to your computer and use it in GitHub Desktop.
class Either | |
def self.pure(value) | |
Right.new(value) | |
end | |
class Left | |
def initialize(left) | |
@left = left | |
end | |
# aka functor map, or fmap or <$> in haskell | |
def map(_f) | |
self | |
end | |
# aka applicative map, or <*> in Haskell | |
def ap(_f) | |
# if we're an Error, don't apply the function to ourself | |
self | |
end | |
def inspect | |
"#<Left #{@left.inspect}>" | |
end | |
end | |
class Right | |
def initialize(right) | |
@right = right | |
end | |
def map(f) | |
Right.new(f.(@right)) | |
end | |
def ap(other) | |
# here's where the magic happens. @right can be a (possibly curried) | |
# function instead of a data value. So do a functor map over the other | |
# value, using OUR OWN @right value as the function to map it. If other | |
# is an Either::Right, the value contained in other.@right will be | |
# applied to our partially applied function. If it's an Either::Left, our | |
# function will be ignored, and the other value will be returned | |
# unchanged | |
other.map(@right) | |
end | |
def inspect | |
"#<Right #{@right.inspect}>" | |
end | |
end | |
end | |
def validate_str(str) | |
if String === str | |
Either::Right.new(str) | |
else | |
Either::Left.new("'#{str.inspect}' is not a String") | |
end | |
end | |
def validate_int(int) | |
if Integer === int | |
Either::Right.new(int) | |
else | |
Either::Left.new("'#{int.inspect}' is not an Int") | |
end | |
end | |
Person = Struct.new(:name, :location, :twitter, :awesomeness) | |
def person() | |
->(name) { | |
->(location) { | |
-> (twitter) { | |
-> (awesomeness) { | |
Person.new(name, location, twitter, awesomeness) | |
} | |
} | |
} | |
} | |
end | |
# plain old (curried) function call | |
puts person.("Julian").("Melbourne").("@madlep").inspect | |
# #<Proc:0x00007f984f0a94e0 applicative.rb:66 (lambda)> | |
puts person.("Julian").("Melbourne").("@madlep").(9001).inspect | |
# #<struct Person name="Julian", location="Melbourne", twitter="@madlep", awesomeness=9001> | |
# calling curried function with no arguments, wrapping it in our Either | |
# applicative - we get an applicative containing the function | |
puts Either.pure(person()) | |
.inspect | |
# #<Right #<Proc:0x00007f984f8f74f8 applicative.rb:71 (lambda)>> | |
# applying some arguments wrapped in applicatives of the same type (Either) | |
# gives us a partially applied function in the Either applicative. | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.inspect | |
# #<Right #<Proc:0x00007f984f8f7250 applicative.rb:72 (lambda)>> | |
# applying more applicative arguments... | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str("Melbourne")) | |
.inspect | |
# #<Right #<Proc:0x00007f984f8f6ee0 applicative.rb:73 (lambda)>> | |
# still more | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str("Melbourne")) | |
.ap(validate_str("@madlep")) | |
.inspect | |
# #<Right #<Proc:0x00007f984f8f6a58 applicative.rb:74 (lambda)>> | |
# and we've applied all of the argunments, and the contained function is | |
# evaluated | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str("Melbourne")) | |
.ap(validate_str("@madlep")) | |
.ap(validate_int(9001)) | |
.inspect | |
# #<Right #<struct Person name="Julian", location="Melbourne", twitter="@madlep", awesomeness=9001>> | |
# if we have errors (validation returns Either::Left value), the applicative is | |
# short circuited, and further arguments are not applied to it. | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str(nil)) | |
.ap(validate_str("@madlep")) | |
.ap(validate_int(9001)) | |
.inspect | |
# #<Left "'nil' is not a String"> | |
# more errors | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str([-37.8136, 144.96332])) | |
.ap(validate_str("@madlep")) | |
.ap(validate_int(9001)) | |
.inspect | |
# #<Left "'[-37.8136, 144.96332]' is not a String"> | |
# more errors, even when we haven't supplied all arguments result in a failure straight away | |
puts Either.pure(person()) | |
.ap(validate_str("Julian")) | |
.ap(validate_str([-37.8136, 144.96332])) | |
.inspect | |
# #<Left "'[-37.8136, 144.96332]' is not a String"> |
Interesting approach. I think I see where it's going. It's taking it in a very OOP way, from the original very FP way 🙂.
The main intent of my original example was to focus on the validation, and how that fits with the applicative functor as defined in functional languages - it's just that currying is a side issue that you need to make applicative useful in a lot of cases. It's pretty much a verbatim port of how you'd use Haskell's Control.Applicative
typeclass instance for Data.Either
if it was implemented in Ruby.
The main thing that changes in the Ruby version, is that instead of functor/applicative functions that accept the values, we implement them as methods on the object that is being dispatched on - seeing as polymorphism in Ruby is duck typed based on objects rather than ad-hoc polymorphism with type classes like in Haskell. So the first argument in the Haskell functions ends up being self
in the Ruby methods implementing the same thing.
So if I understand it right: In your changes, the validation functions would return ApplyArgVisitor
or FindResultVisitor
, and that would then be passed to the curried Person
constructor?
Okay, I played with it for about an hour, and I kinda get it now. The first thing that I think makes it confusing is that
@right
is sometimes the value (eg"Julian"
) and sometimes the function (eg the curried lambda)
Yup. A lot of that comes from taking the applicative pattern from a strongly typed functional language (like Haskell), and implementing it in a dynamic language like Ruby. It does get more confusing to keep track of what exactly is contained in the Applicative
Either
instances:
- for
person()
function evaluation, it's an applicativeEither
value with either:Right
containing a partially function where everything so far as been successful, or the final resultLeft
containing some error if it was encountered applying any of the arguments
- for validation results, it's a plain old
Either
value with either:Right
containing the value of successful validation of some valueLeft
containing the error of that validation
In Haskell, it's not an issue, as the typeclass function of <*>
(aka ap
in the initial example) for Applicative
looks something like:
class Functor f => Applicative f where
(<*>) :: f (a -> b) -> f a -> f b
-- ... other stuff not relevant to our example
Which basically says that if you want to treat an Either
value (or any other type) as an Applicative
, it has be some context with a function. If it's not, then it won't typecheck, and your code won't compile.
You can still create an Either
with whatever value you want, and that is fine, you just can't treat it as an Applicative
value, and can't call pure
or <*>
etc on it.
In Ruby, you need to manually keep track of that by groking the code without any compiler help.
Okay, I played with it for about an hour, and I kinda get it now. The first thing that I think makes it confusing is that
@right
is sometimes the value (eg"Julian"
) and sometimes the function (eg the curried lambda). So I split that into 2 different classes. That made it easier to understand, but then it didn't work for theLeft
class, which wants to be both sides of that interaction, themap
side so that it can choose not map itself, and theap
side so that it can inject itself as the new value.It's basically this, but it lets the value decide what gets returned. There isn't any value you could give here which could do what Result does above, because the value being passed to the curried call never has an opportunity to interject:
I futzed around with the naming a lot and eventually came up with this:
Thinking about the naming, I kind of wanted to make their relationship clearer, eg
give
andreceive
. Which is when I realized it reminded me of one of those GoF patterns. I don't remember which one, but glancing through them, it might have been the visitor. So it may map to ideas we've seen before if we rename them like this: