Last active
January 11, 2021 01:12
-
-
Save madlep/4d2b90184cded2b19ae658ab1ec08bbd to your computer and use it in GitHub Desktop.
Ruby validation applicative example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@JoshCheek
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 forData.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
orFindResultVisitor
, and that would then be passed to the curriedPerson
constructor?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: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 argumentsEither
value with either:Right
containing the value of successful validation of some valueLeft
containing the error of that validationIn Haskell, it's not an issue, as the typeclass function of
<*>
(akaap
in the initial example) forApplicative
looks something like:Which basically says that if you want to treat an
Either
value (or any other type) as anApplicative
, 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 anApplicative
value, and can't callpure
or<*>
etc on it.In Ruby, you need to manually keep track of that by groking the code without any compiler help.