• Stars
    star
    384
  • Rank 111,726 (Top 3 %)
  • Language
    Ruby
  • License
    MIT License
  • Created over 10 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Usable, idiomatic common monads in Ruby

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!).

More Repositories

1

explain

Explain explains your Ruby code in natural language.
Ruby
109
star
2

aversion

Make your Ruby objects versionable
Ruby
79
star
3

adts

Algebraic Data Types for Ruby
Ruby
74
star
4

fuby

Fuby is a hybrid functional/object-oriented programming language on the Rubinius VM
Ruby
62
star
5

mutant

Mutant is a mutation tester. It modifies your code and runs your tests to make sure they fail. The idea is that if code can be changed and your tests don't notice, either that code isn't being covered or it doesn't do anything. This is a rewrite on top of Rubinius.
Ruby
47
star
6

micetrap

Catch evil hackers on the fly by placing open-port traps emulating fake vulnerable services!
Ruby
46
star
7

why

Traceable business logic with decision trees -- boolean algebra on steroids
Clojure
43
star
8

terrorvm

Lightweight, fast Virtual Machine for dynamic, object-oriented languages.
C
42
star
9

saga

Programming language for interactive fiction
CSS
40
star
10

microvm

Stack-based micro (< 150 LOC) virtual machine written in Ruby, running its own micro bytecode format called MC.
Ruby
38
star
11

lambra

Lambra is an experiment to implement a functional, distributed Lisp on the Rubinius VM.
Ruby
36
star
12

traitor

Traits for Ruby 2.0: like mixins, but better
Ruby
32
star
13

rexpl

An interactive bytecode console for Rubinius
Ruby
31
star
14

calc

Simple arithmetic language interpreter with JIT compilation, using LLVM.
C
25
star
15

hijacker

Spy on your ruby objects and send their activity to a hijacker server anywhere through DRb.
Ruby
23
star
16

jam

[WIP] A MIDI-playable modular synth powered by WebAssembly, Rust, WebAudioAPI and Vuejs
Rust
23
star
17

noscript

Noscript is an object-oriented, class-less language running on the Rubinius VM.
Ruby
22
star
18

brainfuck

An implementation of Brainfuck on the Rubinius VM.
Ruby
19
star
19

libtreadmill

An implementation of Baker's Treadmill Garbage Collector.
C
19
star
20

funes

Infer the general shape of data and produce a schema from it
Clojure
18
star
21

oldterror

Terror-based VM.
C
17
star
22

niki

A Ruby DSL to describe and play MIDI songs.
Ruby
15
star
23

to_source

to_source is a reverse parser: it transforms Rubinius' AST nodes back to source code.
Ruby
15
star
24

fastest

th fstst tstng frmwrk - nly 6 LOC
Ruby
12
star
25

assoc.js

Associative arrays for JavaScript
JavaScript
9
star
26

libsweeper

Simple Mark and Sweep garbage collector library
C
8
star
27

minecraft-cookbook

Chef cookbook to install and configure a Minecraft server.
Ruby
8
star
28

domodoro

Distributed pomodoro for the masses
Ruby
7
star
29

schemer

A Scheme interpreter in Ruby
Ruby
7
star
30

yo

Street-oriented programming in Ruby
Ruby
7
star
31

mayl

A console to edit and maintain YAML files for any Ruby project
Ruby
7
star
32

revolver

Programmers are expendable
Ruby
6
star
33

cljs-on-unikernel-demo

Experimenting with running ClojureScript apps on a runtime.js unikernel
JavaScript
6
star
34

duplex

TCP proxy that replays traffic to a third host
C
6
star
35

shitdb.rb

Document-oriented database written in pure Ruby with lame performance as a key feature
Ruby
6
star
36

funk

An implementation of functors, applicative functors and monads on top of Clojure records, protocols and multimethods.
Clojure
6
star
37

libreg

Regular expressions library implemented as non-deterministic finite-state automata.
C
5
star
38

shitdb

Shitty key-value store in C inspired by Redis
C
5
star
39

invaders

Space Invaders clone.
Ruby
4
star
40

jargon

Keep a glossary of confusing terms always handy.
TypeScript
4
star
41

springpad

Command-line client for the Springpad API.
Ruby
4
star
42

rye

a rye tracer (WIP)
Rust
4
star
43

wepcracker

Telefonica WEP Access Points cracker
Ruby
3
star
44

gol-haskell

Game of Life in Haskell
Haskell
3
star
45

bayes-android

A Bayes network simulator for Android
Mirah
3
star
46

rubinius-memoize

Method memoization using Rubinius AST transforms
Ruby
3
star
47

memetalk

Publish auto-generated memes in Talkerapp rooms!
Ruby
3
star
48

clojureshtein

A small Clojure library to calculate similarity between two strings using their Levenshtein distance.
Clojure
3
star
49

feces

Feces is a Ruby client for ShitDB, an รผberwebscale key-value store inspired in Redis.
Ruby
3
star
50

polyglot_rails_example

Polyglot Rails example app on Rubinius + Noscript.
Ruby
2
star
51

rpn

A simple Reverse Polish Notation calculator in Ruby
Ruby
2
star
52

stendhal

A small test framework.
Ruby
2
star
53

lambada-rs

An eternal work in progress.
Rust
2
star
54

guard-stendhal

Guard::Stendhal automatically runs your specs with stendhal
Ruby
2
star
55

ract

A little actor-based language in Rust
Rust
2
star
56

txus.github.io

Blog
Ruby
1
star
57

omnext-willreceiveprops-bug

Reproducing a bug with willReceiveProps.
Clojure
1
star
58

vim-noscript

Noscript syntax highlighting for the Vim editor
Vim Script
1
star
59

regex_playground

Ruby
1
star
60

gametheory

Random game theory-related stuff.
Ruby
1
star
61

autoloading_spike

Example to show how would Autoload work well with explicit requires
Ruby
1
star
62

txustris

Ruby/Gosu based Tetris-like game.
Ruby
1
star
63

re4

Q&A over documents with knowledge graphs & ontology discovery
Python
1
star
64

emacs.d

My ~/.emacs.d folder.
Emacs Lisp
1
star
65

euler

Embarrassingly suboptimal solutions to Project Euler problems
Ruby
1
star
66

presentations

My presentations
JavaScript
1
star
67

micetrap.c

Micetrap rewritten in C.
C
1
star
68

markov-tweets

Markov chains, tweets and Clojure. WIP
Clojure
1
star
69

brainscript

Brainscript is a scripting language that compiles to clean, readable Brainfuck
Ruby
1
star
70

minitest-descriptive

Make your assertion diffs much smarter
Ruby
1
star
71

citylearn-2022

Attempt at implementing MARLISA for CityLearn 2022
Jupyter Notebook
1
star
72

showcase

A sample Rails 4.0 app (a microblogging service) that tries to decouple application logic from both the Database and Rails itself.
Ruby
1
star