GithubHelp home page GithubHelp logo

kleisli's Introduction

Kleisli Build Status

An idiomatic, clean implementation of a few common useful monads in Ruby, written by Ryan Levick and me.

It aims to be idiomatic Ruby to use in Enter-Prise production apps, not a proof of concept.

In your Gemfile:

gem 'kleisli'

We would like to thank Curry and Howard for their correspondence.

Notation

For all its monads, Kleisli implements return (we call it lift instead, as return is a reserved keyword in Ruby) with convenience global methods (see which for each monad below).

Kleisli uses a clever Ruby syntax trick to implement the bind operator, which looks like this: >-> when used with a block. We will probably burn in hell for this. You can also use > or >> if you're going to pass in a proc or lambda object.

Maybe and Either are applicative functors with the apply operator *. Read further to see how it works.

Function composition

You can use Haskell-like function composition with F and the familiar .. This is such a perversion of Ruby syntax that Matz would probably condemn this:

Think of F as the identity function. Although it's just a hack to make it work in Ruby.

# Reminder that (f . g) x= f(g(x))
f = F . first . last
f.call [[1,2], [3,4]]
# => 3

f = F . capitalize . reverse
f.call "hello"
# => "Olleh"

Functions and methods are interchangeable:

foo = lambda { |s| s.reverse }

f = F . capitalize . fn(&foo)
f.call "hello"
# => "Olleh"

All functions and methods are partially applicable:

# Partially applied method:
f = F . split(":") . strip
f.call "  localhost:9092     "
# => ["localhost", "9092"]

# Partially applied lambda:
my_split = lambda { |str, *args| str.split(*args) }
f = F . fn(":", &my_split) . strip
f.call "  localhost:9092     "
# => ["localhost", "9092"]

Finally, for convenience, F is the identity function:

F.call(1) # => 1

Maybe monad

The Maybe monad is useful to express a pipeline of computations that might return nil at any point. user.address.street anyone?

>-> (bind)

require "kleisli"

maybe_user = Maybe(user) >-> user {
  Maybe(user.address) } >-> address {
    Maybe(address.street) }

# If user exists
# => Some("Monad Street")
# If user is nil
# => None()

# You can also use Some and None as type constructors yourself.
x = Some(10)
y = None()

As usual (with Maybe and Either), using point-free style is much cleaner:

Maybe(user) >> F . fn(&Maybe) . address >> F . fn(&Maybe) . street

fmap

require "kleisli"

# If we know that a user always has an address with a street
Maybe(user).fmap(&:address).fmap(&:street)

# If the user exists
# => Some("Monad Street")
# If the user is nil
# => None()

* (applicative functor's apply)

require "kleisli"

add = -> x, y { x + y }
Some(add) * Some(10) * Some(2)
# => Some(12)
Some(add) * None() * Some(2)
# => None

Try

The Try monad is useful to express a pipeline of computations that might throw an exception at any point.

>-> (bind)

require "kleisli"

json_string = get_json_from_somewhere

result = Try { JSON.parse(json_string) } >-> json {
  Try { json["dividend"].to_i / json["divisor"].to_i }
}

# If no exception was thrown:

result       # => #<Try::Success @value=123>
result.value # => 123

# If there was a ZeroDivisionError exception for example:

result           # => #<Try::Failure @exception=#<ZeroDivisionError ...>>
result.exception # => #<ZeroDivisionError ...>

fmap

require "kleisli"

Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).value

# If everything went well:
# => { :my => "json", :with => "symbolized keys" }
# If an exception was thrown:
# => nil

to_maybe

Sometimes it's useful to interleave both Try and Maybe. To convert a Try into a Maybe you can use to_maybe:

require "kleisli"

Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_maybe

# If everything went well:
# => Some({ :my => "json", :with => "symbolized keys" })
# If an exception was thrown:
# => None()

to_either

Sometimes it's useful to interleave both Try and Either. To convert a Try into a Either you can use to_either:

require "kleisli"

Try { JSON.parse(json_string) }.fmap(&:symbolize_keys).to_either

# If everything went well:
# => Right({ :my => "json", :with => "symbolized keys" })
# If an exception was thrown:
# => Left(#<JSON::ParserError: 757: unexpected token at 'json'>)

Either

The Either monad is useful to express a pipeline of computations that might return an error object with some information.

It has two type constructors: Right and Left. As a useful mnemonic, Right is for when everything went "right" and Left is used for errors.

Think of it as exceptions without messing with the call stack.

>-> (bind)

require "kleisli"

result = Right(3) >-> value {
  if value > 1
    Right(value + 3)
  else
    Left("value was less or equal than 1")
  end
} >-> value {
  if value % 2 == 0
    Right(value * 2)
  else
    Left("value was not even")
  end
}

# If everything went well
result # => Right(12)
result.value # => 12

# If it failed in the first block
result # => Left("value was less or equal than 1")
result.value # => "value was less or equal than 1"

# If it failed in the second block
result # => Left("value was not even")
result.value # => "value was not even"

# Point-free style bind!
result = Right(3) >> F . fn(&Right) . *(2)
result # => Right(6)
result.value # => 6

fmap

require "kleisli"

result = if foo > bar
  Right(10)
else
  Left("wrong")
end.fmap { |x| x * 2 }

# If everything went well
result # => Right(20)
# If it didn't
result # => Left("wrong")

* (applicative functor's apply)

require "kleisli"

add = -> x, y { x + y }
Right(add) * Right(10) * Right(2)
# => Right(12)
Right(add) * Left("error") * Right(2)
# => Left("error")

or

or does pretty much what would you expect:

require 'kleisli'

Right(10).or(Right(999)) # => Right(10)
Left("error").or(Left("new error")) # => Left("new error")
Left("error").or { |err| Left("new #{err}") } # => Left("new error")

to_maybe

Sometimes it's useful to turn an Either into a Maybe. You can use to_maybe for that:

require "kleisli"

result = if foo > bar
  Right(10)
else
  Left("wrong")
end.to_maybe

# If everything went well:
result # => Some(10)
# If it didn't
result # => None()

Future

The Future monad models a pipeline of computations that will happen in the future, as soon as the value needed for each step is available. It is useful to model, for example, a sequential chain of HTTP calls.

There's a catch unfortunately -- values passed to the functions are wrapped in lambdas, so you need to call .call on them. See the examples below.

>-> (bind)

require "kleisli"

f = Future("myendpoint.com") >-> url {
  Future { HTTP.get(url.call) }
} >-> response {
  Future {
    other_url = JSON.parse(response.call.body)[:other_url]
    HTTP.get(other_url)
  }
} >-> other_response {
  Future { JSON.parse(other_response.call.body) }
}

# Do some other stuff...

f.await # => block until the whole pipeline is realized
# => { "my" => "response body" }

fmap

require "kleisli"

Future { expensive_operation }.fmap { |x| x * 2 }.await
# => result of expensive_operation * 2

Who's this

This was made by Josep M. Bach (Txus) and Ryan Levick under the MIT license. We are @txustice and @itchyankles on twitter (where you should probably follow us!).

kleisli's People

Contributors

franckverrot avatar machisuji avatar maser avatar mrcasals avatar olleolleolle avatar timriley avatar txus avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

kleisli's Issues

Shouldn't Maybe(None) be None?

I don't know if there's some obvious mistake I'm missing, but I feel that Maybe(nil) == Maybe(None) but that's not the case. Is this on purpose and I'm missing the reason? Thanks!

This is related to my question here: dry-rb/dry-types#35

CI: Make it pass builds

Saw this in output after #30

Resolving dependencies...
Bundler could not find compatible versions for gem "bundler":
  In Gemfile:
    bundler (~> 1.6)

  Current Bundler version:
    bundler (2.3.26)

Store stack trace in None and Left

It would be useful for debugging to store a stack trace when None or Left are initialized. Especially None can sometimes survive a long way until we need to unwrap it and then we don't have any clue how we ended up with it.

Is this something that could be potentially merged?

Kleisli::Try#to_either

I’d like to do this:

xml_string = "…"
Try {
  Nokogiri::XML(xml_string)
}.to_either >-> doc {
  text = doc.xpath
  if text
    Right(text)
  else
    Left("no text found")
  end
}

Would this even be a good idea? If you agree that it would be useful, I’ll implement it and send you a PR.

A Bit Confused By Your Example

Looking good!

I'm a bit confused by your example:

MyOwnIdentityMonad = Kleisli::Monad.create do
  def bind(value, &fn)
    fn.call value
  end

  def result(value)
    value
  end
end

MyOwnIdentityMonad.run "{a => 2,
                         b => a.succ}",
                         "a * b"

This is what a ad hoc implementation of Identity looks like in Haskell

newtype MyIdentity a = MyIdentity { runMyIdentity :: a }

instance Monad MyIdentity where
  MyIdentity x >>= fn = fn x
  return x = MyIdentity x

main :: IO ()
main = print $ runMyIdentity example -- prints "6"

example :: MyIdentity Int
example = do a <- return 2
             b <- return $ succ x
             return $ a * b -- returns MyIdentity 6

What I'm confused about is what a => 2 means. Is it supposed to align with the a <- return 2 part of the Haskell code? If so, how can you bind the literal 2 to a? The way I'm understanding the code is that your example is short for:

2 >>= { |a| a.succ >>= {|b| a * b}}

This doesn't make sense as the type of 2 is Integer and not Identity Int. Am I missing something?

Gem release

I've got a gem that depends on kleisli that could benefit from the Try#to_either change. Would it be possible to get a new gem version released that contains that change?

Optional bind variable

I was playing around with your gem and wanted to know if it's possible to not set a variable to hold the binded value when using the Try monad?

Here is a piece of code to illustrate:

require 'kleisli'

class A

  def call
    Try { foo } >-> _ { Try { bar } }
  end

  def foo
    if rand(3) == 1
      raise 'foo raised'
    else
      true
    end
  end

  def bar
    if rand(3) == 1
      raise 'bar raised'
    else
      true
    end
  end

end

r = A.new.call

puts r.value
puts r.exception

Since I don't use the value returned by the foo method, I used the _ syntax. But I was wondering if there is a nicer way to "chain" the foo and bar method without the need to set the intermediate variables?

Also, is there a better way to chain those methods? Something that would look like this:

Try { foo } >-> Try { bar }

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.