Skip to content

Instantly share code, notes, and snippets.

@madlep
Last active January 11, 2021 01:12
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save madlep/4d2b90184cded2b19ae658ab1ec08bbd to your computer and use it in GitHub Desktop.
Save madlep/4d2b90184cded2b19ae658ab1ec08bbd to your computer and use it in GitHub Desktop.
Ruby validation applicative example
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">
@JoshCheek
Copy link

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 the Left class, which wants to be both sides of that interaction, the map side so that it can choose not map itself, and the ap 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:

Person = Struct.new(:name, :location, :twitter, :awesomeness).method(:new).curry(4)

["Julian", "Melbourne", "@madlep", 9001].reduce(Person, :call)
# => #<struct 
#     name="Julian",
#     location="Melbourne",
#     twitter="@madlep",
#     awesomeness=9001>

I futzed around with the naming a lot and eventually came up with this:

# Gives structs a curried call method
def Struct.call(*args)
  method(:new).curry(members.size).(*args)
end


Call = Struct.new :fn do
  def with(either)
    either.apply(fn)
  end
end

Value = Struct.new :value do
  def apply(fn)
    Call.new(fn.(@value))
  end
end

Result = Struct.new :result do
  def with(either)
    self
  end
  def apply(fn)
    self
  end
end


Person = Struct.new :name, :location, :twitter, :awesomeness

Call.(Person)
  .with(Value.("Julian"))
  .with(Value.("Melbourne"))
  .with(Value.("@madlep"))
  .with(Value.(9001))
# => #<struct Call
#     fn=#<struct Person name=nil, location=nil, twitter=nil, awesomeness=nil>>


Call.(Person)
  .with(Value.("Julian"))
  .with(Value.("Melbourne"))
  .with(Result.("'[-37.8136, 144.96332]' is not a String"))
  .with(Value.(9001))
# => #<struct Result result="'[-37.8136, 144.96332]' is not a String">

Thinking about the naming, I kind of wanted to make their relationship clearer, eg give and receive. 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:

def Struct.call(*args)
  method(:new).curry(members.size).(*args)
end


Curried = Struct.new :fn do
  def accept(visitor)
    visitor.visit(fn)
  end
end

ApplyArgVisitor = Struct.new :value do
  def visit(fn)
    Curried.new(fn.(@value))
  end
end

FindResultVisitor = Struct.new :result do
  def accept(visitor)
    self
  end
  def visit(fn)
    self
  end
end


Person = Struct.new :name, :location, :twitter, :awesomeness

Curried.(Person)
  .accept(ApplyArgVisitor.("Julian"))
  .accept(ApplyArgVisitor.("Melbourne"))
  .accept(ApplyArgVisitor.("@madlep"))
  .accept(ApplyArgVisitor.(9001))
# => #<struct Curried
#     fn=#<struct Person name=nil, location=nil, twitter=nil, awesomeness=nil>>

Curried.(Person)
  .accept(ApplyArgVisitor.("Julian"))
  .accept(ApplyArgVisitor.("Melbourne"))
  .accept(FindResultVisitor.("'[-37.8136, 144.96332]' is not a String"))
  .accept(ApplyArgVisitor.(9001))
# => #<struct FindResultVisitor
#     result="'[-37.8136, 144.96332]' is not a String">

@madlep
Copy link
Author

madlep commented Jan 11, 2021

@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 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 applicative Either value with either:
    • Right containing a partially function where everything so far as been successful, or the final result
    • Left 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 value
    • Left 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment