Skip to content

Instantly share code, notes, and snippets.

@kennyfrc
Last active March 3, 2023 16:22
Show Gist options
  • Save kennyfrc/c332be77f10d65e0b48162ac93ddc25a to your computer and use it in GitHub Desktop.
Save kennyfrc/c332be77f10d65e0b48162ac93ddc25a to your computer and use it in GitHub Desktop.
Ruby Example of Railway Oriented Programming
require 'json'
##
# Combinator module
# contains .map, .bind, .apply, .compose
# a toolkit for building functional programs
##
module Combinator
Result = Data.define(:success, :error, :value) do
def call(&block)
if self.success
self.value
else
self.error
end
end
def tee(&block)
if self.success
p block.call(self.value)
end
self
end
def map(&block)
if self.success
Result.new(true, nil, block.call(self.value))
else
self
end
end
def bind(&block)
if self.success
lambda { Result.new(true, nil, block.call(self.value)) }
else
Result.new(false, "#{self.error}, line no: #{__FILE__}", nil)
end
end
def apply(&block)
if self.success
bind { block.call(self.value) }.call
else
self
end
end
end
def self.map(&block)
begin
Result.new(true, nil, block.call)
rescue => e
Result.new(false, e, nil)
end
end
def self.bind(&block)
lambda { |x| Result.new(true, nil, block.call(x)) }
end
def self.apply(&block)
bind { |x| block.call(x) }.call
end
def self.compose(method1, method2)
lambda { |x| Result.new(true, nil, method1.call(x)).apply { |y| method2.call(y) } }
end
end
##
# validate_input, canonicalize_email!, encrypt_email, update_db!, log
# these are the methods defined to demonstrate the use of the module
# in a web application context
##
def validate_input(user)
if user.email.nil? || user.email.strip.empty?
raise "ValidationError: Email is Blank, line no: #{__FILE__}/#{__LINE__}"
end
if user.name.length > 50
raise "ValidationError: Name is too long, line no: #{__FILE__}/#{__LINE__}"
end
if user.email !~ /@.*\./
raise "ValidationError: Email is invalid, line no: #{__FILE__}/#{__LINE__}"
end
if user.name.nil? || user.name.strip.empty?
raise "ValidationError: Name is Blank, line no: #{__FILE__}/#{__LINE__}"
end
user
end
def canonicalize_email!(user)
User.new(user.name, user.email.downcase)
end
def encrypt_email(user)
User.new(user.name, user.email.reverse)
end
def update_db!(user)
# let's pretend there's mutation here
"Updating DB with #{user.name} and #{user.email}"
end
def log(user)
"Logging user #{user.name}"
end
##
# Example usage
# imagine a web app where the user is created
##
User = Data.define(:name, :email)
user = User.new("John", "john@example.com")
result = Combinator
.map { validate_input(user) }
.map { |user| canonicalize_email!(user) }
.tee { |user| update_db!(user) }
.tee { |user| log(user) }
if result.success
json = { "status" => 200, "data" => { "message": "User of name: #{result.value.name} and email: #{result.value.email} created" } }.to_json
puts json
else
json = { "status" => 400, "data" => { "error": result.error } }.to_json
puts json
end
@kennyfrc
Copy link
Author

kennyfrc commented Mar 1, 2023

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